Responses WebSocket transport ignores SSL_CERT_FILE (custom CA), fails with UnknownIssuer behind TLS-intercepting proxies while HTTP honors it
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 125k
- Forks
- 19.4k
- PR merge metrics
- PR metrics pending
Description
Summary
The Responses WebSocket transport validates the upstream TLS certificate against system roots only — it ignores SSL_CERT_FILE (and appears not to share the codex_http_client::custom_ca policy). The HTTP transport honors SSL_CERT_FILE correctly.
Behind a TLS-intercepting egress proxy (corporate MITM, or a credential-injecting proxy like Agent Vault) whose CA is provided via SSL_CERT_FILE, this splits codex's behavior in two:
- Plain HTTP requests to
chatgpt.comaccept the proxy's re-signed certificate and work fine. - The WebSocket connect to
wss://chatgpt.com/backend-api/codex/responsesrejects the same certificate:
ERROR codex_api::endpoint::responses_websocket: failed to connect to websocket:
IO error: invalid peer certificate: UnknownIssuer,
url: wss://chatgpt.com/backend-api/codex/responses
From the proxy's side this looks like the client aborting the TLS handshake (EOF). The session then burns the WebSocket retry loop before any HTTP fallback (#19821), and in our deployment turns fail outright. Because the built-in openai provider hard-codes supports_websockets: true and cannot be overridden (#13103), there is no way to avoid the broken path without defining a whole custom provider.
PR #31441 ("core: preserve Responses WebSockets with system proxy") describes applying "the same effective outbound proxy and custom-CA policy as HTTP" to WebSocket connections — the proxy part works (the WS connect does route through HTTPS_PROXY), but the custom-CA part does not.
Reproduction
Reproduced on 0.147.0 and 0.154.0 (macOS, ChatGPT subscription auth). No special infrastructure needed — a ~60-line local MITM proxy and a throwaway CA:
1. Make a test CA and a re-signed leaf for chatgpt.com:
openssl genrsa -out ca.key 2048
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3 -subj "/CN=Test MITM CA" -out ca.pem
openssl genrsa -out leaf.key 2048
openssl req -new -key leaf.key -subj "/CN=chatgpt.com" -out leaf.csr
printf "subjectAltName=DNS:chatgpt.com\n" > leaf.ext
openssl x509 -req -in leaf.csr -CA ca.pem -CAkey ca.key -CAcreateserial -days 3 -sha256 -extfile leaf.ext -out leaf.pem
2. Run a re-signing CONNECT proxy (terminates TLS with leaf.pem, then decrypts and re-forwards plaintext to the real chatgpt.com:443 — i.e., a standard MITM). Minimal Python version:
import socket, ssl, threading
srv = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
srv.load_cert_chain("leaf.pem", "leaf.key")
srv.set_alpn_protocols(["http/1.1"])
up = ssl.create_default_context()
def pipe(a, b):
try:
while (d := a.recv(65536)): b.sendall(d)
except Exception: pass
def handle(c):
req = b""
while b"\r\n\r\n" not in req: req += c.recv(4096)
host, port = req.split(b"\r\n",1)[0].split(b" ")[1].decode().rsplit(":",1)
c.sendall(b"HTTP/1.1 200 Connection Established\r\n\r\n")
try:
tc = srv.wrap_socket(c, server_side=True)
print(f"handshake OK host={host}")
except ssl.SSLError as e:
print(f"HANDSHAKE REJECTED host={host} err={e}"); return
u = up.wrap_socket(socket.create_connection((host, int(port))), server_hostname=host)
threading.Thread(target=pipe, args=(tc, u), daemon=True).start(); pipe(u, tc)
s = socket.socket(); s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("127.0.0.1", 8898)); s.listen(64)
while True:
conn, _ = s.accept(); threading.Thread(target=handle, args=(conn,), daemon=True).start()
3. Run codex through it, trusting the CA the documented way:
HTTPS_PROXY=http://127.0.0.1:8898 SSL_CERT_FILE=$PWD/ca.pem RUST_LOG=error \
codex --enable respect_system_proxy exec --skip-git-repo-check "reply with the single word pong"
(We originally drove codex app-server over JSON-RPC through a full thread/start → turn/start; the behavior is the same.)
Observed
- The proxy logs
handshake OK host=chatgpt.comfor the HTTP connections — the custom CA inSSL_CERT_FILEis honored, requests flow, injection-style proxies work. - The WebSocket connect fails with
invalid peer certificate: UnknownIssueron every retry before HTTP fallback kicks in:
2026-09-18T22:41:53.392473Z ERROR codex_api::endpoint::responses_websocket: failed to connect to websocket: IO error: invalid peer certificate: UnknownIssuer, url: wss://chatgpt.com/backend-api/codex/responses
2026-09-18T22:41:53.615316Z ERROR codex_api::endpoint::responses_websocket: failed to connect to websocket: IO error: invalid peer certificate: UnknownIssuer, url: wss://chatgpt.com/backend-api/codex/responses
2026-09-18T22:41:53.850139Z ERROR codex_api::endpoint::responses_websocket: failed to connect to websocket: IO error: invalid peer certificate: UnknownIssuer, url: wss://chatgpt.com/backend-api/codex/responses
... (repeats)
The exec above eventually completes via HTTP fallback after burning the retry loop; in our app-server deployment (agent sandbox behind the same kind of proxy) turns fail outright.
Startup logging confirms the HTTP side has explicit custom-CA plumbing that the WS side apparently doesn't use — without the env var it prints:
INFO codex_http_client::custom_ca: using system root certificates because no CA override
environment variable was selected codex_ca_certificate_configured=false ssl_cert_file_configured=false
With SSL_CERT_FILE set, HTTP accepts the re-signed cert; WS still rejects it.
Expected
The WebSocket transport should build its TLS config from the same custom-CA policy as codex_http_client (per #31441's stated intent), so SSL_CERT_FILE / CODEX_CA_CERTIFICATE apply to wss:// connections too.
Workaround we're using
Point codex at a custom provider that opts out of WebSockets (since the built-in openai provider can't be overridden, per #13103):
model_provider = "openai_https"
[model_providers.openai_https]
name = "OpenAI (HTTPS only)"
wire_api = "responses"
requires_openai_auth = true
base_url = "https://chatgpt.com/backend-api/codex"
supports_websockets = false
This restores fully working turns through the same MITM proxy — confirming the only broken piece is WS certificate validation.
Environment
- codex-cli 0.147.0 and 0.154.0, macOS (arm64)
- Auth: ChatGPT subscription (
auth_mode=Chatgpt) --enable respect_system_proxy, proxy viaHTTPS_PROXY, CA viaSSL_CERT_FILE
Related: #31441 (WS + system proxy / custom-CA policy), #19821 (WS connect failures burn all retries before HTTP fallback), #13103 (built-in provider supports_websockets not overridable), #28503 (feature request for a first-class WS disable).
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the codex_http_client::custom_ca policy and the responses_websocket connection entry point, comparing how each builds TLS configuration. Reproduce with HTTPS_PROXY and SSL_CERT_FILE using the provided proxy, then verify that WebSocket connections accept the custom CA and no longer fail with UnknownIssuer.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- networking, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 58/100