A CSO Online piece built around an October 2026 Hacker News thread made a direct claim: agents aren’t going rogue , frontier labs are skipping basic containment. 948 points, 620 comments, and most of the thread agreeing on the same diagnosis of the OpenAI/Hugging Face registry-proxy intrusion and a related Anthropic eval-environment review: no secure sandbox, live internet reachable from eval runs, no egress monitoring, and credentials sitting in reach of anything that asked for them.
Defensive security research. This post analyses publicly disclosed findings so the controls that stop them can be tested. It describes mechanisms, not procedures, and links the primary sources for anyone verifying the work.
Most of the thread agreed with that diagnosis, and frontier labs with unlimited engineering budget kept skipping the fix anyway. So here are the four failures, reproduced on purpose, with the specific control that closes each one: one agentgateway config and a Docker Compose diff. Code at github.com/themsquared/agent-containment-basics .
The scenario: a registry lookup that pivots
A toy eval agent has one real job: look up a package on a registry API. Then, modeling the registry-proxy pattern the Hugging Face incident made famous, where a tool or a config edit gets pointed at a destination nobody approved, it makes one more request. Same code, same credential-handling logic, pointed somewhere it was never supposed to go.
The agent’s own code (agent/eval_runner.py) is byte-for-byte identical in both runs below. Nothing about the agent changes between the vulnerable version and the contained one. Only its environment and its network membership do, which is the whole argument: these are infrastructure controls, not application logic the agent has to cooperate with.
Failure 1: no secure sandbox, live internet reachable
The vulnerable setup puts the agent, the registry API, and a stand-in for an arbitrary external host on one flat Docker network:
docker compose -f docker-compose.vulnerable.yml up -d registry-api attacker-target
docker compose -f docker-compose.vulnerable.yml run --rm agent
[agent] ambient credential present: True
[agent] legit registry lookup: HTTP 200 <- http://registry-api:8091/package
[agent] unapproved pivot attempt: HTTP 200 <- http://attacker-target:8092/exfil
Both calls succeed, because nothing on this network distinguishes “the registry I’m supposed to call” from “anything else reachable.” Checking the stand-in host’s own request log confirms the pivot landed:
{"path": "/exfil", "api_key": "registry-secret-9f2"}
The fix for this one isn’t a config flag, it’s topology. The contained setup gives the agent container membership in exactly one Docker network, marked internal: true:
networks:
sandbox:
internal: true # no route to the outside world at all, gateway included
egress: {}
agentgateway has membership in both sandbox and egress. The agent has membership in sandbox only. It isn’t blocked from reaching the registry or the stand-in host by a firewall rule that could be misconfigured; it has no network path to either one, the same result a correctly scoped Kubernetes NetworkPolicy or security group produces in production.
Failure 2: no default-deny at the perimeter
With the agent confined to its own network, the only thing it can talk to is agentgateway, on port 8080. The gateway’s entire routing table is one route:
binds:
- port: 8080
listeners:
- protocol: HTTP
routes:
- name: registry-lookup
matches:
- path:
pathPrefix: /package
backends:
- host: registry-api:8091
Run the same agent code against this setup:
docker compose -f docker-compose.isolated.yml up -d registry-api attacker-target agentgateway
docker compose -f docker-compose.isolated.yml run --rm agent
[agent] ambient credential present: False
[agent] legit registry lookup: HTTP 200 <- http://agentgateway:8080/package
[agent] unapproved pivot attempt: HTTP 404 <- http://agentgateway:8080/exfil
/package matches the one declared route and gets proxied. /exfil matches nothing, so there’s no backend to send it to, and the gateway returns a 404 before it goes anywhere. Nobody had to list /exfil, or any other bad path, as denied. Only /package is declared, so only /package is reachable, and that stays true without edits when a new unapproved path shows up later.
Failure 3: ambient credentials
In the vulnerable run, the agent container holds registry-secret-9f2 directly, as an environment variable, because that’s the normal way to wire up an API client. Anything that container does, including the pivot, can carry that credential, because the agent has no way to know which of its own requests are supposed to be authorized and which aren’t.
In the contained run, the agent container has no credential at all:
API_KEY = os.environ.get("API_KEY") # present (ambient) in vulnerable mode, absent in isolated mode
The gateway injects the real credential, but only on the route that’s allowed to use it:
policies:
transformations:
request:
add:
x-api-key: "'registry-secret-9f2'"
Checking the registry’s request log confirms the injected header arrived correctly on the legitimate call, and the stand-in attacker host never saw a credential at all in the contained run, because the request that would have carried one never matched a route that attaches anything. A fully compromised agent, in this setup, is an agent that can make requests with no credential attached to the one place it’s allowed to call, and nothing at all to anywhere else.
Failure 4: no egress monitoring
This is the one CSO’s critique undersells, because it implies monitoring means something more involved than it does. agentgateway’s access log is attached at the listener, not just on matched routes, so it sees every request the listener handles:
... route=default/registry-lookup ... http.path=/package ... http.status=200 ...
... error="route not found" reason=NotFound ... http.path=/exfil ... http.status=404 ...
That second line is the whole point. On the flat network with no gateway, the pivot attempt left no trace anywhere except the attacker’s own log, which obviously isn’t a log anyone defending the system has access to. Here, the rejected request is a structured, queryable entry with a reason, on infrastructure the defender controls, with zero custom instrumentation. The gateway was going to log requests anyway; routing all egress through it is what turns that into an audit trail.
Why this isn’t the whole containment story
This repo closes the perimeter half of containment: default-deny egress, no ambient credentials, an audit trail, a sandbox the agent genuinely cannot route around. It is deliberately not about an agent escaping its own sandbox boundary entirely, the harder case covered by microVM hardening and the kind of red-team campaigns frontier labs have started running against their own infrastructure. The claim here is narrower and, per CSO’s piece, far more commonly skipped: even a sandboxed agent needs a perimeter it can’t talk its way around, because the sandbox boundary is one layer, not the only one. agent-egress-control-bypasses covers a narrower, nastier version of the same perimeter problem, where the egress control itself is a proxy variable the agent can see and change.
Docker Compose stands in here for whatever actually schedules the agent in production, a Kubernetes Deployment managed by something like kagent in most of the setups this campaign covers. The two Docker networks map directly to a NetworkPolicy that puts the agent pod’s egress on the gateway’s ClusterIP and nothing else; nothing about the demo depends on Compose specifically. What does the real work, in either case, is that the agent was never granted a path around the gateway to begin with. No in-cluster policy update, no code change on the agent, can route around a network boundary the agent was never placed inside of.
Try it
git clone https://github.com/themsquared/agent-containment-basics.git
cd agent-containment-basics
./run.sh
No GPU, no cluster, no API keys. Three Python stdlib services, one Docker Compose diff, and a public agentgateway image. Every control here is configuration, not code: the agent’s own logic never needed to change. That’s the actual argument underneath CSO’s piece. These controls are cheap enough that “we didn’t have time” doesn’t hold up.