mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-16 09:58:21 +00:00
fix(iam): merge OIDC extra root CAs (#5915)
Co-authored-by: heihutu <heihutu@gmail.com>
This commit is contained in:
@@ -449,9 +449,15 @@ impl Operation for ValidateOidcConfigHandler {
|
||||
request.provider_id.trim().to_string()
|
||||
};
|
||||
let provider_config = build_provider_config_from_validate(request, &provider_id)?;
|
||||
let validation = rustfs_iam::oidc::validate_oidc_provider_config(&provider_config)
|
||||
let oidc_extra_root_ca = crate::startup_auth::current_oidc_extra_root_ca_material()
|
||||
.await
|
||||
.map_err(|e| S3Error::with_message(S3ErrorCode::InvalidRequest, format!("validation failed: {e}")))?;
|
||||
let validation = rustfs_iam::oidc::validate_oidc_provider_config_with_extra_root_ca(
|
||||
&provider_config,
|
||||
oidc_extra_root_ca.root_ca_pem.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| S3Error::with_message(S3ErrorCode::InvalidRequest, format!("validation failed: {e}")))?;
|
||||
|
||||
json_response(
|
||||
StatusCode::OK,
|
||||
|
||||
@@ -24,9 +24,10 @@ use crate::startup_runtime_sources;
|
||||
use rustfs_common::MtlsIdentityPem;
|
||||
use rustfs_config::{
|
||||
DEFAULT_SERVER_MTLS_ENABLE, DEFAULT_TLS_KEYLOG, DEFAULT_TLS_RELOAD_ENABLE, DEFAULT_TLS_RELOAD_INTERVAL,
|
||||
DEFAULT_TRUST_LEAF_CERT_AS_CA, DEFAULT_TRUST_SYSTEM_CA, ENV_MTLS_CLIENT_CERT, ENV_MTLS_CLIENT_KEY, ENV_SERVER_MTLS_ENABLE,
|
||||
ENV_TLS_KEYLOG, ENV_TLS_RELOAD_ENABLE, ENV_TLS_RELOAD_INTERVAL, ENV_TRUST_LEAF_CERT_AS_CA, ENV_TRUST_SYSTEM_CA,
|
||||
RUSTFS_CA_CERT, RUSTFS_CLIENT_CA_CERT_FILENAME, RUSTFS_CLIENT_CERT_FILENAME, RUSTFS_CLIENT_KEY_FILENAME, RUSTFS_TLS_CERT,
|
||||
DEFAULT_TRUST_LEAF_CERT_AS_CA, DEFAULT_TRUST_SYSTEM_CA, ENV_MTLS_CLIENT_CERT, ENV_MTLS_CLIENT_KEY, ENV_RUSTFS_EXTRA_CA_CERT,
|
||||
ENV_SERVER_MTLS_ENABLE, ENV_TLS_KEYLOG, ENV_TLS_RELOAD_ENABLE, ENV_TLS_RELOAD_INTERVAL, ENV_TRUST_LEAF_CERT_AS_CA,
|
||||
ENV_TRUST_SYSTEM_CA, RUSTFS_CA_CERT, RUSTFS_CLIENT_CA_CERT_FILENAME, RUSTFS_CLIENT_CERT_FILENAME, RUSTFS_CLIENT_KEY_FILENAME,
|
||||
RUSTFS_TLS_CERT,
|
||||
};
|
||||
use rustfs_tls_runtime::{
|
||||
ServerTlsMaterial as RuntimeServerTlsMaterial, TlsGeneration, TlsSource, WebPkiClientVerifierOptions,
|
||||
@@ -34,6 +35,7 @@ use rustfs_tls_runtime::{
|
||||
};
|
||||
use rustfs_utils::{get_env_bool, get_env_opt_str};
|
||||
use rustls::pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject};
|
||||
use std::io::Cursor;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
@@ -267,6 +269,60 @@ fn map_runtime_tls_error(err: rustfs_tls_runtime::TlsRuntimeError) -> TlsMateria
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn validate_configured_oidc_extra_ca_cert() -> Result<(), TlsMaterialError> {
|
||||
if let Some(path) = configured_oidc_extra_ca_cert_path() {
|
||||
let _ = load_configured_oidc_extra_ca_cert().await?;
|
||||
info!(
|
||||
component = LOG_COMPONENT_TLS,
|
||||
subsystem = LOG_SUBSYSTEM_TLS,
|
||||
event = "oidc_extra_ca_validated",
|
||||
source = "oidc_extra_ca_bundle",
|
||||
env_var = ENV_RUSTFS_EXTRA_CA_CERT,
|
||||
path = ?path,
|
||||
"OIDC extra root CA bundle validated"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn load_configured_oidc_extra_ca_cert() -> Result<Option<Vec<u8>>, TlsMaterialError> {
|
||||
let Some(path) = configured_oidc_extra_ca_cert_path() else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let data = tokio::fs::read(&path)
|
||||
.await
|
||||
.map_err(|e| TlsMaterialError::Io(format!("read extra CA bundle {path:?}: {e}")))?;
|
||||
validate_cert_bundle(&data, &path)?;
|
||||
Ok(Some(data))
|
||||
}
|
||||
|
||||
fn configured_oidc_extra_ca_cert_path() -> Option<PathBuf> {
|
||||
let path = get_env_opt_str(ENV_RUSTFS_EXTRA_CA_CERT)?;
|
||||
let path = path.trim();
|
||||
if path.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(PathBuf::from(path))
|
||||
}
|
||||
|
||||
fn validate_cert_bundle(data: &[u8], path: &Path) -> Result<(), TlsMaterialError> {
|
||||
let mut reader = Cursor::new(data);
|
||||
let mut found = false;
|
||||
let mut store = rustls::RootCertStore::empty();
|
||||
for cert in CertificateDer::pem_reader_iter(&mut reader) {
|
||||
let cert = cert.map_err(|e| TlsMaterialError::Parse(format!("invalid extra CA bundle {path:?}: {e}")))?;
|
||||
store
|
||||
.add(cert)
|
||||
.map_err(|e| TlsMaterialError::Parse(format!("invalid extra CA bundle {path:?}: {e}")))?;
|
||||
found = true;
|
||||
}
|
||||
if !found {
|
||||
return Err(TlsMaterialError::Parse(format!("no certificate found in extra CA bundle {path:?}")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load a single certificate file and append PEM data.
|
||||
/// Returns true if the file was successfully loaded.
|
||||
async fn load_cert_file(path: &Path, pem_data: &mut Vec<u8>, desc: &str) -> bool {
|
||||
@@ -681,6 +737,86 @@ mod tests {
|
||||
fs::write(dir.join(rustfs_config::RUSTFS_TLS_KEY), signing_key.serialize_pem()).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
async fn oidc_extra_ca_cert_loads_configured_bundle() {
|
||||
let CertifiedKey { cert, .. } =
|
||||
rcgen::generate_simple_self_signed(vec!["extra-ca.example".to_string()]).expect("generate extra CA cert");
|
||||
let temp_file = tempfile::NamedTempFile::new().expect("create extra CA file");
|
||||
fs::write(temp_file.path(), cert.pem()).expect("write extra CA file");
|
||||
let path = temp_file.path().to_string_lossy().to_string();
|
||||
|
||||
temp_env::async_with_vars([(ENV_RUSTFS_EXTRA_CA_CERT, Some(path.as_str()))], async {
|
||||
let extra_ca = load_configured_oidc_extra_ca_cert()
|
||||
.await
|
||||
.expect("OIDC extra CA should load")
|
||||
.expect("configured OIDC extra CA should be present");
|
||||
|
||||
assert!(extra_ca.starts_with(cert.pem().as_bytes()));
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
async fn oidc_extra_ca_cert_rejects_invalid_pem() {
|
||||
let temp_file = tempfile::NamedTempFile::new().expect("create invalid extra CA file");
|
||||
fs::write(temp_file.path(), b"not a certificate").expect("write invalid extra CA file");
|
||||
let path = temp_file.path().to_string_lossy().to_string();
|
||||
|
||||
temp_env::async_with_vars([(ENV_RUSTFS_EXTRA_CA_CERT, Some(path.as_str()))], async {
|
||||
let err = load_configured_oidc_extra_ca_cert()
|
||||
.await
|
||||
.expect_err("invalid extra CA should fail");
|
||||
|
||||
assert!(err.to_string().contains("no certificate found in extra CA bundle"));
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
async fn oidc_extra_ca_cert_rejects_malformed_der_certificate() {
|
||||
let temp_file = tempfile::NamedTempFile::new().expect("create malformed extra CA file");
|
||||
fs::write(
|
||||
temp_file.path(),
|
||||
b"-----BEGIN CERTIFICATE-----\nbm90IGEgY2VydA==\n-----END CERTIFICATE-----\n",
|
||||
)
|
||||
.expect("write malformed extra CA file");
|
||||
let path = temp_file.path().to_string_lossy().to_string();
|
||||
|
||||
temp_env::async_with_vars([(ENV_RUSTFS_EXTRA_CA_CERT, Some(path.as_str()))], async {
|
||||
let err = load_configured_oidc_extra_ca_cert()
|
||||
.await
|
||||
.expect_err("malformed DER in PEM framing should fail");
|
||||
|
||||
assert!(err.to_string().contains("invalid extra CA bundle"));
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
async fn load_tls_material_does_not_append_oidc_extra_ca_cert() {
|
||||
let temp_dir = TempDir::new().expect("create TLS material dir");
|
||||
write_test_cert_pair(temp_dir.path(), "server.example");
|
||||
let CertifiedKey { cert, .. } =
|
||||
rcgen::generate_simple_self_signed(vec!["extra-ca.example".to_string()]).expect("generate extra CA cert");
|
||||
let temp_file = tempfile::NamedTempFile::new().expect("create extra CA file");
|
||||
fs::write(temp_file.path(), cert.pem()).expect("write extra CA file");
|
||||
let path = temp_file.path().to_string_lossy().to_string();
|
||||
|
||||
temp_env::async_with_vars([(ENV_RUSTFS_EXTRA_CA_CERT, Some(path.as_str()))], async {
|
||||
let snapshot = load_tls_material(temp_dir.path().to_str().expect("TLS material dir should be utf-8"))
|
||||
.await
|
||||
.expect("TLS material should load");
|
||||
|
||||
assert!(snapshot.outbound.root_ca_pem.is_empty());
|
||||
assert!(snapshot.server.is_some());
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_acceptor_accepts_root_single_cert_with_trailing_slash() {
|
||||
ensure_rustls_crypto_provider();
|
||||
|
||||
@@ -14,9 +14,12 @@
|
||||
|
||||
use rustfs_iam::{
|
||||
federation::{FederatedIdentityRegistry, FederatedIdentityService, oidc::StandardOidcAdapter},
|
||||
get_oidc, init_oidc_sys,
|
||||
get_oidc, init_oidc_sys_with_extra_root_ca_provider,
|
||||
oidc::{OidcExtraRootCaMaterial, OidcExtraRootCaProvider},
|
||||
};
|
||||
use std::{
|
||||
collections::hash_map::DefaultHasher,
|
||||
hash::{Hash, Hasher},
|
||||
io::{Error, Result},
|
||||
sync::Arc,
|
||||
};
|
||||
@@ -50,7 +53,7 @@ pub(crate) async fn init_auth_integrations() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
match init_oidc_sys().await {
|
||||
match init_oidc_sys_with_extra_root_ca_provider(oidc_extra_root_ca_provider()).await {
|
||||
Ok(()) => {
|
||||
if let Some(oidc) = get_oidc() {
|
||||
let adapter = Arc::new(StandardOidcAdapter::new(oidc));
|
||||
@@ -72,3 +75,40 @@ pub(crate) async fn init_auth_integrations() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn oidc_extra_root_ca_provider() -> OidcExtraRootCaProvider {
|
||||
OidcExtraRootCaProvider::new(current_oidc_extra_root_ca_material)
|
||||
}
|
||||
|
||||
pub(crate) async fn current_oidc_extra_root_ca_material() -> std::result::Result<OidcExtraRootCaMaterial, String> {
|
||||
let outbound_tls = crate::runtime_sources::current_outbound_tls_state().await;
|
||||
let outbound_generation = outbound_tls.as_ref().map(|state| state.generation.0).unwrap_or_default();
|
||||
let mut root_ca_pem = outbound_tls.as_ref().and_then(|state| state.root_ca_pem.clone());
|
||||
|
||||
if let Some(extra_ca_pem) = crate::server::tls_material::load_configured_oidc_extra_ca_cert()
|
||||
.await
|
||||
.map_err(|err| err.to_string())?
|
||||
{
|
||||
match root_ca_pem.as_mut() {
|
||||
Some(root_ca_pem) => {
|
||||
if !root_ca_pem.is_empty() && !root_ca_pem.ends_with(b"\n") {
|
||||
root_ca_pem.push(b'\n');
|
||||
}
|
||||
root_ca_pem.extend_from_slice(&extra_ca_pem);
|
||||
}
|
||||
None => root_ca_pem = Some(extra_ca_pem),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(OidcExtraRootCaMaterial {
|
||||
generation: oidc_extra_root_ca_generation(outbound_generation, root_ca_pem.as_deref()),
|
||||
root_ca_pem,
|
||||
})
|
||||
}
|
||||
|
||||
fn oidc_extra_root_ca_generation(outbound_generation: u64, root_ca_pem: Option<&[u8]>) -> u64 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
outbound_generation.hash(&mut hasher);
|
||||
root_ca_pem.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
@@ -24,21 +24,14 @@ const EVENT_TLS_OUTBOUND_INITIALIZATION_FAILED: &str = "tls_outbound_initializat
|
||||
const TLS_STARTUP_GENERATION_CONSUMER: &str = "rustfs_server_startup";
|
||||
|
||||
pub(crate) async fn init_outbound_tls_material(config: &Config) -> Result<()> {
|
||||
crate::server::tls_material::validate_configured_oidc_extra_ca_cert()
|
||||
.await
|
||||
.map_err(|err| Error::other(err.to_string()))?;
|
||||
|
||||
if let Some(tls_path) = normalized_tls_path(config.tls_path.as_deref()) {
|
||||
match crate::server::tls_material::load_tls_material(tls_path).await {
|
||||
Ok(snapshot) => {
|
||||
let generation = next_tls_generation(startup_runtime_sources::current_outbound_tls_generation());
|
||||
startup_runtime_sources::publish_outbound_tls_state(generation, &snapshot.outbound).await;
|
||||
startup_runtime_sources::record_tls_generation(TLS_STARTUP_GENERATION_CONSUMER, generation.0);
|
||||
info!(
|
||||
target: "rustfs::main",
|
||||
event = EVENT_TLS_OUTBOUND_INITIALIZED,
|
||||
component = LOG_COMPONENT_MAIN,
|
||||
subsystem = LOG_SUBSYSTEM_STARTUP,
|
||||
tls_path,
|
||||
generation = generation.0,
|
||||
"Initialized TLS outbound material"
|
||||
);
|
||||
publish_outbound_tls_material(&snapshot.outbound, Some(tls_path)).await;
|
||||
}
|
||||
Err(err) => {
|
||||
error!(
|
||||
@@ -61,6 +54,24 @@ pub(crate) async fn init_outbound_tls_material(config: &Config) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn publish_outbound_tls_material(outbound: &rustfs_tls_runtime::OutboundTlsMaterial, tls_path: Option<&str>) {
|
||||
let generation = next_tls_generation(startup_runtime_sources::current_outbound_tls_generation());
|
||||
startup_runtime_sources::publish_outbound_tls_state(generation, outbound).await;
|
||||
startup_runtime_sources::record_tls_generation(TLS_STARTUP_GENERATION_CONSUMER, generation.0);
|
||||
info!(
|
||||
target: "rustfs::main",
|
||||
event = EVENT_TLS_OUTBOUND_INITIALIZED,
|
||||
component = LOG_COMPONENT_MAIN,
|
||||
subsystem = LOG_SUBSYSTEM_STARTUP,
|
||||
state = "initialized",
|
||||
tls_path = tls_path.unwrap_or(""),
|
||||
generation = generation.0,
|
||||
has_root_ca = !outbound.root_ca_pem.is_empty(),
|
||||
has_mtls_identity = outbound.mtls_identity.is_some(),
|
||||
"Initialized TLS outbound material"
|
||||
);
|
||||
}
|
||||
|
||||
fn normalized_tls_path(path: Option<&str>) -> Option<&str> {
|
||||
path.map(str::trim).filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user