fix(startup): never panic when the system CA bundle is absent (#6769)

This commit is contained in:
Zhengchao An
2026-08-28 08:34:51 +08:00
committed by GitHub
parent 22741603f5
commit e388a3ff53
7 changed files with 132 additions and 33 deletions
+35 -1
View File
@@ -352,6 +352,28 @@ pub struct BucketTargetSys {
heartbeat_started: OnceLock<()>, heartbeat_started: OnceLock<()>,
} }
/// Build the bucket-target health-check HTTP client without panicking when
/// the host has no system CA bundle (issue #6734).
///
/// `BucketTargetSys::get()` initializes lazily on the startup path (bucket
/// metadata install calls it on the main thread), and `reqwest::Client::new()`
/// panics when the TLS backend cannot load any system trust root — the state
/// of a minimal container image. Fall back to a client with an explicit empty
/// trust store: HTTP health checks keep working, and HTTPS targets fail closed
/// at the TLS handshake with a clear certificate error instead of aborting
/// the whole process at startup.
fn build_health_check_client() -> HttpClient {
HttpClient::builder().build().unwrap_or_else(|error| {
warn!(
"bucket target health-check HTTP client could not load system TLS roots ({error}); continuing with an empty trust store — HTTPS target health checks will fail until a CA bundle is installed"
);
HttpClient::builder()
.tls_certs_only(std::iter::empty::<reqwest::Certificate>())
.build()
.expect("HTTP client construction must succeed with an explicit empty trust store")
})
}
impl BucketTargetSys { impl BucketTargetSys {
pub fn get() -> &'static Self { pub fn get() -> &'static Self {
GLOBAL_BUCKET_TARGET_SYS.get_or_init(Self::new) GLOBAL_BUCKET_TARGET_SYS.get_or_init(Self::new)
@@ -364,7 +386,7 @@ impl BucketTargetSys {
targets_map: Arc::new(RwLock::new(HashMap::new())), targets_map: Arc::new(RwLock::new(HashMap::new())),
h_mutex: Arc::new(RwLock::new(HashMap::new())), h_mutex: Arc::new(RwLock::new(HashMap::new())),
target_h_mutex: Arc::new(RwLock::new(HashMap::new())), target_h_mutex: Arc::new(RwLock::new(HashMap::new())),
hc_client: Arc::new(HttpClient::new()), hc_client: Arc::new(build_health_check_client()),
a_mutex: Arc::new(Mutex::new(HashMap::new())), a_mutex: Arc::new(Mutex::new(HashMap::new())),
arn_errs_map: Arc::new(RwLock::new(HashMap::new())), arn_errs_map: Arc::new(RwLock::new(HashMap::new())),
target_update_mutexes: Arc::new(Mutex::new(HashMap::new())), target_update_mutexes: Arc::new(Mutex::new(HashMap::new())),
@@ -2490,6 +2512,18 @@ mod tests {
use super::*; use super::*;
use rcgen::generate_simple_self_signed; use rcgen::generate_simple_self_signed;
// The startup panic fix for hosts without a CA bundle (issue #6734) rests
// on two properties: the health-check client constructor never panics, and
// its degraded fallback — an explicit empty trust store — always builds.
#[test]
fn health_check_client_construction_never_panics() {
let _ = build_health_check_client();
HttpClient::builder()
.tls_certs_only(std::iter::empty::<reqwest::Certificate>())
.build()
.expect("empty-trust-store client build must succeed without touching system roots");
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct RecordingHttpConnector { struct RecordingHttpConnector {
request_uris: Arc<std::sync::Mutex<Vec<String>>>, request_uris: Arc<std::sync::Mutex<Vec<String>>>,
+22 -14
View File
@@ -173,20 +173,28 @@ pub async fn lookup_config() -> Result<Args, OpaConfigError> {
impl AuthZPlugin { impl AuthZPlugin {
pub fn new(config: Args) -> Self { pub fn new(config: Args) -> Self {
let client = reqwest::Client::builder() let builder = || {
.timeout(Duration::from_secs(5)) reqwest::Client::builder()
.connect_timeout(Duration::from_secs(1)) .timeout(Duration::from_secs(5))
.pool_max_idle_per_host(10) .connect_timeout(Duration::from_secs(1))
.pool_idle_timeout(Some(Duration::from_secs(60))) .pool_max_idle_per_host(10)
.tcp_keepalive(Some(Duration::from_secs(30))) .pool_idle_timeout(Some(Duration::from_secs(60)))
.tcp_nodelay(true) .tcp_keepalive(Some(Duration::from_secs(30)))
.http2_keep_alive_interval(Some(Duration::from_secs(30))) .tcp_nodelay(true)
.http2_keep_alive_timeout(Duration::from_secs(15)) .http2_keep_alive_interval(Some(Duration::from_secs(30)))
.build() .http2_keep_alive_timeout(Duration::from_secs(15))
.unwrap_or_else(|err| { };
error!("failed to build OPA HTTP client, falling back to default reqwest client: {err}"); // Never fall back to `reqwest::Client::new()`: it panics for the same
reqwest::Client::new() // reason the first build failed (e.g. no system CA bundle, issue
}); // #6734). Retry with an explicit empty trust store instead — an HTTP
// OPA endpoint keeps working, an HTTPS one fails closed per request.
let client = builder().build().unwrap_or_else(|err| {
error!("failed to build OPA HTTP client ({err}); continuing with an empty trust store");
builder()
.tls_certs_only(std::iter::empty::<reqwest::Certificate>())
.build()
.expect("HTTP client construction must succeed with an explicit empty trust store")
});
Self { client, args: config } Self { client, args: config }
} }
@@ -43,7 +43,7 @@ impl AwsMetadataFetcher {
/// ///
/// Returns a new instance of `AwsMetadataFetcher`. /// Returns a new instance of `AwsMetadataFetcher`.
pub fn new(timeout: Duration) -> Self { pub fn new(timeout: Duration) -> Self {
let client = Client::builder().timeout(timeout).build().unwrap_or_else(|_| Client::new()); let client = super::metadata_http_client(timeout);
Self { Self {
client, client,
@@ -34,7 +34,7 @@ pub struct AzureMetadataFetcher {
impl AzureMetadataFetcher { impl AzureMetadataFetcher {
/// Creates a new `AzureMetadataFetcher`. /// Creates a new `AzureMetadataFetcher`.
pub fn new(timeout: Duration) -> Self { pub fn new(timeout: Duration) -> Self {
let client = Client::builder().timeout(timeout).build().unwrap_or_else(|_| Client::new()); let client = super::metadata_http_client(timeout);
Self { Self {
client, client,
@@ -34,7 +34,7 @@ pub struct GcpMetadataFetcher {
impl GcpMetadataFetcher { impl GcpMetadataFetcher {
/// Creates a new `GcpMetadataFetcher`. /// Creates a new `GcpMetadataFetcher`.
pub fn new(timeout: Duration) -> Self { pub fn new(timeout: Duration) -> Self {
let client = Client::builder().timeout(timeout).build().unwrap_or_else(|_| Client::new()); let client = super::metadata_http_client(timeout);
Self { Self {
client, client,
@@ -24,3 +24,40 @@ mod gcp;
pub use aws::*; pub use aws::*;
pub use azure::*; pub use azure::*;
pub use gcp::*; pub use gcp::*;
/// Build the metadata HTTP client without panicking when the host has no
/// system CA bundle (issue #6734).
///
/// `reqwest::Client::new()` panics when the TLS backend cannot load any
/// system trust root, which is exactly the state of a minimal container
/// image. Cloud metadata endpoints are plain HTTP link-local addresses, so a
/// client with an explicit empty trust store is fully functional here; TLS
/// requests through it fail closed at the handshake.
pub(crate) fn metadata_http_client(timeout: std::time::Duration) -> reqwest::Client {
reqwest::Client::builder().timeout(timeout).build().unwrap_or_else(|error| {
tracing::warn!(
"cloud metadata HTTP client could not load system TLS roots ({error}); continuing with an empty trust store"
);
reqwest::Client::builder()
.timeout(timeout)
.tls_certs_only(std::iter::empty::<reqwest::Certificate>())
.build()
.expect("HTTP client construction must succeed with an explicit empty trust store")
})
}
#[cfg(test)]
mod tests {
// The startup panic fix for hosts without a CA bundle (issue #6734) rests
// on the constructor never panicking and its degraded fallback — an
// explicit empty trust store — always building.
#[test]
fn metadata_http_client_construction_never_panics() {
let _ = super::metadata_http_client(std::time::Duration::from_secs(1));
reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(1))
.tls_certs_only(std::iter::empty::<reqwest::Certificate>())
.build()
.expect("empty-trust-store client build must succeed without touching system roots");
}
}
+35 -15
View File
@@ -16,7 +16,7 @@ use crate::version;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::time::Duration; use std::time::Duration;
use thiserror::Error; use thiserror::Error;
use tracing::{debug, error, info}; use tracing::{debug, error, info, warn};
/// Update check related errors /// Update check related errors
#[derive(Error, Debug)] #[derive(Error, Debug)]
@@ -73,17 +73,31 @@ impl Default for VersionChecker {
} }
} }
/// Build the update-check HTTP client without panicking when the host has no
/// system CA bundle (issue #6734): `reqwest::Client::new()` panics for the
/// same reason a builder `build()` fails, so falling back to it turned a
/// degraded environment into a process abort. With an explicit empty trust
/// store the HTTPS version check fails closed per request instead.
fn version_check_client(timeout: Duration) -> reqwest::Client {
let builder = || {
reqwest::Client::builder()
.timeout(timeout)
.user_agent(format!("RustFS/{}", get_current_version()))
};
builder().build().unwrap_or_else(|error| {
warn!("update-check HTTP client could not load system TLS roots ({error}); continuing with an empty trust store");
builder()
.tls_certs_only(std::iter::empty::<reqwest::Certificate>())
.build()
.expect("HTTP client construction must succeed with an explicit empty trust store")
})
}
impl VersionChecker { impl VersionChecker {
/// Create a new version checker /// Create a new version checker
pub fn new() -> Self { pub fn new() -> Self {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(10))
.user_agent(format!("RustFS/{}", get_current_version()))
.build()
.unwrap_or_else(|_| reqwest::Client::new());
Self { Self {
client, client: version_check_client(Duration::from_secs(10)),
version_url: "https://version.rustfs.com/latest.json".to_string(), version_url: "https://version.rustfs.com/latest.json".to_string(),
timeout: Duration::from_secs(10), timeout: Duration::from_secs(10),
} }
@@ -91,14 +105,8 @@ impl VersionChecker {
/// Create version checker with custom configuration /// Create version checker with custom configuration
pub fn with_config(url: String, timeout: Duration) -> Self { pub fn with_config(url: String, timeout: Duration) -> Self {
let client = reqwest::Client::builder()
.timeout(timeout)
.user_agent(format!("RustFS/{}", get_current_version()))
.build()
.unwrap_or_else(|_| reqwest::Client::new());
Self { Self {
client, client: version_check_client(timeout),
version_url: url, version_url: url,
timeout, timeout,
} }
@@ -182,6 +190,18 @@ pub async fn check_updates_with_url(url: String) -> Result<UpdateCheckResult, Up
mod tests { mod tests {
use super::*; use super::*;
// The startup panic fix for hosts without a CA bundle (issue #6734) rests
// on the constructor never panicking and its degraded fallback — an
// explicit empty trust store — always building.
#[test]
fn version_check_client_construction_never_panics() {
let _ = version_check_client(Duration::from_secs(1));
reqwest::Client::builder()
.tls_certs_only(std::iter::empty::<reqwest::Certificate>())
.build()
.expect("empty-trust-store client build must succeed without touching system roots");
}
#[tokio::test] #[tokio::test]
async fn test_get_current_version() { async fn test_get_current_version() {
let version = get_current_version(); let version = get_current_version();