Files
Winston Howes 989f55defa feat(network-proxy): experimental local credential broker (#28034)
## Why

Codex child processes can inherit injectable local credentials directly,
which lets commands read and exfiltrate the real values. This
experimental slice keeps supported workflows working while moving those
credentials behind the managed network proxy.

This PR contains only the proxy-owned broker implementation. The Codex
config and runtime integration is stacked separately in #29752.

## What changed

- discover supported credentials during child setup, retain real values
only in the in-memory proxy broker, and replace them with shaped dummy
values
- require a presented dummy to select a stored credential and preserve
unrelated explicit authorization headers
- bind GitHub cloud, GitHub Enterprise, and OpenAI credentials to their
intended hosts
- inject credentials only into TLS traffic by default; plaintext
injection requires the explicit dangerous opt-in
- use TLS ClientHello routing for CONNECT so non-TLS protocols remain
opaque tunnels
- expose a pure API that identifies environment keys still holding
broker-generated dummies without mutating the caller's environment

## Scope

- supported credentials: `GH_TOKEN`, `GITHUB_TOKEN`,
`GH_ENTERPRISE_TOKEN`, `GITHUB_ENTERPRISE_TOKEN`, and `OPENAI_API_KEY`
- GitHub cloud credentials match `github.com`, `api.github.com`, and
`*.ghe.com`
- GitHub Enterprise credentials match only the normalized non-cloud
`GH_HOST`
- OpenAI API keys match only `api.openai.com`
- this does not cover SSH agents, kube client certificates, filesystem
secret discovery, or context-injected secret scrubbing

## Validation

- `just test -p codex-network-proxy` (191 passed)
- focused opaque CONNECT, plaintext opt-in, dummy-selection, and
child-isolation regressions passed
- scoped Clippy check for `codex-network-proxy` passed

---------

Co-authored-by: viyatb-oai <viyatb@openai.com>
Co-authored-by: Codex <noreply@openai.com>
989f55defa ยท 2026-06-24 13:21:16 -07:00
History
..

codex-network-proxy

codex-network-proxy is Codex's local network policy enforcement proxy. It runs:

  • an HTTP proxy (default 127.0.0.1:3128)
  • a SOCKS5 proxy (default 127.0.0.1:8081, enabled by default)

It enforces an allow/deny policy and a "limited" mode intended for read-only network access.

Quickstart

1) Configure

codex-network-proxy reads from Codex's merged config.toml (via codex-core config loading).

Network settings live under the selected permissions profile. Example config:

default_permissions = "workspace"

[permissions.workspace.network]
enabled = true
proxy_url = "http://127.0.0.1:3128"
# SOCKS5 listener (enabled by default).
enable_socks5 = true
socks_url = "http://127.0.0.1:8081"
enable_socks5_udp = true
# When `enabled` is false, the proxy no-ops and does not bind listeners.
# When true, respect HTTP(S)_PROXY/ALL_PROXY for upstream requests (HTTP(S) proxies only),
# including CONNECT tunnels in full mode.
allow_upstream_proxy = true
# By default, non-loopback binds are clamped to loopback for safety.
# If you want to expose these listeners beyond localhost, you must opt in explicitly.
dangerously_allow_non_loopback_proxy = false
mode = "full" # default when unset; use "limited" for read-only mode
# HTTPS MITM is enabled automatically when `mode = "limited"` or when MITM hooks are configured.
# The CA private key remains in proxy memory. When MITM is active, spawned commands receive CA
# bundle env vars pointing at immutable public files under $CODEX_HOME/proxy/ so common HTTPS
# clients trust the managed CA.

# If false, local/private networking is rejected. Explicit allowlisting of local IP literals
# (or `localhost`) is required to permit them.
# Hostnames that resolve to local/private IPs are still blocked even if allowlisted.
allow_local_binding = false

# DANGEROUS (macOS-only): bypasses unix socket allowlisting and permits any
# absolute socket path from `x-unix-socket`.
dangerously_allow_all_unix_sockets = false

# Hosts must match the allowlist (unless denied).
# Use exact hosts or scoped wildcards like `*.openai.com` or `**.openai.com`.
# The global `*` wildcard is rejected.
# If no domain entries are marked `allow`, the proxy blocks requests until an allowlist is configured.
[permissions.workspace.network.domains]
"*.openai.com" = "allow"
"localhost" = "allow"
"127.0.0.1" = "allow"
"::1" = "allow"
"evil.example" = "deny"

# MITM hooks match HTTPS requests after CONNECT is terminated.
[permissions.workspace.network.mitm.hooks.github_write]
host = "api.github.com"
methods = ["POST", "PUT"]
path_prefixes = ["/repos/openai/"]
action = ["strip_auth"]

# Named actions can be shared across hooks and overridden by higher-precedence config layers.
[permissions.workspace.network.mitm.actions.strip_auth]
strip_request_headers = ["authorization"]

# macOS-only: allows proxying to a unix socket when request includes `x-unix-socket: /path`.
[permissions.workspace.network.unix_sockets]
"/tmp/example.sock" = "allow"

2) Run the proxy

cargo run -p codex-network-proxy --

3) Point a client at it

For HTTP(S) traffic:

export HTTP_PROXY="http://127.0.0.1:3128"
export HTTPS_PROXY="http://127.0.0.1:3128"
export WS_PROXY="http://127.0.0.1:3128"
export WSS_PROXY="http://127.0.0.1:3128"

For SOCKS5 traffic (when enable_socks5 = true):

export ALL_PROXY="socks5h://127.0.0.1:8081"

