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.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