mibyan egress / iron-proxy) from a contributor / plugin author’s perspective. End-user setup + usage docs live at Egress proxy.
The threat model and high-level design are summarised on the user page; this page is about how it’s wired, where the security-relevant code lives, and what invariants you have to preserve if you touch it.
Module layout
Lifecycle
Security invariants
These are the load-bearing properties. If you touch the module, you must preserve them. Where there’s a regression test, it’s named.Filesystem perms
All write paths use
os.open(O_WRONLY | O_CREAT | O_NOFOLLOW, 0o600) + os.fstat().st_uid check. shutil.copy2 + os.chmod is forbidden because it leaks a default-umask window.
Subprocess env minimisation
_build_proxy_subprocess_env MUST NOT use os.environ.copy(). The allowlist is _PROXY_SUBPROCESS_ENV_ALLOWLIST (PATH, HOME, locale, etc.) plus the env names referenced by load_mappings(). Everything else stays on the host.
Regression: test_subprocess_env_strips_unrelated_secrets, test_subprocess_env_strips_proxy_recursion_vars, test_subprocess_env_keeps_infrastructure_vars.
Bind policy
_default_http_listen returns a single-element list: on Linux the docker bridge gateway IP (containers reach the proxy via host.docker.internal:host-gateway, which resolves to the bridge gateway — a loopback bind is unreachable from inside containers there); on macOS/Windows Docker Desktop, loopback (VPNkit routes host.docker.internal to the host). Linux without a detectable docker0 bridge falls back to loopback with a warning. Never 0.0.0.0, never :PORT (INADDR_ANY).
_detect_docker_bridge_ip validates via ipaddress.IPv4Address and rejects is_unspecified / is_loopback / is_multicast / is_reserved / is_link_local / is_global. A hostile ip shim on PATH cannot inject 0.0.0.0.
v0.39 schema constraint and listener roles (verified live against the binary): the binary’s config.Proxy struct has only singular listener fields — there is no http_listens (plural) list. tunnel_listen is the CONNECT + MITM listener (what HTTPS_PROXY traffic hits); http_listen only handles absolute-form plain-HTTP forwards (a CONNECT sent to it is relayed upstream as a regular request and 400s). build_proxy_config therefore binds tunnel_listen on tunnel_port and http_listen on tunnel_port + 1, both on the platform bind host. The Docker backend sets HTTPS_PROXY to tunnel_port and HTTP_PROXY to tunnel_port + 1.
The liveness probes (start_proxy poll loop, get_status) read the configured bind host via _read_http_listen_from_config() and probe THAT host — a hardcoded loopback probe would report a healthy bridge-bound daemon as dead.
Regression: test_default_bind_is_loopback_not_zero_zero (asserts no INADDR_ANY AND that http_listens is NOT in the rendered yaml), test_default_bind_uses_docker_bridge_on_linux, test_default_bind_falls_back_to_loopback_without_bridge, test_default_bind_is_loopback_on_macos, test_detect_docker_bridge_ip_rejects_dangerous (parametrized over 8 attack inputs).
Metrics port collision
metrics.listen defaults to :9090 in iron-proxy v0.39 — the SAME port as Mibyan’s default tunnel_port: 9090. build_proxy_config MUST explicitly pin metrics.listen: 127.0.0.1:0 so the metrics binding gets an ephemeral loopback port that can never collide with the proxy listener regardless of operator-chosen tunnel_port.
Regression: test_metrics_listener_pinned_to_loopback_ephemeral.
Default deny CIDRs
_DEFAULT_UPSTREAM_DENY_CIDRS covers loopback (v4 + v6), link-local (incl. IMDS at 169.254.169.254 and the IPv4-mapped-v6 form), RFC1918, IPv6 ULA, CGNAT, and the RFC2544 benchmark range. build_proxy_config(..., upstream_deny_cidrs=None) MUST emit the default; only an explicit empty list opts out.
Regression: test_default_deny_cidrs_present_when_unspecified, test_default_deny_includes_ipv4_mapped_v6.
Audit log fail-loud
ensure_audit_log raises RuntimeError on any OSError. On the pinned v0.39 the daemon never writes this file (no log.audit_path field), so cmd_setup treats the failure as a WARNING (the file is non-load-bearing until the version bump) and qualifies the success line as “reserved”. When the pin moves to a version with log.audit_path, revisit: the pre-create becomes load-bearing for the 0o600-from-first-byte guarantee and the wizard should fail loud again.
v0.39 schema constraint: log.audit_path is NOT a field in iron-proxy v0.39’s config.Log struct, so build_proxy_config accepts the audit_log kwarg but does NOT emit it into the rendered yaml. Per-request records on v0.39 land in iron-proxy.log alongside daemon-level events. The audit.log file is still pre-created at 0o600 with O_NOFOLLOW so the privacy contract holds when the pinned version is bumped to one that supports the separate stream.
Regression: test_ensure_audit_log_raises_on_immutable_parent, test_audit_log_kwarg_does_not_inject_audit_path_v039.
Bitwarden mode fail-loud
Whencredential_source: bitwarden AND proxy.allow_env_fallback: false (default):
- Missing access token env var ->
cmd_startrefuses. - Missing
project_id->cmd_startrefuses. bws secret listreturns no values for one or more mapped providers ->_build_proxy_subprocess_envraises.
test_cmd_start_refuses_when_bitwarden_token_missing (CLI layer); strict-mode assertions in _build_proxy_subprocess_env (daemon layer).
docker_env collision detection
Whenenforce_on_docker: true, docker_env overrides on any of the egress-controlling vars (HTTPS_PROXY, SSL_CERT_FILE, NODE_EXTRA_CA_CERTS, etc.) OR any mapped real_env_name (OPENROUTER_API_KEY, etc.) raises RuntimeError BEFORE the container starts.
Regression: test_docker_env_collision_with_proxy_raises_when_enforce.
PID recycling defense
_pid_alive MUST consult either the in-process _proxy_nonce (same-process case) OR the on-disk iron-proxy.nonce (cross-CLI case) before trusting an argv[0] basename match. stop_proxy MUST re-check /proc/<pid>/stat starttime before SIGKILL and suppress the signal on starttime drift.
Regression: test_stop_proxy_suppresses_sigkill_on_pid_recycle, test_pid_proc_starttime_parses_comm_with_parens, test_persisted_nonce_roundtrip.
Token preservation on re-setup
merge_mappings(existing, discovered, rotate=False) MUST return prior tokens for providers that overlap. Re-running mibyan egress setup cannot silently 401 running sandboxes. --rotate-tokens is the explicit opt-in.
Regression: test_merge_mappings_preserves_existing_tokens, test_merge_mappings_rotate_mints_fresh_tokens.
credential_source preservation
cmd_setup MUST NOT downgrade credential_source: bitwarden to env on re-run without an explicit --no-bitwarden flag. Running mibyan egress setup (no flag) preserves whatever was previously configured.
Tested via the cmd_setup flow in CLI tests (the bitwarden-preservation path is exercised when --from-bitwarden is followed by a plain setup re-run).
Extension points
Adding a new bearer-token provider
_BEARER_PROVIDERS in iron_proxy.py maps env var name -> tuple of upstream hosts. Adding an entry makes it discoverable by discover_provider_mappings(); the wizard mints a token for it automatically when the env var is present.
_DEFAULT_ALLOWED_HOSTS so the proxy allows the upstream by default. Run test_discover_provider_mappings_* to confirm.
Adding a new header-token provider (x-api-key family)
If the provider authenticates with a static NON-Authorization header (like Anthropic’sx-api-key, Azure’s api-key, or Gemini’s x-goog-api-key), add it to _HEADER_AUTH_PROVIDERS — iron-proxy’s secrets.replace.match_headers targets arbitrary header names, so these are first-class swapped providers:
aliases ONLY for interchangeable env-var names of the same credential (e.g. GOOGLE_API_KEY for GEMINI_API_KEY) — aliased names collapse into a single mapping, because two require: true rules on the same host reject each other’s requests. Also update _DEFAULT_ALLOWED_HOSTS.
Adding a new signature-auth provider (uncovered)
If the provider uses SigV4 / SDK-minted OAuth / request signatures, a static header swap cannot cover it. Add the env var to_NON_BEARER_PROVIDERS so the wizard and mibyan egress status warn about it:
Wiring iron-proxy into a non-Docker backend
_egress_proxy_args_for_docker is Docker-specific. Backends that want similar wiring need their own analogue that:
- Reads
load_config().get("proxy", {}); returns empty args ifenabledis false. - Calls
iron_proxy.get_status(); surfacesenforcesemantics onconfigured/pid/listening/ca_cert_pathfailure paths. - Calls
iron_proxy.load_mappings(); refuses to mount if empty ANDenforce_on_docker: true. - Sets the seven env vars (HTTPS_PROXY, NO_PROXY, REQUESTS_CA_BUNDLE, SSL_CERT_FILE, CURL_CA_BUNDLE, NODE_EXTRA_CA_CERTS, mibyan_EGRESS_PROXY) and the per-mapping
mibyan_PROXY_TOKEN_<NAME>vars. - Distributes the CA cert into the sandbox at a path the runtime will trust (typically
/etc/ssl/certs/mibyan-egress-ca.crt). - Implements collision detection against the user’s backend-specific env config.
Subscribing to per-request audit events
iron-proxy writes line-delimited JSON to~/.mibyan/proxy/iron-proxy.log on the currently pinned v0.39 (daemon + per-request records combined; see “Logging on iron-proxy v0.39” in the user guide). A plugin / external watcher can tail that file and react to allowlist denials, secret swaps, or upstream errors. When the pinned version is bumped to one that supports log.audit_path, the per-request stream moves to audit.log and watchers wired to that path go live without operator action. The schema is documented at docs.iron.sh/audit (link).
Testing
--help is a good first probe for “did my new flag register correctly”.
See also
- User-facing setup + troubleshooting: Egress proxy
- Docker backend internals: Docker
- Bitwarden Secrets Manager integration:
mibyan secrets bitwarden - CLI command reference:
mibyan egress - Sandbox-injected environment variables: Egress proxy (sandbox-injected)

