From 60d7644ef188413336522a9bd7f9e241bc068f84 Mon Sep 17 00:00:00 2001 From: Adam Miller Date: Tue, 30 Jun 2026 17:03:25 -0500 Subject: [PATCH] feat(build): add system CA root mode Allow distro builds to use native trust stores for supervisor upstream TLS while keeping bundled Mozilla roots as the default. Avoid bundled root crates in system-ca-roots builds by using native-root TLS features and z3 0.20. Signed-off-by: Adam Miller --- .github/workflows/branch-checks.yml | 3 + Cargo.lock | 211 ++---------------- Cargo.toml | 9 +- architecture/build.md | 15 ++ crates/openshell-cli/Cargo.toml | 4 +- crates/openshell-core/Cargo.toml | 2 +- crates/openshell-sandbox/Cargo.toml | 6 +- crates/openshell-server/Cargo.toml | 2 +- .../openshell-supervisor-network/Cargo.toml | 8 +- .../src/l7/tls.rs | 100 ++++++--- .../openshell-supervisor-network/src/run.rs | 2 +- tasks/rust.toml | 11 + 12 files changed, 140 insertions(+), 233 deletions(-) diff --git a/.github/workflows/branch-checks.yml b/.github/workflows/branch-checks.yml index 7713febb6..7c5ca78a6 100644 --- a/.github/workflows/branch-checks.yml +++ b/.github/workflows/branch-checks.yml @@ -125,6 +125,9 @@ jobs: - name: Verify telemetry can be compiled out run: mise run rust:verify:telemetry-off + - name: Verify system-ca-roots feature compiles and excludes bundled Mozilla roots + run: mise run rust:verify:system-ca-roots + - name: sccache stats if: always() run: | diff --git a/Cargo.lock b/Cargo.lock index c86773bb7..176e29646 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -549,24 +549,6 @@ dependencies = [ "sha2 0.10.9", ] -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags 2.11.1", - "cexpr", - "clang-sys", - "itertools 0.13.0", - "proc-macro2", - "quote", - "regex", - "rustc-hash 2.1.2", - "shlex", - "syn 2.0.117", -] - [[package]] name = "bitflags" version = "1.3.2" @@ -710,15 +692,6 @@ dependencies = [ "either", ] -[[package]] -name = "bzip2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" -dependencies = [ - "libbz2-rs-sys", -] - [[package]] name = "capctl" version = "0.2.4" @@ -772,15 +745,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - [[package]] name = "cfg-if" version = "1.0.4" @@ -828,17 +792,6 @@ dependencies = [ "inout", ] -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - [[package]] name = "clap" version = "4.6.1" @@ -990,12 +943,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "constant_time_eq" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" - [[package]] name = "core-foundation" version = "0.10.1" @@ -1304,12 +1251,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" -[[package]] -name = "deflate64" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" - [[package]] name = "delegate" version = "0.13.5" @@ -1671,7 +1612,6 @@ checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", - "zlib-rs", ] [[package]] @@ -1882,12 +1822,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi 6.0.0", "wasip2", "wasip3", - "wasm-bindgen", ] [[package]] @@ -2236,7 +2174,6 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.7", ] [[package]] @@ -2914,12 +2851,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" -[[package]] -name = "libbz2-rs-sys" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3a6a8c165077efc8f3a971534c50ea6a1a18b329ef4a66e897a7e3a1494565f" - [[package]] name = "libc" version = "0.2.185" @@ -3089,15 +3020,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" -[[package]] -name = "lzma-rust2" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47bb1e988e6fb779cf720ad431242d3f03167c1b3f2b1aae7f1a94b2495b36ae" -dependencies = [ - "sha2 0.10.9", -] - [[package]] name = "matchers" version = "0.2.0" @@ -3330,20 +3252,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "num" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" -dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", -] - [[package]] name = "num-bigint" version = "0.4.6" @@ -3372,15 +3280,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "num-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "num-traits", -] - [[package]] name = "num-conv" version = "0.2.1" @@ -3407,17 +3306,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -3950,6 +3838,7 @@ dependencies = [ "regorus", "reqwest 0.12.28", "rustls", + "rustls-native-certs", "rustls-pemfile", "serde", "serde_json", @@ -3966,7 +3855,7 @@ dependencies = [ "tower-mcp-types", "tracing", "uuid", - "webpki-roots 1.0.7", + "webpki-roots", ] [[package]] @@ -4452,12 +4341,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" -[[package]] -name = "ppmd-rust" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24" - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -4911,7 +4794,6 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.7", ] [[package]] @@ -5818,6 +5700,7 @@ dependencies = [ "once_cell", "percent-encoding", "rustls", + "rustls-native-certs", "serde", "serde_json", "sha2 0.10.9", @@ -5827,7 +5710,6 @@ dependencies = [ "tokio-stream", "tracing", "url", - "webpki-roots 0.26.11", ] [[package]] @@ -5860,7 +5742,6 @@ dependencies = [ "serde_json", "sha2 0.10.9", "sqlx-core", - "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", "syn 2.0.117", @@ -5899,7 +5780,6 @@ dependencies = [ "percent-encoding", "rand 0.8.6", "rsa 0.9.10", - "serde", "sha1 0.10.6", "sha2 0.10.9", "smallvec", @@ -6296,7 +6176,6 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", - "js-sys", "num-conv", "powerfmt", "serde_core", @@ -6796,12 +6675,6 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "typed-path" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" - [[package]] name = "typenum" version = "1.19.0" @@ -7162,15 +7035,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.7", -] - [[package]] name = "webpki-roots" version = "1.0.7" @@ -7827,27 +7691,31 @@ dependencies = [ [[package]] name = "z3" -version = "0.19.15" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "107cca65ed27d28b11f7c492298a51383333fd48ba6ebe49a432aba96162f678" +checksum = "80c4de445f5c9e3013703a6b8a40c80b4a64c925f0a19e7d0a23a7a9b70e854d" dependencies = [ "log", - "num", "z3-sys", ] [[package]] -name = "z3-sys" -version = "0.10.9" +name = "z3-src" +version = "416.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c82b97329d02d87da6802ed9fda083f1b255d822ab13d5b1fb961196b58a69a1" +checksum = "f2af0c6527de39877cf55cb87f233016573eeeb7cf77afdc1469e4b32faef832" dependencies = [ - "bindgen", "cmake", +] + +[[package]] +name = "z3-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c18b0a91a13522d21b3414847667de2b2056a721a3edcb5b6ee6858352d58db4" +dependencies = [ "pkg-config", - "reqwest 0.12.28", - "serde_json", - "zip", + "z3-src", ] [[package]] @@ -7944,57 +7812,12 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "zip" -version = "8.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcab981e19633ebcf0b001ddd37dd802996098bc1864f90b7c5d970ce76c1d59" -dependencies = [ - "aes", - "bzip2", - "constant_time_eq", - "crc32fast", - "deflate64", - "flate2", - "getrandom 0.4.2", - "hmac", - "indexmap", - "lzma-rust2", - "memchr", - "pbkdf2", - "ppmd-rust", - "sha1 0.10.6", - "time", - "typed-path", - "zeroize", - "zopfli", - "zstd", -] - -[[package]] -name = "zlib-rs" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" - [[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" -[[package]] -name = "zopfli" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" -dependencies = [ - "bumpalo", - "crc32fast", - "log", - "simd-adler32", -] - [[package]] name = "zstd" version = "0.13.3" diff --git a/Cargo.toml b/Cargo.toml index f450cd5c8..b5e38967e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ rustls = { version = "0.23", default-features = false, features = ["std", "loggi rustls-pemfile = "2" rcgen = { version = "0.13", features = ["crypto", "pem"] } webpki-roots = "1" +rustls-native-certs = "0.8" # CLI clap = { version = "4.5", features = ["derive", "env"] } @@ -79,7 +80,7 @@ tower-mcp-types = "0.12.0" reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls-native-roots"] } # WebSocket -tokio-tungstenite = { version = "0.26", features = ["rustls-tls-native-roots"] } +tokio-tungstenite = { version = "0.26", default-features = false, features = ["connect", "rustls-tls-native-roots"] } # Clipboard (OSC 52) base64 = "0.22" @@ -106,10 +107,10 @@ protobuf-src = "1.1.0" url = "2" # Database -sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "sqlite", "migrate"] } +sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio", "tls-rustls-ring-native-roots", "postgres", "sqlite", "migrate", "macros"] } # Kubernetes -kube = { version = "0.90", features = ["runtime", "derive"] } +kube = { version = "0.90", default-features = false, features = ["client", "runtime", "derive", "rustls-tls"] } kube-runtime = "0.90" k8s-openapi = { version = "0.21.1", features = ["v1_26"] } @@ -117,7 +118,7 @@ k8s-openapi = { version = "0.21.1", features = ["v1_26"] } uuid = { version = "1.10", features = ["v4"] } # SMT solver (uses system libz3; enable z3/bundled via the prover's bundled-z3 feature for local dev without system z3) -z3 = "0.19" +z3 = "0.20" [workspace.lints.rust] unsafe_code = "warn" diff --git a/architecture/build.md b/architecture/build.md index a3cb2e25f..258fa2537 100644 --- a/architecture/build.md +++ b/architecture/build.md @@ -39,6 +39,21 @@ are no-ops, so the data-model types stay available and dependent crates compile unchanged. The runtime `OPENSHELL_TELEMETRY_ENABLED` switch remains the way to disable telemetry in a default (telemetry-enabled) build. +Supervisor upstream TLS root-store selection is controlled by mutually +exclusive Cargo features. Default builds enable `bundled-ca-roots`, which +preserves upstream behavior by using Mozilla roots through `webpki-roots` plus +locally-installed CAs from the system bundle. Linux distribution builds should +use `--no-default-features --features system-ca-roots` so supervisor upstream +TLS uses the native trust store and the workspace dependency graph excludes +bundled Mozilla root crates such as `webpki-roots` and `webpki-root-certs`. +Other Rustls clients use native roots directly because that already satisfies +Linux distribution trust-store policy. + +The workspace uses `z3` versions whose `z3-sys` dependency keeps downloader +HTTP/TLS support behind explicit build features, so default system-Z3 builds do +not reintroduce bundled Mozilla roots. Release builds that need bundled Z3 +continue to opt in with `bundled-z3`. + ## Linux Runtime Environments OpenShell uses different Linux libc environments for different host artifacts. diff --git a/crates/openshell-cli/Cargo.toml b/crates/openshell-cli/Cargo.toml index 8e4cb3fb2..bcfb7887d 100644 --- a/crates/openshell-cli/Cargo.toml +++ b/crates/openshell-cli/Cargo.toml @@ -46,7 +46,7 @@ bytes = { workspace = true } http-body-util = { workspace = true } hyper = { workspace = true } hyper-util = { workspace = true } -hyper-rustls = { version = "0.27", default-features = false, features = ["native-tokio", "http1", "http2", "tls12", "logging", "ring", "webpki-tokio"] } +hyper-rustls = { version = "0.27", default-features = false, features = ["native-tokio", "http1", "http2", "tls12", "logging", "ring"] } rustls = { workspace = true } rustls-pemfile = { workspace = true } tokio-rustls = { workspace = true } @@ -63,7 +63,7 @@ tar = "0.4" tempfile = "3" # OIDC/Auth -oauth2 = "5" +oauth2 = { version = "5", default-features = false, features = ["reqwest"] } base64 = { workspace = true } # WebSocket (Cloudflare tunnel proxy) diff --git a/crates/openshell-core/Cargo.toml b/crates/openshell-core/Cargo.toml index 0ff6d06d6..5b1b97e4a 100644 --- a/crates/openshell-core/Cargo.toml +++ b/crates/openshell-core/Cargo.toml @@ -25,7 +25,7 @@ url = { workspace = true } ipnet = "2" base64 = { workspace = true } chrono = { version = "0.4", default-features = false, features = ["clock", "std"], optional = true } -reqwest = { workspace = true, features = ["blocking", "rustls-tls-webpki-roots"], optional = true } +reqwest = { workspace = true, features = ["blocking", "rustls-tls-native-roots"], optional = true } [features] default = ["telemetry"] diff --git a/crates/openshell-sandbox/Cargo.toml b/crates/openshell-sandbox/Cargo.toml index 086dbe02c..037f5c739 100644 --- a/crates/openshell-sandbox/Cargo.toml +++ b/crates/openshell-sandbox/Cargo.toml @@ -18,7 +18,7 @@ path = "src/main.rs" openshell-core = { path = "../openshell-core", default-features = false } openshell-ocsf = { path = "../openshell-ocsf" } openshell-policy = { path = "../openshell-policy" } -openshell-supervisor-network = { path = "../openshell-supervisor-network" } +openshell-supervisor-network = { path = "../openshell-supervisor-network", default-features = false } openshell-supervisor-process = { path = "../openshell-supervisor-process" } # Async runtime @@ -45,11 +45,13 @@ tracing-subscriber = { workspace = true } tracing-appender = { workspace = true } [features] -default = ["telemetry"] +default = ["telemetry", "bundled-ca-roots"] ## Compile in telemetry activity collection (forwards to openshell-core/telemetry). ## On by default; build with `--no-default-features` for a telemetry-free sandbox ## supervisor that never collects or forwards activity summaries. telemetry = ["openshell-core/telemetry"] +bundled-ca-roots = ["openshell-supervisor-network/bundled-ca-roots"] +system-ca-roots = ["openshell-supervisor-network/system-ca-roots"] [dev-dependencies] openshell-core = { path = "../openshell-core", features = ["test-helpers"] } diff --git a/crates/openshell-server/Cargo.toml b/crates/openshell-server/Cargo.toml index b5c9b34d7..f04a9e5e8 100644 --- a/crates/openshell-server/Cargo.toml +++ b/crates/openshell-server/Cargo.toml @@ -108,7 +108,7 @@ bundled-z3 = ["openshell-prover/bundled-z3"] test-support = [] [dev-dependencies] -hyper-rustls = { version = "0.27", default-features = false, features = ["native-tokio", "http1", "tls12", "logging", "ring", "webpki-tokio"] } +hyper-rustls = { version = "0.27", default-features = false, features = ["native-tokio", "http1", "tls12", "logging", "ring"] } rcgen = { version = "0.13", features = ["crypto", "pem"] } tokio-tungstenite = { workspace = true } futures-util = "0.3" diff --git a/crates/openshell-supervisor-network/Cargo.toml b/crates/openshell-supervisor-network/Cargo.toml index 7d0079f7b..de77344df 100644 --- a/crates/openshell-supervisor-network/Cargo.toml +++ b/crates/openshell-supervisor-network/Cargo.toml @@ -32,6 +32,7 @@ rcgen = { workspace = true } regorus = { version = "0.9", default-features = false, features = ["std", "arc", "glob"] } reqwest = { workspace = true } rustls = { workspace = true } +rustls-native-certs = { workspace = true, optional = true } rustls-pemfile = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } @@ -45,7 +46,12 @@ tokio-rustls = { workspace = true } tower-mcp-types = { workspace = true } tracing = { workspace = true } uuid = { workspace = true } -webpki-roots = { workspace = true } +webpki-roots = { workspace = true, optional = true } + +[features] +default = ["bundled-ca-roots"] +bundled-ca-roots = ["dep:webpki-roots"] +system-ca-roots = ["dep:rustls-native-certs"] [dev-dependencies] openshell-core = { path = "../openshell-core", features = ["test-helpers"] } diff --git a/crates/openshell-supervisor-network/src/l7/tls.rs b/crates/openshell-supervisor-network/src/l7/tls.rs index 70e198f42..942e796e1 100644 --- a/crates/openshell-supervisor-network/src/l7/tls.rs +++ b/crates/openshell-supervisor-network/src/l7/tls.rs @@ -8,7 +8,7 @@ //! store, terminates TLS from the client (presenting dynamic certs per hostname), //! inspects the plaintext HTTP, then re-encrypts to upstream using real root CAs. -use miette::{IntoDiagnostic, Result}; +use miette::{IntoDiagnostic, Result, miette}; use rcgen::{CertificateParams, DnType, IsCa, KeyPair, KeyUsagePurpose}; use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName}; use rustls::{ClientConfig, ServerConfig}; @@ -20,6 +20,12 @@ use tokio::io::{AsyncRead, AsyncWrite}; use tokio::net::TcpStream; use tokio_rustls::{TlsAcceptor, TlsConnector}; +#[cfg(all(feature = "bundled-ca-roots", feature = "system-ca-roots"))] +compile_error!("features bundled-ca-roots and system-ca-roots are mutually exclusive"); + +#[cfg(not(any(feature = "bundled-ca-roots", feature = "system-ca-roots")))] +compile_error!("one of bundled-ca-roots or system-ca-roots must be enabled"); + const MAX_CACHED_CERTS: usize = 256; /// System CA bundle search paths (common Linux locations). @@ -180,7 +186,7 @@ pub async fn tls_terminate_client( Ok(tls_stream) } -/// Connect TLS to an upstream server, verifying against webpki-roots. +/// Connect TLS to an upstream server, verifying against the configured CA roots. /// /// Returns a TLS stream for re-encrypted upstream communication. pub async fn tls_connect_upstream( @@ -197,34 +203,75 @@ pub async fn tls_connect_upstream( Ok(tls_stream) } -/// Build a rustls `ClientConfig` with Mozilla + system root CAs for upstream connections. +/// Build a rustls `ClientConfig` using the configured CA root source. /// -/// `system_ca_bundle` is the pre-read PEM contents of the system CA bundle -/// (from [`read_system_ca_bundle`]). Pass the same string to [`write_ca_files`] -/// to avoid reading the bundle from disk twice. -pub fn build_upstream_client_config(system_ca_bundle: &str) -> Arc { +/// In `bundled-ca-roots` mode this uses Mozilla roots from `webpki-roots` overlaid +/// with any locally-installed CAs from `system_ca_bundle` (e.g. corporate or private +/// CAs added to `/etc/pki/ca-trust`). Duplicates with the Mozilla bundle are harmless. +/// +/// In `system-ca-roots` mode this uses the platform/native trust store exclusively; +/// `system_ca_bundle` is ignored because the native store already reflects all +/// operator-installed trust anchors. +pub fn build_upstream_client_config(system_ca_bundle: &str) -> Result> { + let mut config = ClientConfig::builder() + .with_root_certificates(build_upstream_root_store(system_ca_bundle)?) + .with_no_client_auth(); + config.alpn_protocols = vec![b"http/1.1".to_vec()]; + + Ok(Arc::new(config)) +} + +fn build_upstream_root_store(system_ca_bundle: &str) -> Result { let mut root_store = rustls::RootCertStore::empty(); - root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); - // System bundles typically overlap with webpki-roots (Mozilla roots); - // duplicates are harmless and ensure we also pick up any custom/corporate CAs. - let (added, ignored) = load_pem_certs_into_store(&mut root_store, system_ca_bundle); - if added > 0 { - tracing::debug!(added, "Loaded system CA certificates for upstream TLS"); + #[cfg(feature = "bundled-ca-roots")] + { + root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); + // Overlay system/corporate CAs so custom trust anchors are honoured in + // default upstream builds. Duplicates with webpki-roots are harmless. + let (added, ignored) = load_pem_certs_into_store(&mut root_store, system_ca_bundle); + if added > 0 { + tracing::debug!(added, "loaded system CA certificates for upstream TLS"); + } + if ignored > 0 { + tracing::warn!( + ignored, + "some system CA certificates could not be parsed and were ignored" + ); + } } + + #[cfg(feature = "system-ca-roots")] + { + let _ = system_ca_bundle; // native store already includes operator-installed CAs + add_native_roots(&mut root_store)?; + } + + if root_store.is_empty() { + return Err(miette!("no TLS root certificates available")); + } + + Ok(root_store) +} + +#[cfg(feature = "system-ca-roots")] +fn add_native_roots(root_store: &mut rustls::RootCertStore) -> Result<()> { + let native_certs = rustls_native_certs::load_native_certs(); + let cert_count = native_certs.certs.len(); + let (added, ignored) = root_store.add_parsable_certificates(native_certs.certs); + let ignored = ignored + native_certs.errors.len(); + if ignored > 0 { - tracing::warn!( - ignored, - "Some system CA certificates could not be parsed and were ignored" - ); + tracing::debug!(ignored, "ignored unparsable native root certificates"); } - let mut config = ClientConfig::builder() - .with_root_certificates(root_store) - .with_no_client_auth(); - config.alpn_protocols = vec![b"http/1.1".to_vec()]; + if added == 0 { + return Err(miette!( + "no usable native TLS root certificates found ({cert_count} loaded, {ignored} ignored)" + )); + } - Arc::new(config) + Ok(()) } /// Write CA certificate files for the sandbox trust store. @@ -234,8 +281,7 @@ pub fn build_upstream_client_config(system_ca_bundle: &str) -> Arc /// 2. Combined bundle: system CAs + sandbox CA (for `SSL_CERT_FILE` which replaces default) /// /// `system_ca_bundle` is the pre-read PEM contents of the system CA bundle -/// (from [`read_system_ca_bundle`]). Pass the same string to -/// [`build_upstream_client_config`] to avoid reading the bundle from disk twice. +/// (from [`read_system_ca_bundle`]). /// /// Returns `(ca_cert_path, combined_bundle_path)`. pub fn write_ca_files( @@ -266,6 +312,7 @@ pub fn write_ca_files( /// Returns `(added, ignored)` counts. Invalid or unparseable certificates /// are silently ignored, matching the behavior of /// `RootCertStore::add_parsable_certificates`. +#[cfg_attr(feature = "system-ca-roots", allow(dead_code))] fn load_pem_certs_into_store( root_store: &mut rustls::RootCertStore, pem_data: &str, @@ -289,7 +336,7 @@ fn load_pem_certs_into_store( /// /// Returns the PEM contents of the first non-empty bundle found, or an empty /// string if none of the well-known paths exist. Call once and pass the result -/// to both [`write_ca_files`] and [`build_upstream_client_config`]. +/// to [`write_ca_files`]. pub fn read_system_ca_bundle() -> String { for path in SYSTEM_CA_PATHS { if let Ok(contents) = std::fs::read_to_string(path) @@ -299,7 +346,6 @@ pub fn read_system_ca_bundle() -> String { } } // No system bundle found — combined file will contain only the sandbox CA. - // This is acceptable since the proxy uses webpki-roots independently. String::new() } @@ -426,7 +472,7 @@ mod tests { #[test] fn upstream_config_alpn() { let _ = rustls::crypto::ring::default_provider().install_default(); - let config = build_upstream_client_config(""); + let config = build_upstream_client_config("").unwrap(); assert_eq!(config.alpn_protocols, vec![b"http/1.1".to_vec()]); } diff --git a/crates/openshell-supervisor-network/src/run.rs b/crates/openshell-supervisor-network/src/run.rs index 9553e0673..3fe810391 100644 --- a/crates/openshell-supervisor-network/src/run.rs +++ b/crates/openshell-supervisor-network/src/run.rs @@ -209,7 +209,7 @@ pub async fn run_networking( // path injected by enrich_*_baseline_paths(), so no // explicit Landlock entry is needed here. - let upstream_config = build_upstream_client_config(&system_ca_bundle); + let upstream_config = build_upstream_client_config(&system_ca_bundle)?; let cert_cache = CertCache::new(ca); let state = Arc::new(ProxyTlsState::new(cert_cache, upstream_config)); ocsf_emit!( diff --git a/tasks/rust.toml b/tasks/rust.toml index a8856377f..c72d12a1b 100644 --- a/tasks/rust.toml +++ b/tasks/rust.toml @@ -39,3 +39,14 @@ run = [ "cargo build -p openshell-sandbox --bin openshell-sandbox --no-default-features", "tasks/scripts/verify-telemetry-compiled-out.sh absent target/debug/openshell-sandbox", ] + +["rust:verify:system-ca-roots"] +description = "Verify system-ca-roots feature compiles and excludes bundled Mozilla root crates" +run = [ + # Check that the workspace, including test targets, compiles cleanly under system-ca-roots. + "cargo check --workspace --all-targets --no-default-features --features system-ca-roots", + # Guard: webpki-roots must not appear in the production dependency graph. + "bash -c 'if cargo tree -i webpki-roots --no-default-features --features system-ca-roots 2>/dev/null | grep -q webpki-roots; then echo \"ERROR: webpki-roots found in system-ca-roots production build\" >&2; exit 1; fi'", + # Guard: webpki-root-certs must not appear either (webpki-roots re-exports it). + "bash -c 'if cargo tree -i webpki-root-certs --no-default-features --features system-ca-roots 2>/dev/null | grep -q webpki-root-certs; then echo \"ERROR: webpki-root-certs found in system-ca-roots production build\" >&2; exit 1; fi'", +]