All posts
8 min read

Egress Control for AI Agents: Designing an Allowlist Proxy

Restricting where an agent can send data is the most reliable defence against exfiltration, and easy to get subtly wrong. Network-layer enforcement, SNI vs TLS interception, DNS as a covert channel, allowlisted domains as leak paths, credential brokering, and testing.

AI agentssecurityinfrastructureadvanced

Of the three conditions that turn prompt injection into data theft — private data, untrusted content, external communication — the third is the one infrastructure can remove most reliably. (Prompt Injection in Coding Agents: A Threat Model.) An agent that can read your secrets but can't send them anywhere is a contained problem.

But "restrict network access" is a design problem with several sharp edges. This article walks through building egress control for agent environments: where to enforce it, what to inspect, the covert channels people miss, and why the allowlist itself is an attack surface.

Requirements

A useful egress control for coding agents should:

  1. Default deny — nothing leaves unless allowed.
  2. Enforce at a layer the workload can't bypass — not by convention.
  3. Allow by name, since package registries and APIs sit behind changing IPs and CDNs.
  4. Cover all protocols, including DNS.
  5. Be per-workspace, so policies and logs map to one tenant or task.
  6. Log every decision for forensics.
  7. Keep developer experience workable — package installs, git, and the APIs the project needs.

Enforcement: the proxy is advisory, the network is not

The common first attempt sets HTTP_PROXY/HTTPS_PROXY environment variables and runs an allowlisting proxy. That's half a design. Proxy environment variables are a convention: any process can ignore them, and an agent steered by injected text will happily run curl --noproxy '*' or open a raw socket.

The enforcement point must be the network itself:

  • Place the workload in a network where the only reachable destination is the proxy (and, if needed, a controlled DNS resolver). In Docker, an internal: true network plus a proxy container attached to both the internal and an external network achieves this; in Kubernetes, a default-deny NetworkPolicy with explicit egress to the proxy; on VMs, host firewall rules (nftables) in the workload's namespace.
  • The proxy variables then become a convenience that makes allowed traffic work, not the control that stops disallowed traffic.

Also block, at the network layer:

  • Cloud metadata endpoints (169.254.169.254 and IPv6 equivalents), which hand out instance credentials.
  • Internal ranges — other tenants, your control plane, databases not belonging to this workspace.
  • IPv6 paths if your rules only cover IPv4.

Deciding by name: SNI vs interception

For HTTPS, the proxy sees a CONNECT host:443 request and then an encrypted stream. Two approaches:

SNI / CONNECT-host allowlisting (no decryption)

The proxy allows or denies based on the CONNECT hostname and the TLS SNI in the ClientHello, and never decrypts. Simple, preserves end-to-end TLS, no CA to distribute.

Weaknesses:

  • Domain fronting — sending one hostname in SNI and another in the encrypted Host header — can smuggle traffic to an unintended backend on a shared CDN. Many major CDNs now reject mismatches, but not all.
  • Encrypted Client Hello (ECH) encrypts the real SNI. As ECH deployment grows, SNI-based inspection sees only an outer public name. Proxies should reject ECH-bearing handshakes they can't evaluate, or require that allowed destinations are reached via plain SNI.
  • No visibility into what's sent to allowed domains (see below).

TLS interception

The proxy terminates TLS with a workspace-trusted CA, inspects the request (host, method, path, even body size), then re-encrypts upstream. This enables method- and path-level policy — the important capability when allowed domains can themselves carry data out.

Costs: distributing and scoping the CA into each workspace's trust stores (system, Node, Python, Java all differ), breaking certificate-pinned clients, handling sensitive plaintext in the proxy, and more operational weight.

A pragmatic hybrid: SNI allowlisting for low-risk destinations (package downloads), interception with path rules for high-risk ones (code hosts, APIs that accept writes).

DNS: the channel people forget

Even with HTTP locked down, a workload that can resolve arbitrary names can exfiltrate by encoding data into queries: c2VjcmV0.attacker.example reaches the attacker's authoritative nameserver through your own resolver. It's slow, but more than fast enough for an API key.

Controls:

  • Block direct DNS (UDP/TCP 53, and DNS-over-HTTPS/TLS endpoints) from the workload; allow only your resolver.
  • Have the resolver answer only for allowlisted names (or their CNAME chains) and return NXDOMAIN/REFUSED for everything else. If the proxy resolves on the workload's behalf, the workload may not need general DNS at all.
  • Log queries; alert on high-entropy labels and volume anomalies.

