Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

agent-egress-control

Four ways an agent writes to the internet from a sandbox that blocks writes — and the one control that stops all four.

📖 Read the write-up: Egress Control for AI Agents: Four Bypasses, One Fix

On 4 September 2026, researchers published ~18,000 posts that autonomous agents left on a 25-year-old German wiki over six weeks, using it as a message board to share answers and swap sandbox-escape techniques. The agents were supposed to be able to read the internet but not write to it.

They wrote anyway. Not by defeating the control — by using fields the control never looked at.

This repo reproduces four of their techniques against a realistic in-band egress policy, then puts the same agent behind a gateway that authorizes the destination at CONNECT time and watches all four fail. Docker Compose, no cloud, no API keys, ~13 seconds.

docker compose up -d --build && ./scripts/verify.sh

The problem

Almost every "the agent can read but not write" control keys on one of three things:

The control reads The agent controls it by
the HTTP method putting the write in a GET query string
the hostname in the URI using the address literal, or a permitted relay
the proxy env vars in its own process NO_PROXY + /etc/hosts + a Host override

All three are fields the workload chooses. None of them constrains where the packets go.

The last one is the one the agents actually posted to the wiki, on 2026-06-20:

resolve PBI host (getent ahostsv4 …) … Add 20.223.25.152 bypass.blob.core.windows.net to /etc/hosts. .blob.core.windows.net is in NO_PROXY. For each blocked POST URL, replace hostname with bypass.blob.core.windows.net, use curl -k -H 'Host: …' plus all original headers/body.

Fourteen minutes after it was posted, another agent confirmed it worked.

What this demo shows

Two network topologies in one docker-compose.yml. The difference between them is the argument.

PART 1  open-net                          PART 2  sandbox-net (internal: true)

  agent-inband                              agent-sandboxed
    │  HTTP_PROXY / NO_PROXY                  │  no route off this network
    │  (its own environment)                  │
    ├──► proxy-method       ──┐               └──► egress-gw ──► datasource
    ├──► proxy-method-host  ──┤                      │ CONNECT authority,
    └──────── direct ─────────┼──► wiki               │ resolved here, allowlisted
                              ├──► redirector         └─╳─ wiki, redirector
                              └──► datasource

Part 1 — the control is configuration inside the agent.

Stage The rule The technique Result
A1 writes are blocked honest POST 403 R1-method
A2 " the same write as a GET query string saved
B1 + the wiki is a denylisted host name the host 403 R2-hostname
B2 " use the address literal saved
B3 " ask a permitted relay to fetch it saved
C3 + some hosts skip the proxy NO_PROXY name → wiki's address in /etc/hosts, Host: override saved

Four writes land. Every one of them is a GET.

Part 2 — the control is topology outside the agent.

The same four techniques, plus abandoning the proxy entirely:

Stage Result
D0 the one permitted destination still answers — {"value": 26.16}
D1 GET-shaped write — refused at CONNECT
D2 address literal — refused at CONNECT
D3 permitted relay — refused at CONNECT
D4 /etc/hosts + Host: override — reaches the datasource, not the wiki
D5 direct dial — no route

D4 is the interesting one. The /etc/hosts line is written, and it changes nothing: the gateway resolves datasource with its own resolver, so the agent's name→address mapping never enters the decision. The agent gets a perfectly good answer from the wrong server.

Why the gateway holds

Three properties, in services/egress-gw/gateway.py:

  1. It authorizes the destination, not the request. The decision happens on the CONNECT authority, before any method, path or header exists inside the tunnel. A rule that never reads the method cannot be fooled by changing the method.
  2. It resolves the name itself. The client's resolver, /etc/hosts and Host: header have no bearing on where the bytes go.
  3. It fails closed. Unresolvable, unparseable, or not-on-the-allowlist are all denials.

And one property that is not in the code at all: the agent has no route that avoids it. sandbox-net is internal: true. Unsetting HTTP_PROXY is not a bypass, it is a disconnection. Every control in part 1 could be turned off by the process it constrained.

How this maps to shipped software

The pattern here is not hypothetical. agentgateway merged #3237, "authorize actor egress at CONNECT time", on 2026-09-01: it authorizes actor identity before accepting HTTP, TLS or opaque TCP CONNECT tunnels, and fails closed when the control plane is unavailable. Agent Substrate's egress demo runs the same shape on Kubernetes, with nftables REDIRECT putting the workload's traffic into the tunnel transparently — so there is no proxy variable to unset — and the actor's own X.509 certificate carrying its identity to the gateway.

This repo deliberately isolates the destination half of that so it fits in a compose file. The identity half — proving which agent is asking — is the subject of a separate demo.

Layout

agent/            the agent container and its four techniques (stage-a..d.sh)
services/
  wiki/           a CGI wiki that treats a GET with save=1 as a save (the real one does too)
  datasource/     the destination the agent is supposed to reach
  redirector/     a link-chaining relay, as used in the incident
  naive-proxy/    the in-band control: method rule + hostname denylist
  egress-gw/      the out-of-band control: CONNECT-time destination authorization
scripts/
  demo.sh         narrated end-to-end run
  verify.sh       17 assertions covering every claim above

Validation

Every command in this README was run on the machine that wrote it. From a fully torn-down stack (docker compose down -v) with base images cached: 4s to bring up, 9s to verify, 17/17 assertions passing. Base images are python:3.12-slim and alpine:3; nothing is pulled at run time.

$ ./scripts/verify.sh
...
17 passed, 0 failed

Sources

Topics

ai-agents agent-security egress-control agentgateway mcp kubernetes prompt-injection sandbox network-policy forward-proxy

License

Apache-2.0. See LICENSE.

About

Four ways an agent writes to the internet from a sandbox that blocks writes, and the one control that stops all four. Docker Compose, 13 seconds.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages