fix(iam): merge OIDC extra root CAs (#5915)

Co-authored-by: heihutu <heihutu@gmail.com>
This commit is contained in:
houseme
2026-08-10 15:24:52 +08:00
committed by GitHub
parent d900e11a09
commit d97e059c3c
9 changed files with 665 additions and 68 deletions
+7 -1
View File
@@ -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,
+139 -3
View File
@@ -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();
+42 -2
View File
@@ -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()
}
+23 -12
View File
@@ -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())
}