feat(sandbox,podman): trust corporate CA for https:// proxies and intercepted TLS - #2512
feat(sandbox,podman): trust corporate CA for https:// proxies and intercepted TLS#2512feloy wants to merge 1 commit into
Conversation
|
Rather than make a change for just podman, can you explore having parity across all compute drivers, or note why its not possible before we merge this? |
My idea would be to implement by pieces as small as possible. The first part (#2245) is connection to proxy with http only and Podman only. This PR is connection to proxy with https and certificate, for Podman only. The follow-up PRs would be the full stack (http+https) for each other driver. For Docker, it should be very similar, but I still didn't investigate what would be needed for others drivers. |
|
Just a quick note to say I tested this PR and it worked in my environment. This got things working with my proxy that does SSL interception. It may be useful to separate the host path and gateway container paths so they don't need to match. For example, something like: With the current setup, I had to be careful to make sure the host and container paths to ca-bundle.pem matched. Otherwise sandbox creation fails when the gateway tries to validate the (single) path specified by proxy_ca_bundle (a host path). |
@kevin-pedretti thanks for testing this PR. I don't think this change is necessary: |
…ercepted TLS The corporate proxy chaining only accepted plain http:// proxy URLs, so operators whose forward proxy terminates TLS with a private corporate CA had no way to reach it, and TLS-intercepting proxies (mitmproxy, squid ssl-bump) that re-sign tunneled server certificates broke every upstream handshake after CONNECT. The supervisor now accepts https:// proxy URLs: it wraps the connection to the proxy in TLS before the CONNECT handshake, verifying the proxy certificate against the built-in Mozilla roots, the system CA bundle, and an optional operator corporate CA bundle. The upstream dial returns a Plain/Tls stream enum consumed generically by the relay paths. The corporate CA is delivered as a driver-supplied command-line argument (--upstream-proxy-ca-bundle), never an environment variable, matching the hardened proxy-config model where a sandbox image cannot influence the operator's egress boundary. It is folded into the sandbox combined trust bundle (write_ca_files) and the L7 upstream verification store (build_upstream_client_config) at startup, so intercepted upstream handshakes succeed and sandbox workloads trust the re-signed certificates. Configuration is fail-closed: a CA bundle set without a proxy, or an unreadable or certificate-free file, is fatal rather than silently weakening the trust boundary. The shared parse_upstream_proxy_url validator accepts https:// (recording the scheme so the driver and supervisor agree), keeping the explicit-port requirement. The Podman driver gains a proxy_ca_bundle operator setting (TOML, --sandbox-proxy-ca-bundle, OPENSHELL_SANDBOX_PROXY_CA_BUNDLE) that bind-mounts the host PEM read-only into the sandbox (a CA certificate is not secret) and points --upstream-proxy-ca-bundle at it, with a create-time readability check. The standalone dev gateway task passes OPENSHELL_SANDBOX_PROXY_CA_BUNDLE through to the generated podman config, so a local gateway can be pointed at a TLS-intercepting proxy without hand-editing the regenerated TOML. Refs NVIDIA#1792 Signed-off-by: Philippe Martin <phmartin@redhat.com>
johntmyers
left a comment
There was a problem hiding this comment.
gator-agent
PR Review Status
Validation: This PR is project-valid as a focused follow-up to the already-triaged corporate-proxy gap in #1792 and the earlier HTTP proxy work in #2245. Thanks @derekwaynecarr — I checked the driver-parity concern you raised. @feloy explained that this is an incremental Podman delivery with other drivers planned as follow-ups; the supervisor portion is shared, so that scope choice does not prevent code review, while maintainers retain the final scope decision. I also checked @feloy's explanation of the CA path behavior: the configured host path is mounted at a fixed container path, matching the current driver implementation.
Head SHA: a6b67a548178b5f4a65aeaf7e93adef947a53c49
Base SHA: 877ddbacb4b915fa1d5bc6f02302d9a5c13a18e1
Merge base SHA: 877ddbacb4b915fa1d5bc6f02302d9a5c13a18e1
Patch ID: f7885d7f4a97aabbe6342feda4064351f7b4d5ef
Gator payload: 4
Review mode: initial
Previous reviewed SHA: none
Review budget exhausted: no
Maintainer decision required: no
Blocking findings:
GATOR-a6b67a54-01: the corporate CA validation counts decoded PEM blocks without checking whether rustls accepts any certificate as a trust anchor, so a malformed bundle can pass startup and fail later despite the documented fail-closed behavior. See the inline finding.
Carried findings:
- None
Non-blocking suggestions:
- For an
https://proxy, consider allowingproxy_auth_filewithoutproxy_auth_allow_insecure; the credential is sent only after the verified TLS connection, so the current plaintext-risk acknowledgement and diagnostic are misleading for this newly supported path. Relevant sites:crates/openshell-driver-podman/src/config.rs:368,crates/openshell-supervisor-network/src/upstream_proxy.rs:483, andcrates/openshell-driver-podman/README.md:370.
Docs: Fern reference docs and the Podman/architecture documentation are updated.
Next state: gator:in-review
| // Reject a file that parses to zero certificates up front instead of | ||
| // silently trusting only the built-in roots (or failing every proxy | ||
| // handshake later with an opaque TLS error). | ||
| let count = rustls_pemfile::certs(&mut pem.as_bytes()).flatten().count(); |
There was a problem hiding this comment.
gator-agent
Warning — GATOR-a6b67a54-01
Invariant: A configured corporate CA bundle must contain at least one X.509 certificate that rustls actually accepts as a trust anchor; syntactically valid PEM framing with invalid DER must not satisfy fail-closed validation and silently fall back to other roots.
Prerequisite: A Podman gateway operator configures proxy_ca_bundle with a non-empty regular file whose PEM block decodes but whose DER is not a valid X.509 trust anchor, such as a truncated or incorrectly generated bundle.
Entry point → sink: documented [openshell.drivers.podman] proxy_ca_bundle with a supported corporate proxy → UpstreamProxyConfig::from_args accepts it, then build_upstream_client_config ignores the unusable certificate and the first private-CA proxy or intercepted-upstream TLS handshake fails.
Base → head: the base had no CA-bundle input and rejected https:// proxy URLs → this head counts every successfully base64-decoded CERTIFICATE block with rustls_pemfile::certs(...).flatten() even when RootCertStore later rejects its DER; built-in roots keep the aggregate store non-empty, so startup succeeds.
Impact: A documented present-but-invalid setting is accepted rather than failing closed. Sandboxes start, but corporate TLS egress then fails at runtime with an opaque outage; a publicly trusted proxy listener can also operate although the explicitly configured corporate bundle contributed no usable anchor.
Reproducer: In the upstream_proxy unit module, install the ring provider, write -----BEGIN CERTIFICATE-----\nAQID\n-----END CERTIFICATE-----\n to a temporary file, and construct config with HTTPS_PROXY=https://proxy.corp.com:3130 and PROXY_CA_BUNDLE=<temp path>. Head returns Ok; the expected deterministic result is an error naming PROXY_CA_BUNDLE and reporting zero usable certificates/trust anchors.
PR ownership: This PR introduces proxy_ca_bundle, documents unreadable or certificate-free bundles as fatal, and adds this insufficient count check; the misleading acceptance does not exist at base. The same helper is reread from run.rs:223, so both proxy-listener and intercepted-upstream paths share the invariant.
Requested change: Validate decoded certificates with the same rustls RootCertStore acceptance path used for TLS and reject the bundle unless at least one certificate is added successfully. Mixed bundles may remain tolerant, but add the invalid-DER reproducer so zero accepted anchors fail at supervisor startup.
Summary
Adds support for
https://corporate egress proxies and for trusting an operator-provided corporate CA bundle, so sandboxes can reach a forward proxy that terminates TLS with a private CA and can operate behind a TLS-intercepting proxy (mitmproxy, squidssl-bump) that re-signs tunneled server certificates. The feature is delivered through the hardened, driver-supplied argument model — never environment variables — and is fail-closed.Related Issue
Refs #1792
Changes
openshell-core):parse_upstream_proxy_urlnow acceptshttps://(recording the scheme via a newUpstreamProxyAddr.secure), keeping the explicit-port requirement; adds thePROXY_CA_MOUNT_PATHcontainer mount constant.openshell-supervisor-network):ProxyEndpointgains a TLS client config; for anhttps://proxy the connection to the proxy is wrapped in TLS (verifying the proxy certificate) before the CONNECT handshake.PrefixedStreamnow wraps aPlain/TlsUpstreamStreamenum so the relay paths consume either transport transparently.build_upstream_client_config;tls_connect_upstreamwas already generic, sol7/tls.rsis unchanged.run.rsfolds the corporate CA into the sandbox combined trust bundle (write_ca_files) and the L7 upstream verification store, so intercepted upstream handshakes succeed and sandbox workloads trust the re-signed certificates.--upstream-proxy-ca-bundlesupervisor argument (noenv =); new Podman driverproxy_ca_bundlesetting (TOML key,--sandbox-proxy-ca-bundle,OPENSHELL_SANDBOX_PROXY_CA_BUNDLE). The driver validates the pairing (a CA bundle requires a proxy URL), bind-mounts the host PEM read-only into the sandbox (a CA certificate is not secret, so a plain bind mount rather than a driver secret), and performs a create-time readability check.tasks/scripts/gateway.sh) passesOPENSHELL_SANDBOX_PROXY_CA_BUNDLEthrough to the generated Podman config, so a local gateway can be pointed at a TLS-intercepting proxy without hand-editing the regenerated TOML.docs/reference/gateway-config.mdx, the Podman driverREADME.md, and thearchitecture/sandbox.mdtrust-model section.Testing
mise run pre-commitpasseshttp/https, socks rejected), TLS-wrapped CONNECT against a fake TLS proxy (trusted-CA success and untrusted-cert rejection), fail-closed CA-bundle validation, and Podman driver config validation + bind-mount/argv assertions.podman_corporate_proxycase drives anhttps://proxy end to end: the fake proxy self-signs, exposes its CA, and that CA is fed back viaproxy_ca_bundle. (Runs only in the Podman e2e job.)https://proxy with its CA supplied viaproxy_ca_bundle. This exercises both halves in one run: the TLS-wrapped CONNECT to the proxy listener, and the upstream certificate that mitmproxy re-signs with the same CA.Checklist