Skip to content

feat(kubernetes): support corporate proxy CA bundle for https:// and TLS-intercepting proxies #3443

Description

@feloy

Follow-up to #1792, tracking the Kubernetes driver the way #3088 tracked the VM driver.

User Story

As a platform operator running OpenShell on Kubernetes behind a corporate egress proxy, I want to supply the corporate CA bundle to sandbox supervisors, so that agents can reach policy-approved external APIs through an https:// forward proxy or a TLS-intercepting proxy whose certificates are signed by a private CA.

Problem Statement

#1792 added corporate forward-proxy egress. The Podman (#2512) and VM (#3090) drivers both expose proxy_ca_bundle; the Kubernetes driver does not.

[openshell.drivers.kubernetes] accepts https_proxy, no_proxy, proxy_auth_secret_name, proxy_auth_secret_key, proxy_auth_allow_insecure, and proxy_connect_by_hostname (crates/openshell-driver-kubernetes/src/config.rs:201-219), and the supervisor command it builds never includes --upstream-proxy-ca-bundle (crates/openshell-driver-kubernetes/src/sandbox_runtime.rs:283-298). The Helm chart has no matching value (deploy/helm/openshell/values.yaml:58).

An https:// proxy URL is accepted by Kubernetes-driver validation — parse_upstream_proxy_url accepts both schemes (crates/openshell-core/src/driver_utils.rs:289) — and reaches the supervisor, which does TLS-wrap the connection. Verification then uses only the built-in Mozilla roots plus the supervisor image's system bundle, so a proxy certificate issued by a private corporate CA fails the handshake with no way to supply the anchor. The same gap applies to a TLS-intercepting proxy reached over http://, which re-signs tunneled upstream certificates with its own CA.

Two secondary defects in the same code path:

  • The Kubernetes driver hand-rolls its proxy validation (config.rs:379) instead of calling the shared validate_upstream_proxy_settings (driver_utils.rs:651), so the two can drift.
  • As a result it requires proxy_auth_allow_insecure = true whenever a credential Secret is configured, including for an https:// proxy (config.rs:447), where the credential travels inside a verified TLS session. The shared validator correctly waives the acknowledgement in that case (driver_utils.rs:691).

Impact / Why This Matters

Operators on Kubernetes whose proxy presents a private-CA certificate, or who sit behind a TLS-intercepting proxy, have no working configuration. Setting https_proxy = "https://..." passes startup validation and then fails at sandbox egress, which reads as a runtime networking fault rather than a missing capability.

There is no workaround. The Kubernetes driver exposes no pod-template or volume override, so an operator cannot mount a CA over the supervisor container's system trust store, and sandbox pods run with automountServiceAccountToken: false (sandbox_runtime.rs:401). The only remaining option is forking the supervisor image per corporate environment.

The documentation is currently accurate — "Only http:// proxy endpoints and TLS CONNECT traffic are supported" (docs/reference/gateway-config.mdx:563) — so this is a documented capability gap rather than a silent bug. But it blocks exactly the enterprise Kubernetes deployments #1792 was opened for, and it was explicitly requested there: "If the server address uses https, we need option to pass custom CA."

Proposed Design

Add proxy_ca_bundle to [openshell.drivers.kubernetes], keeping the operator-facing key identical to the Podman and VM drivers so the corporate-proxy configuration surface stays uniform across compute backends.

The value is a path the gateway Pod can read — supplied by the operator the same way any other gateway file is, not a reference to an object in the sandbox namespace. The gateway is responsible for getting those bytes to each sandbox supervisor.

Observable behavior:

  • With proxy_ca_bundle set, the supervisor trusts the bundle for the TLS handshake with an https:// proxy, for upstream certificates re-signed by a TLS-intercepting proxy, and in the trust bundle exposed to sandbox processes — matching the Podman and VM drivers exactly.
  • Configuration is fail-closed and consistent with the existing drivers: an empty value, a path that cannot be read, a non-regular file, a bundle over the size bound, a bundle contributing no usable trust anchor, or proxy_ca_bundle without https_proxy is a startup or sandbox-create error naming the offending key — never a silent fall-back to direct dialing or to the built-in roots.
  • The trust anchor a sandbox starts with does not change underneath it for that sandbox's lifetime.
  • The setting works in all three workspace modes (shared, managed, operator), including namespaces the gateway creates itself.
  • The Helm chart exposes the bundle under upstreamProxy, mounts it into the gateway Pod, and renders the resulting proxy_ca_bundle path into gateway.toml.
  • The Kubernetes driver's proxy validation is replaced by the shared validate_upstream_proxy_settings, which also drops the incorrect proxy_auth_allow_insecure requirement for an https:// proxy.

Security note for the implementation: the corporate CA is folded into three trust stores (crates/openshell-supervisor-network/src/run.rs:354-381) — the proxy-listener handshake, the L7 upstream verification store, and the sandbox trust bundle. Whoever controls those bytes can transparently intercept all sandbox egress, and OpenShell's L7 enforcement can no longer distinguish a genuine upstream certificate from a re-signed one. That is inherent to TLS interception and already accepted for Podman and VM, but it is the reason the bundle must be anchored in the gateway's own trust domain rather than in the workload namespace.

Acceptance Criteria

  • [openshell.drivers.kubernetes] proxy_ca_bundle is accepted and documented alongside the other corporate-proxy keys.
  • A sandbox whose gateway sets https_proxy = "https://..." with a private-CA proxy completes policy-approved TLS egress; without proxy_ca_bundle the same configuration fails closed with a clear error.
  • A TLS-intercepting proxy reached over http:// works when its re-signing CA is supplied via proxy_ca_bundle.
  • Invalid proxy_ca_bundle values (empty, unreadable, non-regular file, oversized, certificate-free, or set without https_proxy) fail closed with an error naming the key.
  • The setting works in shared, managed, and operator workspace modes.
  • The Helm chart exposes the CA bundle under upstreamProxy and renders proxy_ca_bundle; chart tests cover the rendered output.
  • The Kubernetes driver validates corporate-proxy settings through openshell_core::driver_utils::validate_upstream_proxy_settings, and a credential Secret with an https:// proxy no longer requires proxy_auth_allow_insecure.
  • docs/reference/gateway-config.mdx and docs/reference/sandbox-compute-drivers.mdx no longer state that only http:// proxy endpoints are supported for Kubernetes.
  • Architecture docs and the relevant crate READMEs reflect the new setting.

Alternatives Considered

Reference a pre-existing ConfigMap in the sandbox namespace by name/key, mirroring how proxy_auth_secret_name handles proxy credentials. Rejected on three grounds:

  1. It widens the trust boundary from the gateway's trust domain to anyone holding update on ConfigMaps in the workload namespace. Given the three-store fold described above, that is control over interception of all sandbox egress.
  2. Kubelet live-updates mounted ConfigMaps, so the trust anchor is not pinned.
  3. It cannot work in managed or operator workspace mode: the gateway auto-creates per-workspace namespaces (crates/openshell-driver-kubernetes/src/driver.rs:961) and deploy/helm/openshell/templates/role.yaml grants it no ConfigMap verbs at all, so no operator can pre-provision an object into a namespace that does not yet exist.

The credential precedent does not fully transfer. In shared mode the gateway holds no get on Secrets at all (role.yaml:54-58 grants create, list, delete). In multi-namespace modes it does hold name-restricted get + patch on explicitly configured Secrets (clusterrole.yaml:105-119), which is how ensure_tls_secret copies TLS and image-pull Secrets into workspace namespaces. That mechanism could in principle carry a credential too, but a CA certificate is not secret, so reading it from the gateway's own filesystem is simpler and avoids widening what the gateway may read.

Inline the PEM in gateway.toml. Works, and needs no new RBAC, but bloats the rendered config, diverges from the proxy_ca_bundle path semantics the other drivers use, and gives up the existing bounded-read and regular-file checks.

Document the gap and require a forked supervisor image with the corporate CA baked into its system trust store. Rejected: it pushes image maintenance onto every affected operator, ties trust-anchor rotation to an image rebuild, and would still leave the https:// proxy URL misleadingly accepted by validation.

Agent Investigation

Supervisor-side support is already complete and driver-independent. upstream_proxy.rs TLS-wraps the proxy connection for a secure URL and builds its trust store from the built-in Mozilla roots, the system bundle, and the optional corporate bundle (upstream_proxy.rs:644-660). Validation is shared between host and guest via read_upstream_proxy_ca_bundle_file (driver_utils.rs:484), which bounds the read at 1 MiB, rejects non-regular files, and requires rustls to accept at least one anchor rather than merely parsing PEM framing (validate_upstream_proxy_ca_bundle_pem, driver_utils.rs:520). The bundle is validated even for an http:// proxy, because an intercepting proxy reached in plaintext still re-signs tunneled certificates (upstream_proxy.rs:530). Only the Kubernetes driver's plumbing is missing.

Related pre-existing gap, same feature, tracked separately: proxy_auth_secret_name is documented as "an existing Secret in the sandbox namespace," which no operator can pre-provision in managed or operator workspace mode for the same auto-created-namespace reason described above. Scoped out of this issue rather than addressed here; the ensure_tls_secret copying mechanism (driver.rs:1121) that already carries TLS and image-pull Secrets into workspace namespaces looks directly reusable for it.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    state:triage-neededOpened without agent diagnostics and needs triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions