Skip to content

client: apply custom CA handling to websocket TLS#14240

Merged
joshka-oai merged 1 commit intojoshka/login-custom-ca-shared-clientsfrom
joshka/login-custom-ca-websocket-tls
Mar 12, 2026
Merged

client: apply custom CA handling to websocket TLS#14240
joshka-oai merged 1 commit intojoshka/login-custom-ca-shared-clientsfrom
joshka/login-custom-ca-websocket-tls

Conversation

@joshka-oai
Copy link
Collaborator

@joshka-oai joshka-oai commented Mar 10, 2026

Stacked PRs

This work is split across three stacked PRs:

Review order: #14178, then #14239, then #14240.

Builds on top of #14239, which itself builds on #14178.

Problem

The shared custom-CA path covered reqwest clients after #14239, but secure websocket connections still used tungstenite's default TLS connector. In enterprise MITM setups, that meant HTTPS requests could succeed while websocket connections still failed because they were not loading the same custom root CA bundle.

What This Delivers

Secure websocket connections now honor the same custom CA configuration as the HTTPS clients introduced in the earlier stacked PRs. After this lands, setups that already work for HTTPS behind an intercepting proxy can also work for websocket-backed features instead of failing later during websocket TLS setup.

For users and operators, the configuration does not change: CODEX_CA_CERTIFICATE wins, then SSL_CERT_FILE, then system roots. The difference is that websocket TLS now participates in that same policy.

Mental model

There is now one shared custom-CA policy for both HTTPS and secure websocket connections.

reqwest callers continue to use build_reqwest_client_with_custom_ca(...).
Websocket callers now ask codex-client for a rustls client config when a custom CA bundle is configured, then pass that config into tungstenite explicitly.

The env precedence remains the same:

  • CODEX_CA_CERTIFICATE wins
  • otherwise fall back to SSL_CERT_FILE
  • otherwise use system roots

Non-goals

This does not add a live end-to-end TLS interception test. It does not change the fallback behavior when no custom CA env var is set. It also does not try to generalize all websocket transport concerns into codex-client; it only extends the shared CA-loading policy to the websocket TLS boundary.

Tradeoffs

The main tradeoff is that websocket callers now have an explicit shared rustls-config path only when a custom CA bundle is configured. That keeps the normal no-override path simple, but it means the websocket implementation still has two TLS setup paths: default connector when no override is set, explicit connector when it is.

This PR also renames the shared CA error type to match reality. The error is no longer reqwest-specific, because the same CA-loading failures now surface from websocket TLS configuration too.

Architecture

codex-client::custom_ca now exposes an optional rustls client-config builder alongside the existing reqwest client builder.

The websocket clients in codex-api now consume that shared config:

  • responses websocket connections
  • realtime websocket connections

When no custom CA env var is set, websocket callers still use their ordinary default connector path. When a custom CA bundle is configured, they build an explicit rustls connector with the same roots that the shared HTTPS path uses.

Observability

The websocket path now inherits the same CA-loading logs and user-facing errors as the shared HTTPS path. Failures to read, parse, or register custom CA certificates are surfaced before websocket TLS is attempted, instead of failing later as an opaque certificate-validation problem.

Tests

This PR relies on the existing codex-client CA tests plus the websocket-focused suites in codex-api and codex-core.

Automated coverage run for this stack:

  • cargo test -p codex-client -p codex-api
  • cargo test -p codex-core websocket_fallback -- --nocapture

Manual validation:

  • CODEX_CA_CERTIFICATE=~/.mitmproxy/mitmproxy-ca-cert.pem HTTPS_PROXY=http://127.0.0.1:8080 just codex
  • with mitmproxy installed via brew install mitmproxy
  • and mitmproxy also configured as the system proxy in macOS Wi-Fi settings

That manual check was specifically useful because it exercises the websocket path behind a real intercepting proxy instead of only the reqwest HTTPS path.

@joshka-oai joshka-oai force-pushed the joshka/login-custom-ca-shared-clients branch from 5e3eb1c to 6c2de29 Compare March 10, 2026 19:10
@joshka-oai joshka-oai force-pushed the joshka/login-custom-ca-websocket-tls branch 2 times, most recently from 2fbc0a5 to 7703ac6 Compare March 10, 2026 19:13
@joshka-oai joshka-oai force-pushed the joshka/login-custom-ca-shared-clients branch from 6c2de29 to 18917a9 Compare March 10, 2026 19:13
@joshka-oai joshka-oai force-pushed the joshka/login-custom-ca-websocket-tls branch from 7703ac6 to 002245e Compare March 10, 2026 19:33
@joshka-oai joshka-oai force-pushed the joshka/login-custom-ca-shared-clients branch from 18917a9 to 9490701 Compare March 10, 2026 19:33
Use the shared custom CA loader to build explicit rustls configs for
the responses and realtime websocket clients, so secure websocket
connections honor CODEX_CA_CERTIFICATE / SSL_CERT_FILE alongside the
existing reqwest paths.

Co-authored-by: Codex <noreply@openai.com>
@joshka-oai joshka-oai force-pushed the joshka/login-custom-ca-shared-clients branch from 9490701 to e6446a9 Compare March 12, 2026 23:34
@joshka-oai joshka-oai force-pushed the joshka/login-custom-ca-websocket-tls branch from 002245e to 017a0e7 Compare March 12, 2026 23:34
@joshka-oai joshka-oai merged commit b5f3e63 into joshka/login-custom-ca-shared-clients Mar 12, 2026
16 of 20 checks passed
@joshka-oai joshka-oai deleted the joshka/login-custom-ca-websocket-tls branch March 12, 2026 23:34
@github-actions github-actions bot locked and limited conversation to collaborators Mar 12, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants