arizuko › components › egress
egress
What it is
egress is a forward HTTP/CONNECT proxy that allows one fixed list of hosts, for one client. It reads the destination out of the request the client sends, matches it against the allowlist, and either splices the connection or answers 403. That is the whole program.
runed starts one of these per spawned agent container, with that folder’s allowlist baked into the process at start, and kills it when the turn ends. There is no long-lived proxy, no registry of who is who, and no admin API — every one of those existed so that one proxy could serve many tenants, and one proxy per spawn needs none of them (specs/6/24).
Why it exists
An agent runs generated code with the network open. You want it to reach github.com and api.anthropic.com — not your internal services, not a crypto miner shipped in a compromised npm package, not wherever a prompt injection points it. A network-level firewall doesn’t know which folder is calling. Per-process iptables is fiddly. The proxy sits in the one place that knows both: the single path this spawn has out.
Per-spawn is also what removes a leak the shared model could not close. A shared proxy keyed tenants by source IP, so it depended on the consumer unregistering when a container exited. Docker reuses source addresses; if runed died between a container exiting and the unregister call, the stale entry survived and the next tenant on that address inherited the previous folder’s allowlist. Per-spawn deletes the registry, so there is nothing to go stale.
How it fits
runed spawn
|
| 1. create/reuse the folder network (docker network --internal)
| 2. docker run the proxy on it, --client <spawn ip>, allowlist baked in
| 3. docker network connect <uplink> <proxy>
| 4. poll the proxy's /health until 200 <-- the spawn does not exist yet
| 5. docker run the agent, HTTP_PROXY -> the proxy
v
agent --internal net--> its proxy --uplink--> allowed hosts only
The proxy is not what confines the agent — the missing route is. The folder network is created with --internal, so it has no route to the internet at all. The proxy is the only container attached to both that network and EGRESS_UPLINK_NETWORK (the compose project’s default bridge). The agent is attached to the internal network only. An agent that ignores HTTPS_PROXY and opens a raw socket to an IP literal reaches nothing, because there is nowhere for the packet to go.
--network container:<proxy> looks like it removes a startup race and instead removes the enforcement: the proxy has to reach the internet to forward, so its namespace carries a default route, and everything sharing it inherits that route. Measured on a live docker host — a container run that way, with no proxy environment at all, fetched an unlisted host directly and exited 0. Same rule for a second interface on the agent.
Order is enforced, not hoped for. The proxy answers GET /health with 200 only once its listener is bound and its allowlist is loaded. runed polls that (15s ceiling) and starts the agent only on a 200; a TCP connect would not do, since it succeeds the moment the port is bound. A proxy that fails to start, fails to connect to the uplink, or never reports healthy fails the spawn. There is no fallback to an unfiltered container.
The proxy serves exactly one address. --client is the spawn’s address; every other peer gets a 403, including the folder’s other spawns on the same network. That matters because concurrent spawns in one folder do not all hold the same list — an elevated /root turn’s is unrestricted — so without the pin a plain spawn could send its CONNECT to a neighbour’s proxy and borrow rules it was never granted. It is a bind scope, not a lookup: nothing is registered, and a second client is refused rather than served from some other rule set.
Inputs: the resolved allowlist for the folder (store.ResolveAllowlist walks the folder ancestry over network_rules and dedupes) plus runed’s four EGRESS_* values. Outputs: one proxy container per spawn, and a 403 for anything off the list.
Matching
Host names only — exact, subdomain-aware (--allow github.com covers api.github.com), case-insensitive, trailing dot stripped. A bare * allows everything. IP entries in the list are skipped: the filter works on the NAME, because the name is what the client puts in the request line. No path rules, no method rules.
An empty allowlist denies everything. That is a legitimate rule — a folder with no network grants — so the proxy starts and logs a warning rather than refusing to run.
The wildcard * is appended only when the folder holds the egress grant, or the turn is an elevated /root turn. The default role:member does not carry it, at any folder depth.
What it doesn’t do
- no TLS termination, no CA, no key material — CONNECT is tunnelled, never decrypted
- no request or response body access, no content filtering, no secret injection
- no registry, no admin API, no client library
- no transparent interception and no SNI peeking — forward mode reads the destination from the request line, so it never needed either
- no DNS filter — a single-tenant proxy already sees every destination, and a rule expressed twice can disagree with itself
- no supervision — the spawn’s lifetime is the proxy’s lifetime, and runed owns both
Interception stays declined (specs/5/13-ext-mcp.md): a proxy that never terminates TLS has nothing to leak if it is compromised, which is what keeps it small enough to audit.
Standalone usage
Yes. egress is a plain Go binary with three flags and no dependency on anything else in arizuko.
# build the image (repo root)
make images # produces arizuko-egress
# or the bare binary
make -C egress build # produces ./egress/egress
# run it: allow two hosts, serve one client
./egress/egress --listen :3128 --allow api.anthropic.com,github.com --client 10.99.4.7
| flag | env | default | meaning |
|---|---|---|---|
--listen | EGRESS_LISTEN | :3128 | listen address |
--allow | EGRESS_ALLOW | (empty) | comma-separated allowlist; a bare * allows all |
--client | EGRESS_CLIENT | (empty) | the only peer address served; empty accepts any peer |
The allowlist is fixed for the life of the process. To change the rules, start a new proxy — which is what a per-spawn proxy does on every turn anyway.
GET /health returns 200 {"status":"ok"} once the listener is bound and the allowlist loaded, 503 {"status":"starting"} before. It is answered before the --client check, because whoever starts the sandbox is not the client. Proxy requests arrive in absolute form (GET http://host/path), so an origin-form GET /health is unambiguously a probe of this process rather than traffic to forward.
Running something in a box locally
crackbox is the local-development command that composes egress with a sandbox so you can try the same shape on your own machine. Production never runs it — runed starts egress directly.
crackbox run --allow github.com,api.anthropic.com -- curl https://github.com
crackbox run --image python:3 --allow pypi.org,files.pythonhosted.org -- pip install requests
crackbox run --kvm swaps the container for a qemu VM using the sandbox/ library, where the allowlist becomes netfilter rules on the VM’s tap device instead of a proxy hop. That library is shipped but not wired into the arizuko spawn path — runed spawns Docker containers, and crackbox run --kvm is its only consumer today.
Key env vars
These are read by runed, not by the proxy binary. Naming EGRESS_IMAGE is the whole switch — there is no separate enable flag.
EGRESS_IMAGE— the image holding theegressbinary, e.g.arizuko-egress:latest. Empty means egress isolation is off and spawns run unfiltered.EGRESS_NETWORK_PREFIX— prefix for the per-folder docker networks.arizuko generatewrites<app>_<flavor>.EGRESS_UPLINK_NETWORK— the network giving each proxy its route out, and runed its path to the/healthprobe.arizuko generatewrites<project>_default.EGRESS_SUBNET— parent CIDR carved into per-folder/24s. Default10.99.0.0/16.
Full list and defaults in reference/env. If a .env still sets the retired CRACKBOX_ADMIN_API while EGRESS_IMAGE is empty, runed refuses to start and names both variables — an upgraded instance must not spawn unfiltered while looking healthy.
Go deeper
- security — where egress sits among the other agent-run controls.
- components/runed — the daemon that starts the proxy and the agent, in that order.
- reference/cli —
arizuko network allow / deny / list / resolve, which write the rules the proxy is started with. egress/README.md,crackbox/README.md,sandbox/README.md— the three directories and what each one is.specs/6/24— the cut: what the shared model cost and why per-spawn is safer, not just smaller.specs/6/9— the KVM/qemu sandbox library.