TLSv1.3-only streamable HTTP MCP behind nginx fails with native-tls backend on macOS
Summary
When Codex is configured to use a streamable HTTP MCP server behind an nginx 1.18.0 reverse proxy that only offers TLSv1.3, Codex fails to connect on macOS with a low-level TLS error originating from the native TLS stack:
rmcp::transport::worker: worker quit with fatal: Transport channel closed, when Client(reqwest::Error { kind: Request, url: "https://<endpoint>", source: hyper_util::client::legacy::Error(Connect, Error { code: -9836, message: "bad protocol version" }) })
Enabling TLSv1.2 on the nginx ssl_protocols line immediately makes the endpoint work again with Codex, while TLSv1.3 alone still works fine with other clients.
Environment
- Codex CLI: 0.58.0 (Rust backend)
- OS: macOS (Apple Silicon)
- Server: simple HTTPS endpoint behind nginx 1.18.0
- nginx TLS config (simplified):
ssl_protocols TLSv1.3;(failing case)ssl_protocols TLSv1.2 TLSv1.3;(working case)
Minimal reproduction (Codex + minimal nginx endpoint)
This repro uses Codex configured with a streamable HTTP MCP entry, but the MCP URL just points at a trivial nginx location that returns a static 200 response. The backend does not need to implement the MCP protocol; the failure occurs at the TLS layer before HTTP semantics matter.
- Configure nginx with a TLSv1.3-only test endpoint. For example:
```nginx
server {
listen 443 ssl http2;
server_name tls13-only.example.com;
ssl_protocols TLSv1.3; # failing case
# ssl_protocols TLSv1.2 TLSv1.3; # working case
# certificate directives elided for brevity
location /test {
return 200 'ok';
}
}
```
Reload nginx so the TLSv1.3-only config is active.
- Add a streamable HTTP MCP entry in Codex that points at this endpoint. In
~/.codex/config.toml:
``toml``
[mcp_servers.tls13_test]
transport = "streamable_http"
url = "https://tls13-only.example.com/test"
- Run Codex against this MCP server with a simple input. For example:
``bash``
codex \
--config default_mcp_server_name=tls13_test \
exec --skip-git-repo-check "ping"
- Observe the failure on macOS with TLSv1.3-only nginx. Codex logs an error like:
``text``
rmcp::transport::worker: worker quit with fatal: Transport channel closed, when Client(reqwest::Error { kind: Request, url: "https://tls13-only.example.com/test", source: hyper_util::client::legacy::Error(Connect, Error { code: -9836, message: "bad protocol version" }) })
- Flip nginx back to allow TLSv1.2 and retest. Change
ssl_protocolsto:
``nginx``
ssl_protocols TLSv1.2 TLSv1.3;
Reload nginx and re-run the same codex exec command; Codex now successfully connects to the endpoint (the MCP handshake will fail later because /test is not actually an MCP server, but the TLS error disappears).
Root cause analysis
codex-rmcp-clientdepends on:
``toml``
reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "rustls-tls"] }
which indicates the intention to use the rustls TLS backend for MCP HTTP.
- Elsewhere in the workspace,
reqwestwas pulled in with default features (which includenative-tls), for example:
```toml
# codex-rs/Cargo.toml
reqwest = "0.12"
# codex-rs/lmstudio/Cargo.toml (upstream main)
reqwest = { version = "0.12", features = ["json", "stream"] }
# codex-rs/tui/Cargo.toml (upstream main)
reqwest = { version = "0.12", features = ["json"] }
```
- Cargo unified these features, so the final
reqwest 0.12in the Codex CLI had bothnative-tlsandrustls-tlsenabled.
- In that combined configuration,
reqweston macOS chooses the OS-native TLS backend by default. Against a TLSv1.3-only nginx 1.18.0 endpoint, that backend fails with the OSStatus -9836 ("bad protocol version") error, which Codex sees.
- A standalone reproducer using
reqwest 0.12+native-tls(no Codex code) fails in the same way against the same endpoint, while areqwest 0.12+rustls-tlsclient succeeds, confirming that the failure is tied to the native TLS backend rather than Codex logic or rustls.
Proposed fix
Use reqwest with default-features = false at the workspace level and explicitly turn on rustls-tls in the crates that need HTTPS, so that Codex’s RMCP HTTP client uses rustls consistently:
- In
codex-rs/Cargo.toml:
```toml
[workspace.dependencies]
- reqwest = "0.12"
- reqwest = { version = "0.12", default-features = false }
```
- In crates that use reqwest directly (e.g.
codex-rs/lmstudio,codex-rs/tui), change:
``toml``
reqwest = { version = "0.12", features = ["json", ...] }
to:
``toml``
reqwest = { version = "0.12", default-features = false, features = ["json", ..., "rustls-tls"] }
A draft PR implementing this approach is available here: https://github.com/openai/codex/pull/6873
Impact
- This resolves the TLS handshake failure for TLSv1.3-only HTTP endpoints behind nginx on macOS when accessed via Codex’s RMCP HTTP client.
- It also aligns the implementation with the apparent intent in
codex-rmcp-clientto use rustls for MCP HTTP traffic.
5 Comments
The same issue occurred when connecting to LiteLLM behind nginx.
Logs output by the VS Code extension:
Closing outdated bug.
FYI: you need to change your LiteLLM proxy to allow TLS1.2, codex can not do TLS1.3.
I can confirm this bug is still very much alive on the latest release, and — importantly — it's not MCP-specific. It also breaks the core model-provider
/v1/responsesstreaming connection, which matches what @zolgear reported above againstcodex_api::endpoint::responses. Given that, I think closing this as "outdated" was premature; requesting a reopen.Environment
npm install -g @openai/codex)codex execmodel_provider = "proxy"custom OpenAI-compatible provider,wire_api = "responses",base_urlpointed at a Cloudflare-fronted origin that enforces a TLS 1.3 minimumcodex doctorconfirms all of this — see below)config.tomland identical Codex version work reliably against the same endpoint from Linux hosts (wherenative-tlsmaps to OpenSSL) — only macOS reproduces it, consistent with this issue's title/diagnosis of thenative-tls/Secure Transport backend.codex doctorself-diagnosisPacket-level root cause (this is the part I think is new information)
I captured traffic during live failures. The pattern is deterministic and clarifies what's actually happening — and it contradicts the workaround suggested above ("codex can not do TLS1.3"). Codex can negotiate TLS 1.3 correctly — it does so successfully on some connection attempts. The bug is that it's inconsistent: it silently alternates between two different TLS behaviors across connection attempts within the same run, and roughly 80-90% of new connections land on the broken one.
Failing attempts (the majority, in bursts of many rapid reconnects):
15 03 01 00 02 02 46— TLS record type0x15(Alert), level0x02(fatal), description0x46= 70 =protocol_version.client_version = 0x0303(TLS 1.2), cipher suite list is all legacy CBC/GCM suites (no0x1301/0x1302/0x1303),supported_groupsonly listssecp256r1/secp384r1/secp521r1(no x25519), 8-entry legacysignature_algorithms, and nosupported_versions(0x002b) orkey_share(0x0033) extension at all — i.e. it never advertises TLS 1.3 support. No GREASE values either. This fingerprint doesn't match rustls or OpenSSL/BoringSSL output — it matches Apple Secure Transport's classic ClientHello shape.stream disconnected before completion: error sending request for url (...).Succeeding attempts (the minority): full modern TLS 1.3 ClientHello with
supported_versions,key_share(observed with a post-quantum hybrid group,X25519MLKEM768), GREASE, and modern cipher suites — negotiates cleanly, streams the SSE response to completion with no errors.In one captured burst I counted 9 failed connection attempts out of 10 before one landed on the working path.
curlagainst the exact same URL, same machine, same moment, succeeds via modern TLS 1.3 on 100% of attempts (obviously since it doesn't have this dual-backend issue) — confirming this isn't a server-side/network issue, DNS, VPN, or MTU problem, it's purely which TLS backend/code path a given Codex connection attempt happens to use.Why this matters beyond MCP
This confirms the
reqwest/native-tlsvsrustls-tlssplit described in the original report isn't isolated tocodex-rmcp-client— the corecodex_http_clientused for the primary Responses API streaming connection exhibits the exact same intermittent backend selection, with the exact sameprotocol_versionfatal alert. Anyone whose OpenAI-compatible endpoint (self-hosted, LiteLLM, Cloudflare-fronted, etc.) enforces TLS 1.3 will hit this on macOS specifically, on the main request path, not just MCP.Happy to provide the full raw
RUST_LOG=tracelogs / pcap if useful for triage — trimmed here for length and to avoid pasting any account-identifying info.@cjangrist, this bug report is very stale. If you are still seeing similar symptoms with the latest versions, please open a new issue and include the details requested in the bug report template.