4) Understand blocks / debugging

When a request is blocked, the proxy responds with 403 and includes:

  • x-proxy-error: one of:
    • blocked-by-allowlist
    • blocked-by-denylist
    • blocked-by-method-policy
    • blocked-by-policy

In "limited" mode, only GET, HEAD, and OPTIONS are allowed. HTTPS CONNECT requests and HTTPS SOCKS5 TCP targets on :443 require MITM to enforce limited-mode method policy; otherwise they are blocked. SOCKS5 UDP and non-HTTPS SOCKS5 TCP remain blocked in limited mode.

Websocket clients typically tunnel wss:// through HTTPS CONNECT; those CONNECT targets still go through the same host allowlist/denylist checks.

Library API

codex-network-proxy can be embedded as a library with a thin API:

use codex_network_proxy::{NetworkProxy, NetworkDecision, NetworkPolicyRequest};

let proxy = NetworkProxy::builder()
    .http_addr("127.0.0.1:8080".parse()?)
    .policy_decider(|request: NetworkPolicyRequest| async move {
        // Example: auto-allow when exec policy already approved a command prefix.
        if let Some(command) = request.command.as_deref() {
            if command.starts_with("curl ") {
                return NetworkDecision::Allow;
            }
        }
        NetworkDecision::Deny {
            reason: "policy_denied".to_string(),
        }
    })
    .build()
    .await?;

let handle = proxy.run().await?;
handle.shutdown().await?;

When unix socket proxying is enabled (unix_sockets or dangerously_allow_all_unix_sockets), proxy bind overrides are still clamped to loopback to avoid turning the proxy into a remote bridge to local daemons.

Policy hook (exec-policy mapping)

The proxy exposes a policy hook (NetworkPolicyDecider) that can override allowlist-only blocks. It receives command and exec_policy_hint fields when supplied by the embedding app. This lets core map exec approvals to network access, e.g. if a user already approved curl * for a session, the decider can auto-allow network requests originating from that command.

Important: Explicit deny rules still win. The decider only gets a chance to override not_allowed (allowlist misses), not denied or not_allowed_local.

OTEL Audit Events (embedded/managed)

When codex-network-proxy is embedded in managed Codex runtime, policy decisions emit structured OTEL-compatible events with target=codex_otel.network_proxy.

Event name:

  • codex.network_proxy.policy_decision
    • emitted for each policy decision (domain and non_domain).
    • network.policy.scope = "domain" for host-policy evaluations (evaluate_host_policy).
    • network.policy.scope = "non_domain" for mode-guard/proxy-state checks (including unix-socket guard paths and unix-socket allow decisions).

Common fields:

  • event.name
  • event.timestamp (RFC3339 UTC, millisecond precision)
  • optional metadata:
    • conversation.id
    • app.version
    • user.account_id
  • policy/network:
    • network.policy.scope (domain or non_domain)
    • network.policy.decision (allow, deny, or ask)
    • network.policy.source (baseline_policy, mode_guard, proxy_state, decider)
    • network.policy.reason
    • network.transport.protocol
    • server.address
    • server.port
    • http.request.method (defaults to "none" when absent)
    • client.address (defaults to "unknown" when absent)
    • network.policy.override (true only when decider-allow overrides baseline not_allowed)

Unix-socket block-path audits use sentinel endpoint values:

  • server.address = "unix-socket"
  • server.port = 0

Audit events intentionally avoid logging full URL/path/query data.

Platform notes

  • Unix socket proxying via the x-unix-socket header is macOS-only; other platforms will reject unix socket requests.
  • HTTPS tunneling uses rustls via Rama's rama-tls-rustls; this avoids BoringSSL/OpenSSL symbol collisions in mixed TLS dependency graphs.

Security notes (important)

This section documents the protections implemented by codex-network-proxy, and the boundaries of what it can reasonably guarantee.

  • Allowlist-first policy: if domains has no allow entries, requests are blocked until an allowlist is configured.

  • Domain patterns: exact hosts are supported, *.example.com matches subdomains only, and **.example.com matches the apex plus subdomains; the global * wildcard is only accepted when explicitly enabled for allowlist compilation and is otherwise rejected.

  • Deny wins: domains entries marked deny always override the allowlist.

  • Local/private network protection: when allow_local_binding = false, the proxy blocks loopback and common private/link-local ranges. Explicit allowlisting of local IP literals (or localhost) is required to permit them; hostnames that resolve to local/private IPs are still blocked even if allowlisted (best-effort DNS lookup).

  • Limited mode enforcement:

    • only GET, HEAD, and OPTIONS are allowed
    • HTTPS CONNECT requests and HTTPS SOCKS5 TCP targets on :443 require MITM so the proxy can enforce limited-mode method policy; SOCKS5 UDP and non-HTTPS SOCKS5 TCP remain blocked
  • Listener safety defaults:

    • the HTTP proxy listener clamps non-loopback binds unless explicitly enabled via dangerously_allow_non_loopback_proxy
  • when unix socket proxying is enabled, all proxy listeners are forced to loopback to avoid turning the proxy into a remote bridge into local daemons.

  • dangerously_allow_all_unix_sockets = true bypasses the unix socket allowlist entirely (still macOS-only and absolute-path-only). Use only in tightly controlled environments.

  • enabled is enforced at runtime; when false the proxy no-ops and does not bind listeners. Limitations:

  • DNS rebinding is hard to fully prevent without pinning the resolved IP(s) all the way down to the transport layer. If your threat model includes hostile DNS, enforce network egress at a lower layer too (e.g., firewall / VPC / corporate proxy policies).