2026-01-23 20:47:09 -05:00
|
|
|
# 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`)
|
2026-02-10 15:13:52 -08:00
|
|
|
- a SOCKS5 proxy (default `127.0.0.1:8081`, enabled by default)
|
2026-01-23 20:47:09 -05:00
|
|
|
|
|
|
|
|
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).
|
|
|
|
|
|
config: add initial support for the new permission profile config language in config.toml (#13434)
## Why
`SandboxPolicy` currently mixes together three separate concerns:
- parsing layered config from `config.toml`
- representing filesystem sandbox state
- carrying basic network policy alongside filesystem choices
That makes the existing config awkward to extend and blocks the new TOML
proposal where `[permissions]` becomes a table of named permission
profiles selected by `default_permissions`. (The idea is that if
`default_permissions` is not specified, we assume the user is opting
into the "traditional" way to configure the sandbox.)
This PR adds the config-side plumbing for those profiles while still
projecting back to the legacy `SandboxPolicy` shape that the current
macOS and Linux sandbox backends consume.
It also tightens the filesystem profile model so scoped entries only
exist for `:project_roots`, and so nested keys must stay within a
project root instead of using `.` or `..` traversal.
This drops support for the short-lived `[permissions.network]` in
`config.toml` because now that would be interpreted as a profile named
`network` within `[permissions]`.
## What Changed
- added `PermissionsToml`, `PermissionProfileToml`,
`FilesystemPermissionsToml`, and `FilesystemPermissionToml` so config
can parse named profiles under `[permissions.<profile>.filesystem]`
- added top-level `default_permissions` selection, validation for
missing or unknown profiles, and compilation from a named profile into
split `FileSystemSandboxPolicy` and `NetworkSandboxPolicy` values
- taught config loading to choose between the legacy `sandbox_mode` path
and the profile-based path without breaking legacy users
- introduced `codex-protocol::permissions` for the split filesystem and
network sandbox types, and stored those alongside the legacy projected
`sandbox_policy` in runtime `Permissions`
- modeled `FileSystemSpecialPath` so only `ProjectRoots` can carry a
nested `subpath`, matching the intended config syntax instead of
allowing invalid states for other special paths
- restricted scoped filesystem maps to `:project_roots`, with validation
that nested entries are non-empty descendant paths and cannot use `.` or
`..` to escape the project root
- kept existing runtime consumers working by projecting
`FileSystemSandboxPolicy` back into `SandboxPolicy`, with an explicit
error for profiles that request writes outside the workspace root
- loaded proxy settings from top-level `[network]`
- regenerated `core/config.schema.json`
## Verification
- added config coverage for profile deserialization,
`default_permissions` selection, top-level `[network]` loading, network
enablement, rejection of writes outside the workspace root, rejection of
nested entries for non-`:project_roots` special paths, and rejection of
parent-directory traversal in `:project_roots` maps
- added protocol coverage for the legacy bridge rejecting non-workspace
writes
## Docs
- update the Codex config docs on developers.openai.com/codex to
document named `[permissions.<profile>]` entries, `default_permissions`,
scoped `:project_roots` syntax, the descendant-path restriction for
nested `:project_roots` entries, and top-level `[network]` proxy
configuration
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/13434).
* #13453
* #13452
* #13451
* #13449
* #13448
* #13445
* #13440
* #13439
* __->__ #13434
2026-03-06 15:39:13 -08:00
|
|
|
Network settings live under the selected permissions profile. Example config:
|
2026-01-23 20:47:09 -05:00
|
|
|
|
|
|
|
|
```toml
|
config: add initial support for the new permission profile config language in config.toml (#13434)
## Why
`SandboxPolicy` currently mixes together three separate concerns:
- parsing layered config from `config.toml`
- representing filesystem sandbox state
- carrying basic network policy alongside filesystem choices
That makes the existing config awkward to extend and blocks the new TOML
proposal where `[permissions]` becomes a table of named permission
profiles selected by `default_permissions`. (The idea is that if
`default_permissions` is not specified, we assume the user is opting
into the "traditional" way to configure the sandbox.)
This PR adds the config-side plumbing for those profiles while still
projecting back to the legacy `SandboxPolicy` shape that the current
macOS and Linux sandbox backends consume.
It also tightens the filesystem profile model so scoped entries only
exist for `:project_roots`, and so nested keys must stay within a
project root instead of using `.` or `..` traversal.
This drops support for the short-lived `[permissions.network]` in
`config.toml` because now that would be interpreted as a profile named
`network` within `[permissions]`.
## What Changed
- added `PermissionsToml`, `PermissionProfileToml`,
`FilesystemPermissionsToml`, and `FilesystemPermissionToml` so config
can parse named profiles under `[permissions.<profile>.filesystem]`
- added top-level `default_permissions` selection, validation for
missing or unknown profiles, and compilation from a named profile into
split `FileSystemSandboxPolicy` and `NetworkSandboxPolicy` values
- taught config loading to choose between the legacy `sandbox_mode` path
and the profile-based path without breaking legacy users
- introduced `codex-protocol::permissions` for the split filesystem and
network sandbox types, and stored those alongside the legacy projected
`sandbox_policy` in runtime `Permissions`
- modeled `FileSystemSpecialPath` so only `ProjectRoots` can carry a
nested `subpath`, matching the intended config syntax instead of
allowing invalid states for other special paths
- restricted scoped filesystem maps to `:project_roots`, with validation
that nested entries are non-empty descendant paths and cannot use `.` or
`..` to escape the project root
- kept existing runtime consumers working by projecting
`FileSystemSandboxPolicy` back into `SandboxPolicy`, with an explicit
error for profiles that request writes outside the workspace root
- loaded proxy settings from top-level `[network]`
- regenerated `core/config.schema.json`
## Verification
- added config coverage for profile deserialization,
`default_permissions` selection, top-level `[network]` loading, network
enablement, rejection of writes outside the workspace root, rejection of
nested entries for non-`:project_roots` special paths, and rejection of
parent-directory traversal in `:project_roots` maps
- added protocol coverage for the legacy bridge rejecting non-workspace
writes
## Docs
- update the Codex config docs on developers.openai.com/codex to
document named `[permissions.<profile>]` entries, `default_permissions`,
scoped `:project_roots` syntax, the descendant-path restriction for
nested `:project_roots` entries, and top-level `[network]` proxy
configuration
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/13434).
* #13453
* #13452
* #13451
* #13449
* #13448
* #13445
* #13440
* #13439
* __->__ #13434
2026-03-06 15:39:13 -08:00
|
|
|
default_permissions = "workspace"
|
|
|
|
|
|
|
|
|
|
[permissions.workspace.network]
|
2026-01-23 20:47:09 -05:00
|
|
|
enabled = true
|
|
|
|
|
proxy_url = "http://127.0.0.1:3128"
|
2026-02-10 15:13:52 -08:00
|
|
|
# SOCKS5 listener (enabled by default).
|
|
|
|
|
enable_socks5 = true
|
2026-01-27 10:09:39 -08:00
|
|
|
socks_url = "http://127.0.0.1:8081"
|
2026-02-10 15:13:52 -08:00
|
|
|
enable_socks5_udp = true
|
2026-01-23 20:47:09 -05:00
|
|
|
# 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.
|
2026-02-10 15:13:52 -08:00
|
|
|
allow_upstream_proxy = true
|
2026-01-23 20:47:09 -05:00
|
|
|
# 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
|
2026-01-27 10:09:39 -08:00
|
|
|
mode = "full" # default when unset; use "limited" for read-only mode
|
2026-02-24 10:15:15 -08:00
|
|
|
# When true, HTTPS CONNECT can be terminated so limited-mode method policy still applies.
|
|
|
|
|
mitm = false
|
|
|
|
|
# CA cert/key are managed internally under $CODEX_HOME/proxy/ (ca.pem + ca.key).
|
2026-01-23 20:47:09 -05:00
|
|
|
|
|
|
|
|
# Hosts must match the allowlist (unless denied).
|
2026-03-06 13:06:24 -08:00
|
|
|
# Use exact hosts or scoped wildcards like `*.openai.com` or `**.openai.com`.
|
|
|
|
|
# The global `*` wildcard is rejected.
|
2026-01-23 20:47:09 -05:00
|
|
|
# If `allowed_domains` is empty, the proxy blocks requests until an allowlist is configured.
|
2026-02-10 15:13:52 -08:00
|
|
|
allowed_domains = ["*.openai.com", "localhost", "127.0.0.1", "::1"]
|
2026-01-23 20:47:09 -05:00
|
|
|
denied_domains = ["evil.example"]
|
|
|
|
|
|
|
|
|
|
# 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.
|
fix: support managed network allowlist controls (#12752)
## Summary
- treat `requirements.toml` `allowed_domains` and `denied_domains` as
managed network baselines for the proxy
- in restricted modes by default, build the effective runtime policy
from the managed baseline plus user-configured allowlist and denylist
entries, so common hosts can be pre-approved without blocking later user
expansion
- add `experimental_network.managed_allowed_domains_only = true` to pin
the effective allowlist to managed entries, ignore user allowlist
additions, and hard-deny non-managed domains without prompting
- apply `managed_allowed_domains_only` anywhere managed network
enforcement is active, including full access, while continuing to
respect denied domains from all sources
- add regression coverage for merged-baseline behavior, managed-only
behavior, and full-access managed-only enforcement
## Behavior
Assuming `requirements.toml` defines both
`experimental_network.allowed_domains` and
`experimental_network.denied_domains`.
### Default mode
- By default, the effective allowlist is
`experimental_network.allowed_domains` plus user or persisted allowlist
additions.
- By default, the effective denylist is
`experimental_network.denied_domains` plus user or persisted denylist
additions.
- Allowlist misses can go through the network approval flow.
- Explicit denylist hits and local or private-network blocks are still
hard-denied.
- When `experimental_network.managed_allowed_domains_only = true`, only
managed `allowed_domains` are respected, user allowlist additions are
ignored, and non-managed domains are hard-denied without prompting.
- Denied domains continue to be respected from all sources.
### Full access
- With managed requirements present, the effective allowlist is pinned
to `experimental_network.allowed_domains`.
- With managed requirements present, the effective denylist is pinned to
`experimental_network.denied_domains`.
- There is no allowlist-miss approval path in full access.
- Explicit denylist hits are hard-denied.
- `experimental_network.managed_allowed_domains_only = true` now also
applies in full access, so managed-only behavior remains in effect
anywhere managed network enforcement is active.
2026-03-06 17:52:54 -08:00
|
|
|
allow_local_binding = false
|
2026-01-23 20:47:09 -05:00
|
|
|
|
|
|
|
|
# macOS-only: allows proxying to a unix socket when request includes `x-unix-socket: /path`.
|
|
|
|
|
allow_unix_sockets = ["/tmp/example.sock"]
|
2026-02-20 10:56:57 -08:00
|
|
|
# DANGEROUS (macOS-only): bypasses unix socket allowlisting and permits any
|
|
|
|
|
# absolute socket path from `x-unix-socket`.
|
|
|
|
|
dangerously_allow_all_unix_sockets = false
|
2026-01-23 20:47:09 -05:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 2) Run the proxy
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
cargo run -p codex-network-proxy --
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 3) Point a client at it
|
|
|
|
|
|
|
|
|
|
For HTTP(S) traffic:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
export HTTP_PROXY="http://127.0.0.1:3128"
|
|
|
|
|
export HTTPS_PROXY="http://127.0.0.1:3128"
|
2026-02-17 13:49:43 -08:00
|
|
|
export WS_PROXY="http://127.0.0.1:3128"
|
|
|
|
|
export WSS_PROXY="http://127.0.0.1:3128"
|
2026-01-23 20:47:09 -05:00
|
|
|
```
|
|
|
|
|
|
2026-01-27 10:09:39 -08:00
|
|
|
For SOCKS5 traffic (when `enable_socks5 = true`):
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
export ALL_PROXY="socks5h://127.0.0.1:8081"
|
|
|
|
|
```
|
|
|
|
|
|
2026-01-23 20:47:09 -05:00
|
|
|
### 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`
|
|
|
|
|
|
2026-02-24 10:15:15 -08:00
|
|
|
In "limited" mode, only `GET`, `HEAD`, and `OPTIONS` are allowed. HTTPS `CONNECT` requests require
|
|
|
|
|
MITM to enforce limited-mode method policy; otherwise they are blocked. SOCKS5 remains blocked in
|
|
|
|
|
limited mode.
|
2026-01-23 20:47:09 -05:00
|
|
|
|
2026-02-17 13:49:43 -08:00
|
|
|
Websocket clients typically tunnel `wss://` through HTTPS `CONNECT`; those CONNECT targets still go
|
|
|
|
|
through the same host allowlist/denylist checks.
|
|
|
|
|
|
2026-01-23 20:47:09 -05:00
|
|
|
## Library API
|
|
|
|
|
|
|
|
|
|
`codex-network-proxy` can be embedded as a library with a thin API:
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
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?;
|
|
|
|
|
```
|
|
|
|
|
|
2026-02-20 10:56:57 -08:00
|
|
|
When unix socket proxying is enabled (`allow_unix_sockets` or
|
2026-03-05 22:03:16 -08:00
|
|
|
`dangerously_allow_all_unix_sockets`), proxy bind overrides are still clamped to loopback to
|
2026-02-20 10:56:57 -08:00
|
|
|
avoid turning the proxy into a remote bridge to local daemons.
|
2026-01-23 20:47:09 -05:00
|
|
|
|
|
|
|
|
### 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`.
|
|
|
|
|
|
feat(network-proxy): add embedded OTEL policy audit logging (#12046)
**PR Summary**
This PR adds embedded-only OTEL policy audit logging for
`codex-network-proxy` and threads audit metadata from `codex-core` into
managed proxy startup.
### What changed
- Added structured audit event emission in `network_policy.rs` with
target `codex_otel.network_proxy`.
- Emitted:
- `codex.network_proxy.domain_policy_decision` once per domain-policy
evaluation.
- `codex.network_proxy.block_decision` for non-domain denies.
- Added required policy/network fields, RFC3339 UTC millisecond
`event.timestamp`, and fallback defaults (`http.request.method="none"`,
`client.address="unknown"`).
- Added non-domain deny audit emission in HTTP/SOCKS handlers for
mode-guard and proxy-state denies, including unix-socket deny paths.
- Added `REASON_UNIX_SOCKET_UNSUPPORTED` and used it for unsupported
unix-socket auditing.
- Added `NetworkProxyAuditMetadata` to runtime/state, re-exported from
`lib.rs` and `state.rs`.
- Added `start_proxy_with_audit_metadata(...)` in core config, with
`start_proxy()` delegating to default metadata.
- Wired metadata construction in `codex.rs` from session/auth context,
including originator sanitization for OTEL-safe tagging.
- Updated `network-proxy/README.md` with embedded-mode audit schema and
behavior notes.
- Refactored HTTP block-audit emission to a small local helper to reduce
duplication.
- Preserved existing unix-socket proxy-disabled host/path behavior for
responses and blocked history while using an audit-only endpoint
override (`server.address="unix-socket"`, `server.port=0`).
### Explicit exclusions
- No standalone proxy OTEL startup work.
- No `main.rs` binary wiring.
- No `standalone_otel.rs`.
- No standalone docs/tests.
### Tests
- Extended `network_policy.rs` tests for event mapping, metadata
propagation, fallbacks, timestamp format, and target prefix.
- Extended HTTP tests to assert unix-socket deny block audit events.
- Extended SOCKS tests to cover deny emission from handler deny
branches.
- Added/updated core tests to verify audit metadata threading into
managed proxy state.
### Validation run
- `just fmt`
- `cargo test -p codex-network-proxy` ✅
- `cargo test -p codex-core` ran with one unrelated flaky timeout
(`shell_snapshot::tests::snapshot_shell_does_not_inherit_stdin`), and
the test passed when rerun directly ✅
---------
Co-authored-by: viyatb-oai <viyatb@openai.com>
2026-02-25 11:46:37 -05:00
|
|
|
## 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.
|
|
|
|
|
|
2026-01-23 20:47:09 -05:00
|
|
|
## Platform notes
|
|
|
|
|
|
|
|
|
|
- Unix socket proxying via the `x-unix-socket` header is **macOS-only**; other platforms will
|
|
|
|
|
reject unix socket requests.
|
feat: enable premessage-deflate for websockets (#10966)
note:
unfortunately, tokio-tungstenite / tungstenite upgrade triggers some
problems with linker of rama-tls-boring with openssl:
```
error: linking with `/Users/apanasenko/Library/Caches/cargo-zigbuild/0.20.1/zigcc-x86_64-unknown-linux-musl-ff6a.sh` failed: exit status: 1
|
= note: "/Users/apanasenko/Library/Caches/cargo-zigbuild/0.20.1/zigcc-x86_64-unknown-linux-musl-ff6a.sh" "-m64" "<sysroot>/lib/rustlib/x86_64-unknown-linux-musl/lib/self-contained/rcrt1.o" "<sysroot>/lib/rustlib/x86_64-unknown-linux-musl/lib/self-contained/crti.o" "<sysroot>/lib/rustlib/x86_64-unknown-linux-musl/lib/self-contained/crtbeginS.o" "<1 object files omitted>" "-Wl,--as-needed" "-Wl,-Bstatic" "/var/folders/kt/52y_g75x3ng8ktvk3rfwm6400000gp/T/rustcyGQdYm/{liblzma_sys-662a82316f96ec30,libbzip2_sys-bf78a2d58d5cbce6,liblibsqlite3_sys-6c004987fd67a36a,libtree_sitter_bash-220b99a97d331ab7,libtree_sitter-858f0a1dbfea58bd,libzstd_sys-6eb237deec748c5b,libring-2a87376483bf916f,libopenssl_sys-7c189e68b37fe2bb,liblibz_sys-4344eef4345520b1,librama_boring_sys-0414e98115015ee0}.rlib" "-lc++" "-lc++abi" "-lunwind" "-lc" "<sysroot>/lib/rustlib/x86_64-unknown-linux-musl/lib/libcompiler_builtins-*.rlib" "-L" "/var/folders/kt/52y_g75x3ng8ktvk3rfwm6400000gp/T/rustcyGQdYm/raw-dylibs" "-Wl,-Bdynamic" "-Wl,--eh-frame-hdr" "-Wl,-z,noexecstack" "-nostartfiles" "-L" "/Users/apanasenko/code/codex/codex-rs/target/x86_64-unknown-linux-musl/release/build/libz-sys-ff5ea50d88c28ffb/out/lib" "-L" "/Users/apanasenko/code/codex/codex-rs/target/x86_64-unknown-linux-musl/release/build/ring-bdec3dddc19f5a5e/out" "-L" "/Users/apanasenko/code/codex/codex-rs/target/x86_64-unknown-linux-musl/release/build/openssl-sys-96e0870de3ca22bc/out/openssl-build/install/lib" "-L" "/Users/apanasenko/code/codex/codex-rs/target/x86_64-unknown-linux-musl/release/build/zstd-sys-0cc37a5da1481740/out" "-L" "/Users/apanasenko/code/codex/codex-rs/target/x86_64-unknown-linux-musl/release/build/tree-sitter-72d2418073317c0f/out" "-L" "/Users/apanasenko/code/codex/codex-rs/target/x86_64-unknown-linux-musl/release/build/tree-sitter-bash-bfd293a9f333ce6a/out" "-L" "/Users/apanasenko/code/codex/codex-rs/target/x86_64-unknown-linux-musl/release/build/libsqlite3-sys-b78b2cfb81a330fc/out" "-L" "/Users/apanasenko/code/codex/codex-rs/target/x86_64-unknown-linux-musl/release/build/bzip2-sys-69a145cc859ef275/out/lib" "-L" "/Users/apanasenko/code/codex/codex-rs/target/x86_64-unknown-linux-musl/release/build/lzma-sys-07e92d0b6baa6fd4/out" "-L" "/Users/apanasenko/code/codex/codex-rs/target/x86_64-unknown-linux-musl/release/build/rama-boring-sys-0bc2dfbf669addc4/out/build/crypto/" "-L" "/Users/apanasenko/code/codex/codex-rs/target/x86_64-unknown-linux-musl/release/build/rama-boring-sys-0bc2dfbf669addc4/out/build/ssl/" "-L" "/Users/apanasenko/code/codex/codex-rs/target/x86_64-unknown-linux-musl/release/build/rama-boring-sys-0bc2dfbf669addc4/out/build/" "-L" "/Users/apanasenko/code/codex/codex-rs/target/x86_64-unknown-linux-musl/release/build/rama-boring-sys-0bc2dfbf669addc4/out/build" "-L" "<sysroot>/lib/rustlib/x86_64-unknown-linux-musl/lib/self-contained" "-L" "<sysroot>/lib/rustlib/x86_64-unknown-linux-musl/lib" "-o" "/Users/apanasenko/code/codex/codex-rs/target/x86_64-unknown-linux-musl/release/deps/codex_network_proxy-d08268b863517761" "-Wl,--gc-sections" "-static-pie" "-Wl,-z,relro,-z,now" "-Wl,-O1" "-Wl,--strip-all" "-nodefaultlibs" "<sysroot>/lib/rustlib/x86_64-unknown-linux-musl/lib/self-contained/crtendS.o" "<sysroot>/lib/rustlib/x86_64-unknown-linux-musl/lib/self-contained/crtn.o"
= note: some arguments are omitted. use `--verbose` to show all linker arguments
= note: warning: ignoring deprecated linker optimization setting '1'
warning: unable to open library directory '/Users/apanasenko/code/codex/codex-rs/target/x86_64-unknown-linux-musl/release/build/rama-boring-sys-0bc2dfbf669addc4/out/build/crypto/': FileNotFound
ld.lld: error: duplicate symbol: SSL_export_keying_material
>>> defined at ssl_lib.c:3816 (ssl/ssl_lib.c:3816)
>>> libssl-lib-ssl_lib.o:(SSL_export_keying_material) in archive /var/folders/kt/52y_g75x3ng8ktvk3rfwm6400000gp/T/rustcyGQdYm/libopenssl_sys-7c189e68b37fe2bb.rlib
>>> defined at t1_enc.cc:205 (/Users/apanasenko/code/codex/codex-rs/target/x86_64-unknown-linux-musl/release/build/rama-boring-sys-0bc2dfbf669addc4/out/boringssl/ssl/t1_enc.cc:205)
>>> t1_enc.cc.o:(.text.SSL_export_keying_material+0x0) in archive /var/folders/kt/52y_g75x3ng8ktvk3rfwm6400000gp/T/rustcyGQdYm/librama_boring_sys-0414e98115015ee0.rlib
ld.lld: error: duplicate symbol: d2i_ASN1_TIME
>>> defined at a_time.c:27 (crypto/asn1/a_time.c:27)
>>> libcrypto-lib-a_time.o:(d2i_ASN1_TIME) in archive /var/folders/kt/52y_g75x3ng8ktvk3rfwm6400000gp/T/rustcyGQdYm/libopenssl_sys-7c189e68b37fe2bb.rlib
>>> defined at a_time.cc:34 (/Users/apanasenko/code/codex/codex-rs/target/x86_64-unknown-linux-musl/release/build/rama-boring-sys-0bc2dfbf669addc4/out/boringssl/crypto/asn1/a_time.cc:34)
>>> a_time.cc.o:(.text.d2i_ASN1_TIME+0x0) in archive /var/folders/kt/52y_g75x3ng8ktvk3rfwm6400000gp/T/rustcyGQdYm/librama_boring_sys-0414e98115015ee0.rlib
```
that force me to migrate away from rama-tls-boring to rama-tls-rustls
and pin `ring` for rustls.
2026-02-07 17:59:34 -08:00
|
|
|
- HTTPS tunneling uses rustls via Rama's `rama-tls-rustls`; this avoids BoringSSL/OpenSSL symbol
|
|
|
|
|
collisions in mixed TLS dependency graphs.
|
2026-01-23 20:47:09 -05:00
|
|
|
|
|
|
|
|
## 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 `allowed_domains` is empty, requests are blocked until an allowlist is configured.
|
2026-03-06 13:06:24 -08:00
|
|
|
- Domain patterns: exact hosts plus scoped wildcards (`*.example.com`, `**.example.com`) are supported; the global `*` wildcard is rejected.
|
2026-01-23 20:47:09 -05:00
|
|
|
- Deny wins: entries in `denied_domains` 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` remains a tunnel; limited-mode method enforcement does not apply to HTTPS
|
|
|
|
|
- Listener safety defaults:
|
2026-03-05 22:03:16 -08:00
|
|
|
- the HTTP proxy listener clamps non-loopback binds unless explicitly enabled via
|
2026-01-23 20:47:09 -05:00
|
|
|
`dangerously_allow_non_loopback_proxy`
|
2026-03-05 22:03:16 -08:00
|
|
|
- when unix socket proxying is enabled, all proxy listeners are forced to loopback to avoid turning the
|
2026-01-23 20:47:09 -05:00
|
|
|
proxy into a remote bridge into local daemons.
|
2026-02-20 10:56:57 -08:00
|
|
|
- `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.
|
2026-01-23 20:47:09 -05:00
|
|
|
- `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).
|