Agent Egress Control and Network Policy for AI Agents
Allowlisting where an agent may connect, proxying package installs through infrastructure you own, and why egress is the most effective control surface for autonomous code
Why Egress Is the Control Surface
Covered elsewhere on this site as a threat; here as wiring
Prompt injection is the case where text an agent reads is treated as an instruction it follows. Simon Willison's lethal trifecta names what makes that dangerous - private data, untrusted content, and the ability to communicate externally - and the threat model lives on prompt injection defense. Of the three, external communication is the only one you can revoke and still have a useful agent.
The question is how, not whether. Everything below assumes the agent runs in a container you control (sandbox environments), that its identity is scoped rather than shared, and that one policy covers a coding agent, a CI job and a browser-driving agent (computer use agents) alike.
Allowlist by Destination, Method and Path - Not by Port
Port rules stopped carrying information when everything moved to 443
A rule permitting outbound TCP 443 permits the package registry, the code host, the model provider, a pastebin and an attacker's collector alike, because they are all the same port. The unit that carries meaning is the destination host, then the HTTP method and path.
Method and path separate reading from writing at one hostname: permission to reach api.github.com covers reading an issue, force-pushing a branch and rotating a webhook alike. "GET and HEAD to api.github.com, POST only to one path prefix" is an actual boundary. Coder's Agent Firewall expresses rules as space-separated key=value pairs using the keys domain, method and path; the documentation notes that "Agent Firewall was previously known as 'Agent Boundaries'."
Rule forms documented for the Coder Agent Firewall
# space-separated key=value pairs; keys are domain, method, path
domain=github.com
domain=*.github.com
method=GET,HEAD domain=api.github.com
method=POST domain=api.example.com path=/users,/posts
path=/api/v1/*,/api/v2/*
# the open-source engine underneath takes the same rules on the CLI
boundary --allow "key=value [key=value ...]" -- commandThe engine is coder/boundary, a Go project created in September 2025, whose README states a "Default deny-all security model" where "all traffic is denied unless explicitly allowed," implemented through "HTTP/HTTPS interception with transparent proxy and TLS certificate injection." Releases ran through v0.10.0 (July 7, 2026), which added session IDs, audit logging and header injection. Sourcing matters: those are README claims, while the product documentation states neither and covers audit instead, recording "Whether the request was allowed (allow) or blocked (deny)."
TLS Interception: What You Gain and What You Pay
Domain granularity is cheap; path granularity costs the encryption boundary
Two honest positions, and the choice decides most of the design. The cheap one filters on the connection request only. Squid's peek-and-splice parses "the TLS Client Hello and extracts SNI (if any)," then splices the connection to "become a TCP tunnel without decoding the connection" (Squid project wiki). You get the hostname, you allow or deny on it, and no plaintext exists in your infrastructure.
The expensive one is that method and path rules are not expressible without decryption. They live inside the encrypted request, so a proxy enforcing them must terminate TLS and be trusted by the workspace - which is why coder/boundary injects a certificate. No third option filters on paths and preserves end-to-end encryption.
Interception is defensible when the workspace is ephemeral, the CA is minted per run, and only agent traffic crosses the proxy. It is not defensible when the path carries third-party or end-user data, the traffic is regulated, the CA would outlive the run, or a human developer shares the proxy. There, stay on SNI-only filtering and accept domain granularity.
A MITM Proxy Is the Highest Value Target You Own
It sees every token, source file and payload in cleartext, across every run, in one process. Treat it as tier-zero infrastructure, or do not run it.
Package Installs Through a Mirror You Own
The largest legitimate hole in any agent allowlist
An agent that runs an install command needs a registry, and the naive fix is to allowlist the public one and its CDN. That rule reopens most of what the allowlist was for: installation is arbitrary code execution against a namespace anyone can publish into. Google's Artifact Registry documents the alternative: "A remote repository acts as a proxy for the upstream source so that you have more control over your dependencies," caching each package version on first request and serving the cached copy thereafter (Google Cloud documentation). The agent then needs one package destination, and it is one you can freeze.
Three details decide whether this helps. An unknown package must fail loudly: a mirror that falls through to upstream on a miss has no teeth, and the miss should surface as a build error with a name attached. Resolution must come from the lockfile only, with no float. And install scripts resolve their own domains at build time, which never appear in the dependency manifest and will be most of your unexplained denials in week one. See supply chain security.
Credential Injection at the Proxy
The strongest version keeps the token out of the model's context
If the agent holds the credential, any injection that reads an environment variable holds it too. Cloudflare's Sandbox SDK documents the alternative: "Your Worker issues a short-lived JWT token to the sandbox," and "The Worker validates the JWT and injects the real credential before forwarding the request," so that "Real credentials never enter the sandbox" (Cloudflare documentation). Note the scope: that is credential injection, not allowlisting, and the product docs say only that it is "Available on Workers Paid plan."
Pipelock, an open-source agent firewall covered by Help Net Security (May 4, 2026), states the same shape as a two-zone split. It "sits outside the agent at the egress boundary," so "the agent process holds the secrets and operates without direct network access, the proxy holds network access and no secrets, and all traffic crosses a scanning boundary between the two zones." Its repository dates from February 2026: a reference architecture, not a settled dependency. Which identity the proxy injects for is agent identity, on the zero trust assumption that the workspace is hostile.
Illustrative egress-proxy policy (not a vendor schema)
default: deny # anything not matched below is refused and logged
routes:
- name: read-github-api
match:
domain: api.github.com
method: [GET, HEAD]
inject:
header: Authorization
secret_ref: vault://agents/repo-read # resolved at the proxy, never in the workspace
log: full
- name: publish-release
match:
domain: api.github.com
method: [POST]
path: /repos/example-org/*/releases
approval: human # held until an operator approves
inject:
header: Authorization
secret_ref: vault://agents/release-write
log: full
- name: internal-package-mirror
match:
domain: artifacts.internal.example.com
method: [GET, HEAD]
inject: none # mirror needs no credential
log: summaryDefault Deny, and How to Discover the Real Destination List
Nobody writes this list from memory, and guessing gets the rollout reverted
Default deny is the right end state and the wrong first move. Turn it on cold and every build breaks within the hour, the change is reverted by lunch, and the second attempt is politically harder. Run the proxy in observe mode first, logging everything, over a full release cycle - a single day misses the monthly dependency refresh.
Rank the result two ways: by total request count, and by the number of distinct runs that touched each destination. A host with a million requests from one run is a build cache; a host with three requests across two hundred runs is load-bearing and easy to miss. Split into permanent destinations, which get real rules; one-off destinations, which get dropped; and anything you cannot attribute to a person or pipeline, which goes to review before enforcement day. The long tail will be dozens of hosts at one or two runs each. The temptation is a wildcard that swallows them; leave them denied.
Three Things to Ship With Enforcement
A break-glass path that adds a destination in minutes with an owner and an expiry, or engineers route around the proxy and you lose the visibility too. An alert on every denial, rare enough in steady state that each is worth a look. And a scheduled review, because allowlists rot: drop rules with no traffic, alert on any bare wildcard.
Kubernetes NetworkPolicy and Its Real Limits
Necessary as a floor, insufficient as the whole control
Deny all egress from agent pods, then add back what is needed. The caveat that costs an afternoon: default-deny-egress also blocks DNS, so it needs an explicit allow to cluster DNS on UDP and TCP port 53.
Default deny egress, then allow DNS and the proxy
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-egress
spec:
podSelector: {}
policyTypes:
- Egress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-and-egress-proxy
spec:
podSelector:
matchLabels:
role: agent-workspace
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
- to:
- ipBlock:
cidr: 10.0.0.0/24
ports:
- protocol: TCP
port: 5978Peers are only podSelector, namespaceSelector and ipBlock, which "selects particular IP CIDR ranges to allow as ingress sources or egress destinations." There is no hostname peer, and the project's list of what network policies cannot do includes "Targeting of services by name" and "The ability to log network security events (for example connections that are blocked or accepted)" (Kubernetes documentation). "Allow api.github.com" is not a sentence the API can express. One nuance is often stated wrongly: policy is already deny-by-default once it selects a pod; what is missing is explicit deny rules, so you cannot carve an exception out of a broad allow.
CIDR-based egress against a CDN-fronted service is therefore unworkable: the address set behind a code host or a registry is large, shared with unrelated tenants, and changes without notice, so any list you write is wrong within weeks. Use NetworkPolicy for the coarse shape and an L7 egress proxy or service mesh on top for host, method and path. See Kubernetes agent sandbox and network security.
Control Surfaces Compared
| Control surface | What it can express | What it costs |
|---|---|---|
| Kubernetes NetworkPolicy | Pod label, namespace label, IP CIDR, port | No hostnames, methods, paths, logging or deny rules |
| SNI-only egress proxy | Destination hostname, without decrypting | Blind to method, path and payload |
| MITM L7 proxy | Domain, method, path, header injection, full audit | CA trust in the workspace; every token visible in one place |
| Internal package mirror | Which package versions resolve, from vetted copies | You now operate a registry; install scripts still escape it |
| Credential-injection proxy | Which identity may call which route | Concentrates every credential; writes need approval |
Log Every Outbound Request
The allowlist is the control; the log is the detection surface
Record destination, method, path, response size and outcome, keyed to a run identifier. In a cluster only the proxy can produce this, because the Kubernetes documentation lists logging of blocked and accepted connections among the things NetworkPolicy cannot do.
Three anomalies are cheap to compute: a destination never seen before for that agent, which catches a new exfiltration path on first use; response bytes far above the distribution for that route across peer runs; and a burst of requests to one host. Feed them into agent observability, and write the response down in advance - who kills the run, who rotates which credential, who quarantines the branch - as covered in agent runbooks.
What Egress Control Does Not Buy You
It bounds the damage from an attack; it does not prevent one
Egress control does not stop an injection succeeding. The agent still reads the poisoned comment and still acts on it; what changes is where the attempt terminates. The hardest remaining gap is exfiltration through an allowed destination. If the agent may push a branch or comment on an issue - and it must, or it cannot do the job - those are channels to anyone who can read them, and no rule tells a legitimate commit from one whose diff carries a secret.
Below that sit low-bandwidth channels that are close to unclosable: a branch name, a commit message, request ordering, timing, DNS lookups themselves. The proxy is also a single point of failure and a concentration of every credential it injects. Allowlists rot. Method and path rules exist only if you decrypt. And an agent inside a perfect allowlist can still write a subtle bug into a file it was legitimately allowed to edit, which no egress policy will ever see.
Measure the Allowlist by What It Denies
A rule set that has never blocked anything is either perfectly tuned or quietly permissive, and the two look identical from outside.
Related Reading
The threat model, the identity layer, the runtime underneath
Prompt Injection Defense
The threat model behind every rule here
Agent Identity
Who the proxy injects a credential for, and how it is scoped
Network Security
The surrounding design this policy plugs into
Kubernetes Agent Sandbox
Pod-level isolation and where NetworkPolicy fits
Agent Runbooks
What happens after a denial alert fires
Agent Observability
Where the proxy log goes downstream
