From 50bf34f5e2e971f5c1a2db6dced85a494a1786dc Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Sat, 27 Jun 2026 20:29:00 +0300 Subject: [PATCH 1/2] build(deps): bump reqwest to 0.13, refresh dependencies - reqwest 0.12 -> 0.13: the `rustls-tls` feature was removed in 0.13, which broke dependency resolution in CI - 0.13's `rustls` feature hardwires the aws-lc-rs provider (C FFI, awkward for the musl static build), so use `rustls-no-provider` and install ring as the process default provider in the JWKS client, keeping the pure-Rust default and dropping aws-lc-rs from the tree - roots now resolve via rustls-platform-verifier (system store with bundled Mozilla roots as fallback) - refresh anyhow, envoy-types, getrandom and transitive deps Supersedes #62. --- Cargo.toml | 8 +++++++- src/auth/jwks.rs | 5 +++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index bfe9713..b94e85c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,7 +69,13 @@ redis = { version = "1.2", features = ["tokio-comp"], optional = true } # features (see [features] below); `use_pem` is always required for PEM key # parsing and is therefore enabled unconditionally. jsonwebtoken = { version = "10", default-features = false, features = ["use_pem"] } -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] } +# reqwest 0.13's `rustls` feature hardwires the aws-lc-rs provider (C FFI, awkward +# for the musl static build). We keep the pure-Rust `ring` provider instead: +# `rustls-no-provider` builds rustls without a bundled provider, and we install +# ring as the process default (see auth/jwks.rs). Roots come from the system trust +# store via rustls-platform-verifier (pulled in by `rustls-no-provider`). +reqwest = { version = "0.13", default-features = false, features = ["rustls-no-provider", "json"] } +rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] } globset = "0.4" base64 = "0.22" sha2 = "0.11" diff --git a/src/auth/jwks.rs b/src/auth/jwks.rs index ec2cf7a..2b6de4d 100644 --- a/src/auth/jwks.rs +++ b/src/auth/jwks.rs @@ -36,6 +36,11 @@ const JWKS_HTTP_TIMEOUT: Duration = Duration::from_secs(5); impl JwksCache { /// Create a cache for `uri` (keys are loaded lazily on first lookup). pub fn new(uri: String) -> Self { + // reqwest is built with `rustls-no-provider`, so it resolves the rustls + // CryptoProvider from the process default. Install ring (idempotent: a + // returned Err just means it was already set) before constructing the + // client, otherwise the TLS handshake has no provider to use. + let _ = rustls::crypto::ring::default_provider().install_default(); let client = reqwest::Client::builder() .timeout(JWKS_HTTP_TIMEOUT) .build() From 81a02b90795652811b3a1780a6d6c750865643ff Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Sat, 27 Jun 2026 21:44:22 +0300 Subject: [PATCH 2/2] fix(auth): pin self-contained rustls TLS for the JWKS client reqwest 0.13's rustls-no-provider leaves the client on the platform verifier with no bundled roots, which breaks TLS to the JWKS endpoint on musl / scratch / distroless images that ship no system CA bundle. It also required installing a process-global ring provider as a side effect of the JwksCache constructor, an ordering hazard for library/test callers. Hand reqwest a fully preconfigured rustls ClientConfig instead: the ring provider is set per-config (no global install_default) and Mozilla's root store is bundled via webpki-roots, restoring reqwest 0.12's self-contained behaviour. Verified the exact config path handshakes against a live JWKS endpoint. --- Cargo.toml | 9 ++++++--- src/auth/jwks.rs | 24 +++++++++++++++++++----- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b94e85c..29d39c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,11 +71,14 @@ redis = { version = "1.2", features = ["tokio-comp"], optional = true } jsonwebtoken = { version = "10", default-features = false, features = ["use_pem"] } # reqwest 0.13's `rustls` feature hardwires the aws-lc-rs provider (C FFI, awkward # for the musl static build). We keep the pure-Rust `ring` provider instead: -# `rustls-no-provider` builds rustls without a bundled provider, and we install -# ring as the process default (see auth/jwks.rs). Roots come from the system trust -# store via rustls-platform-verifier (pulled in by `rustls-no-provider`). +# `rustls-no-provider` builds rustls without a bundled provider, and auth/jwks.rs +# hands reqwest a fully preconfigured rustls ClientConfig (ring provider + bundled +# Mozilla roots from webpki-roots). Bundling the roots keeps the binary self- +# contained, so it works on musl / scratch / distroless images with no system CA +# bundle (matching reqwest 0.12's old `rustls-tls` behaviour). reqwest = { version = "0.13", default-features = false, features = ["rustls-no-provider", "json"] } rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] } +webpki-roots = "1" globset = "0.4" base64 = "0.22" sha2 = "0.11" diff --git a/src/auth/jwks.rs b/src/auth/jwks.rs index 2b6de4d..342a129 100644 --- a/src/auth/jwks.rs +++ b/src/auth/jwks.rs @@ -33,16 +33,30 @@ const MIN_REFRESH_INTERVAL: Duration = Duration::from_secs(60); /// Bound the worst-case latency of a slow/stalled JWKS endpoint. const JWKS_HTTP_TIMEOUT: Duration = Duration::from_secs(5); +/// Build the rustls client config for the JWKS HTTPS client. +/// +/// Uses the pure-Rust `ring` provider (installed per-config, not process-global) +/// and bundles Mozilla's root store via `webpki-roots`, so the binary needs no +/// system CA bundle and works in musl / scratch / distroless images. +fn build_tls_config() -> rustls::ClientConfig { + let mut roots = rustls::RootCertStore::empty(); + roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); + rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider())) + .with_safe_default_protocol_versions() + .expect("ring provider supports the default TLS protocol versions") + .with_root_certificates(roots) + .with_no_client_auth() +} + impl JwksCache { /// Create a cache for `uri` (keys are loaded lazily on first lookup). pub fn new(uri: String) -> Self { - // reqwest is built with `rustls-no-provider`, so it resolves the rustls - // CryptoProvider from the process default. Install ring (idempotent: a - // returned Err just means it was already set) before constructing the - // client, otherwise the TLS handshake has no provider to use. - let _ = rustls::crypto::ring::default_provider().install_default(); let client = reqwest::Client::builder() .timeout(JWKS_HTTP_TIMEOUT) + // Hand reqwest a fully preconfigured rustls backend rather than + // relying on a process-global default provider: no install ordering + // constraint, no global side effect, safe for library/test callers. + .tls_backend_preconfigured(build_tls_config()) .build() .unwrap_or_default(); Self {