openai / openai/codex

Responses WebSocket transport ignores SSL_CERT_FILE (custom CA), fails with UnknownIssuer behind TLS-intercepting proxies while HTTP honors it

Open
#46,489 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug CLI connectivity
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.com accept the proxy's re-signed certificate and work fine.
  • The WebSocket connect to wss://chatgpt.com/backend-api/codex/responses rejects 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/startturn/start; the behavior is the same.)

Observed

  • The proxy logs handshake OK host=chatgpt.com for the HTTP connections — the custom CA in SSL_CERT_FILE is honored, requests flow, injection-style proxies work.
  • The WebSocket connect fails with invalid peer certificate: UnknownIssuer on 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 via HTTPS_PROXY, CA via SSL_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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.