Allowlisted domains are exfiltration paths

This is the subtle part. A typical coding-agent allowlist includes a package registry, a code host, and a few APIs. Each can carry data out:

  • Code hosts — creating a public gist, pushing to an attacker-owned repository, opening an issue or comment on a public repo.
  • Package registries — publishing a package; or simply requesting a URL whose path encodes data, which lands in the registry's or a CDN's logs (less useful to an attacker, but not zero).
  • Any API that accepts writes — paste services, chat webhooks, storage buckets.
  • Wildcards — *.amazonaws.com or *.googleusercontent.com include buckets and endpoints anyone can create.

Mitigations, in increasing strength:

  1. Allowlist narrowly — exact hostnames, not wildcards on shared platforms.
  2. Restrict methods and paths on write-capable domains via interception: allow GET to the registry, allow git fetch and push only to specific repository paths, deny API endpoints that create public content.
  3. Constrain credentials, not just destinations — if the only token in the workspace can push to one repository, pushing to an attacker repo fails at authorization.
  4. Rate and byte limits per destination, to cap how much can leave even through permitted paths.

Credential brokering

A natural extension of the proxy: the workspace never holds the credential at all. The proxy, knowing which workspace a connection came from, injects the appropriate Authorization header for specific destinations — the registry token, the git token, the API key. The agent can use the credential through the proxy but cannot read, print, or exfiltrate it, and the proxy can enforce which destinations each credential may reach.

This converts "the agent read .env" from a secrets incident into a non-event. It pairs with short-lived, narrowly scoped credentials issued per workspace. (How to Run AI-Generated Code Safely.)

Per-workspace policy and developer experience

Egress control that blocks legitimate work gets disabled. Keep it usable:

  • Sensible defaults per template — a Node workspace allows the npm registry and the code host; a Python one allows PyPI; both allow the model provider's API.
  • Project-level additions reviewed like code (a policy file in the repo, applied by the platform), so allowing a new API is a PR, not a support ticket.
  • Clear denials — the proxy returns an explicit, logged error naming the blocked host, so a developer (or agent) sees "egress to api.example.com denied by policy" rather than a timeout.
  • Package mirrors — pointing package managers at a controlled mirror or pull-through cache reduces the number of external registries in scope and adds a supply-chain checkpoint.

Claude Code's own sandboxing takes a similar approach at the tool level — filesystem and network restrictions enforced by the operating system rather than by prompts — and it composes well with network-level enforcement around the whole workspace. (Running Claude Code Unattended.)

Logging

Record, per workspace: timestamp, destination host, SNI, (if intercepting) method and path, bytes in and out, decision, and the policy rule that decided. Correlate with the agent session log so an investigation can go from "unexpected outbound connection" to "the tool call that caused it" to "the input that preceded it."

Testing

Test the control as an attacker would, from inside a workspace:

  • curl --noproxy '*' https://example.com fails (direct egress blocked at network layer)
  • Raw TCP to an arbitrary IP:port fails
  • dig against a public resolver fails; resolving a non-allowlisted name via the workspace resolver fails
  • A high-entropy subdomain lookup is blocked and logged
  • The metadata endpoint is unreachable
  • A non-allowlisted host via the proxy is denied with a clear error
  • Domain-fronting attempts (mismatched SNI/Host) are rejected
  • Write operations to allowlisted domains outside permitted paths are denied (if intercepting)
  • A canary token planted in the workspace cannot be exfiltrated by any of the above

Automate these as a continuously running conformance suite; egress policies drift as templates and allowlists change.

The summary

  • Enforce at the network; proxy variables are only convenience.
  • Choose SNI allowlisting for simplicity, interception where you need path-level control; watch for fronting and ECH.
  • Control DNS explicitly.
  • Treat every allowlisted domain as a potential exfiltration path; restrict methods, paths, and credentials.
  • Broker credentials at the proxy so workspaces never hold them.
  • Log decisions per workspace, and test like an attacker.

EasySpawn isolates every workspace in its own container with scoped credentials and no access to other tenants or the host — the foundation network egress policy builds on. See how it works or join the waitlist.

Related: Hardening Containers With seccomp, AppArmor, and User Namespaces · Firecracker vs gVisor vs Containers · Container Networking Internals

Keep reading