Skip to content

Commit 53a7adb

Browse files
r1violletclaude
andcommitted
fix(profiling): skip system cert loading for non-HTTPS endpoints
Since 9a61cae (perf(profiling): cache TLS in ProfileExporter::new, 2026-02-27), TLS configuration is initialized unconditionally in ProfileExporter::new(), even when the endpoint uses plain HTTP (e.g. agent mode at http://<host>:8126), unix sockets, or named pipes. There are two layers to the problem: 1. Our cached_tls_config() calls rustls-platform-verifier which eagerly loads the system CA store on Linux. 2. Even without our call, reqwest::ClientBuilder::build() also attempts to initialize its own TLS backend (loading system certs) unless a preconfigured TLS config is provided. Both fail with the same error in minimal container images (e.g. bare ubuntu:20.04 without the ca-certificates package): failed to initialize TLS configuration: unexpected error: No CA certificates were loaded from the system This surfaced in ddprof after the libdatadog v29 upgrade: the profiler targets the Datadog agent over HTTP and has no reason to touch the cert store at all. Fix: - For HTTPS endpoints, use the existing cached platform TLS config (loads system CA certs via rustls-platform-verifier). - For all other schemes (http, unix, windows, file), provide reqwest with a minimal TLS config that has an empty root store. This prevents reqwest from loading system certs itself, while still giving it a valid TLS backend. TLS is never negotiated on these transports anyway. Verified with a Docker repro: ubuntu:22.04 after `dpkg --force-depends --purge ca-certificates`, all 5 exporter_e2e tests pass with the fix and fail without it. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent db9b9f4 commit 53a7adb

2 files changed

Lines changed: 36 additions & 1 deletion

File tree

libdd-profiling/src/exporter/profile_exporter.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,16 @@ impl ProfileExporter {
7878
mut tags: Vec<Tag>,
7979
endpoint: Endpoint,
8080
) -> anyhow::Result<Self> {
81-
let tls_config = super::tls::cached_tls_config()?;
81+
// For HTTPS, use the cached platform TLS config (loads system CA certs).
82+
// For all other schemes (http, unix, windows, file), provide a minimal
83+
// config with an empty root store so reqwest doesn't attempt to load
84+
// system CA certificates itself — which fails in minimal container
85+
// environments. TLS will never be negotiated on those transports anyway.
86+
let tls_config = if endpoint.url.scheme_str() == Some("https") {
87+
super::tls::cached_tls_config()?
88+
} else {
89+
super::tls::empty_tls_config()?
90+
};
8291
// Pre-build all static headers
8392
let mut headers = reqwest::header::HeaderMap::new();
8493

libdd-profiling/src/exporter/tls.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,26 @@ impl TlsConfig {
6969
}
7070
}
7171

72+
impl TlsConfig {
73+
/// Create a minimal TLS configuration with an empty root store.
74+
///
75+
/// Used for non-HTTPS endpoints (HTTP, unix sockets, named pipes) where TLS
76+
/// will never actually be negotiated. Providing this to reqwest prevents it
77+
/// from attempting to load system CA certificates on its own, which fails in
78+
/// minimal container environments that have no CA certificates installed.
79+
pub fn new_empty() -> Result<Self, rustls::Error> {
80+
let provider = rustls::crypto::CryptoProvider::get_default()
81+
.cloned()
82+
.unwrap_or_else(|| std::sync::Arc::new(Self::default_crypto_provider()));
83+
84+
let config = rustls::ClientConfig::builder_with_provider(provider)
85+
.with_safe_default_protocol_versions()?
86+
.with_root_certificates(rustls::RootCertStore::empty())
87+
.with_no_client_auth();
88+
Ok(Self(config))
89+
}
90+
}
91+
7292
static TLS_CONFIG: std::sync::LazyLock<Result<TlsConfig, String>> =
7393
std::sync::LazyLock::new(|| {
7494
TlsConfig::new().map_err(|err| format!("failed to initialize TLS configuration: {err}"))
@@ -80,3 +100,9 @@ pub(crate) fn cached_tls_config() -> anyhow::Result<TlsConfig> {
80100
.map(Clone::clone)
81101
.map_err(|err| anyhow::anyhow!("{err}"))
82102
}
103+
104+
/// Returns a TLS config with an empty root store, for use with non-HTTPS endpoints.
105+
/// Never fails — no system cert loading is attempted.
106+
pub(crate) fn empty_tls_config() -> anyhow::Result<TlsConfig> {
107+
TlsConfig::new_empty().map_err(|err| anyhow::anyhow!("failed to build TLS config: {err}"))
108+
}

0 commit comments

Comments
 (0)