
Fifteen years building engineering platforms, currently focused on advanced AI infrastructure at yeet. I love turning the deeply complex topics into something everyone can understand. I relate deeply with the core yeet philosophy that you can just build things.
Last updated: October 2026
Quick answer. Yes, structurally. An agent is an SSRF primitive you installed deliberately: a process that makes outbound requests whose destinations are chosen at runtime by untrusted input. The controls that work are the ones that work for SSRF generally, applied to the agent process rather than to a web handler. Deny link-local by default, because
169.254.169.254is the cloud metadata endpoint and the classic SSRF target. Put egress policy around the agent process itself rather than its sandbox, since an agent's model, MCP and connector traffic originates outside the command sandbox. Before writing any allowlist, capture what the agent actually reaches, because a policy written from assumptions either breaks the agent or permits more than you intended.
I build eBPF tooling at yeet, and the agent egress conversations I find most useful are the ones that stop treating this as a new problem. The threat model is familiar. What is new is that the attacker-controlled input and the request-making process now live in the same trusted binary.
Because it does exactly what an SSRF vulnerability does, except that you installed it on purpose and it is working as designed rather than as a bug. Recognising that shape is useful because it means the defences are already well understood: this is a known problem class pointed at a new process, not a novel threat requiring novel controls.
Classic SSRF is a server making an outbound request to a destination an attacker influenced. The agent case has the same shape with different parts. The agent reads content it did not write: a web page, an issue description, a dependency's README, a tool result. That content can carry an instruction. The agent then makes requests using its own tools, from a process running as you, on your network.
Two properties make it sharper than the web-handler version. The destination set is unbounded, because an agent is designed to reach arbitrary hosts rather than a fixed list of integrations. And the request-making capability is the product feature, so you cannot remove it without removing the agent.
What you can do is bound where those requests are allowed to go, which is the same answer SSRF has always had. The controls do not need inventing for the agent case; they need pointing at a different process than the one most documentation assumes.
Start with the internal addresses an agent has no legitimate reason to reach, because that is where a single successful request does immediate damage rather than gradual harm. External destinations are a larger and less urgent problem, and trying to enumerate them first is how this work stalls before anything ships.
169.254.169.254. The single most valuable SSRF target on any cloud host, because the instance metadata service serves credentials to anything that asks. Link-local as a whole, 169.254.0.0/16, should be denied.127.0.0.1 hosts local services, admin interfaces and MCP servers running as separate processes. Deny broadly, permit the specific ports the agent actually uses.10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16. Your internal services generally do not need to be reachable from a coding agent, and this is where lateral movement lives.That list is deny-first and small, which is deliberate: it is tractable to write and it covers the cases where a successful request is immediately serious. An allowlist of permitted external destinations is a stronger control and a much larger project, so starting with the deny list gets most of the protection on day one.
Around the agent process rather than inside its command sandbox, and this distinction decides whether the policy covers the traffic you care about or almost none of it. Getting it wrong produces a configuration that reads as thorough and governs the wrong process tree.
An agent's command sandbox governs the subprocesses it spawns. Codex's network restriction, for example, puts sandboxed commands in a network namespace via bubblewrap. What that never covered is the agent's own traffic: its model requests, MCP server connections, web search and connector calls all originate from the parent process, outside the command sandbox by design.
So a policy expressed only in the agent's sandbox configuration controls the shell commands and leaves the agent's own tool calls untouched, which is the opposite of the threat model above. An injected instruction acts through the agent's tools, not through a shell command.
The control that matches is an outer boundary: a network namespace with default-deny egress wrapping the agent process, host firewall rules scoped to its user or cgroup, or a managed proxy the agent is forced through with everything else denied. Each bounds the agent itself rather than its children.
Measure first, because the gap between what an agent actually needs and what you assume it needs is wide enough to take the agent down. This is the step people skip, and it is the one that determines whether the policy survives contact with real work.
An agent host reaches more destinations than anyone predicts: the model API and its CDN, MCP servers and whatever those reach onward, package registries plus the object storage they redirect to, and whatever the agent's actual work touched. Writing a policy from that list is straightforward. Writing it from memory is how you discover at 2am that package installs resolve through a host nobody wrote down.
curl -fsSL https://yeet.cx | sh # install yeet, once
yeet run gh:yeet-src/pktscope # decoded packets, including TLS SNI
pktscope decodes the TLS ClientHello, so a handshake shows as TLS Handshake ClientHello · sni=example.com rather than an address that tells you nothing about who owns it. The fuller treatment of what an agent connects to covers the four categories a normal baseline contains, and anything outside them is what the policy exists for.
Run it across a representative working session, take the set of names, and use that as the starting allowlist. Keep it attached afterwards, because the useful signal in steady state is not the list but the additions: a destination that was not in the baseline is either new legitimate work or the thing you wrote the policy for.
One measured caveat if you were planning to build this from DNS logs: on glibc hosts, resolution goes through NSS over a Unix socket, so queries often never hit the wire. SNI names close to everything; DNS fires close to never.
Unless you have explicitly blocked it, yes, and this is worth checking on your own hosts rather than assuming either way, because the default on most cloud instances is that the path is open. Nothing in a coding agent's configuration denies it, so the only thing standing between an agent and instance credentials is whatever you put there.
Nothing in a coding agent's default configuration denies 169.254.169.254. An agent that can make an HTTP request can request it, and on a cloud instance with a permissive instance role the response is credentials. IMDSv2 raises the bar by requiring a PUT to obtain a token first, which defeats the simplest reflected-SSRF cases but not an agent that can issue arbitrary requests, since it can perform both steps.
Checking takes a single request from the agent host, and unlike most security questions the answer is binary rather than a matter of degree. Either the path is open or it is not:
curl -sS -m 3 -o /dev/null -w '%{http_code}\n' http://169.254.169.254/latest/meta-data/
A response means the path is open from that host, and an agent running there can reach it exactly as easily as curl just did. Blocking link-local for the agent's user or namespace closes it, and keeping a capture attached tells you afterwards whether anything tried, which is the part a firewall rule alone does not give you.
No, and expecting it to is the mistake that makes egress policy look like a failure when it is doing precisely the job it was built for. The two controls address different stages of the same attack.
Prompt injection is an input problem: the model acted on instructions in content it read, which is one of the two agent threat models worth separating. Egress policy does not prevent the model from being convinced, it bounds what a convinced model can reach. Those are different stages, and no amount of network control addresses the first.
What egress policy does, and what it is worth buying it for, is reduce the blast radius of a successful injection and produce a record. A denied connection to an unexpected destination is both a prevented action and a signal that something tried, which is also how you would detect an agent exfiltrating code. Without the policy you get neither; without the capture you get the first and not the second.
The layered position is unglamorous and correct: input controls reduce how often injection succeeds, egress controls bound what a success can achieve, and observation tells you that it happened at all. Dropping any one of the three leaves a gap the other two cannot cover, which is why this reads as boring defence in depth rather than a single clever fix.
An AI agent is an SSRF primitive with a user manual, and the controls are the familiar ones aimed at a new process. Deny link-local, loopback and private ranges first, since those are where a successful request is immediately expensive. Put the policy around the agent process rather than its command sandbox, because the traffic that matters originates outside the sandbox. And capture the real destination set before writing an allowlist, because the alternative is a policy that either breaks the agent or permits what you were trying to stop.
Structurally it is one: a process making outbound requests to destinations influenced by untrusted content it reads. The only difference from a vulnerability is that the capability is intended and you installed it deliberately. That framing is useful because it means the defences are already well understood rather than needing invention for the agent case.
Yes. It is the cloud instance metadata endpoint, and on an instance with a permissive role it returns credentials to anything on the host that asks. Nothing in a default coding agent configuration denies it, so an agent able to make an HTTP request can reach it. Denying link-local as a whole, 169.254.0.0/16, is the broader form of the same control.
Partially, and less than people assume in this case. Requiring a PUT to obtain a token first defeats the simplest reflected-SSRF patterns, where an attacker can only induce a single GET. An agent that can issue arbitrary requests can perform both steps itself, so IMDSv2 is a real improvement against classic SSRF and not a substitute for blocking link-local on an agent host.
Around the agent process itself: a network namespace with default-deny egress, host firewall rules scoped to the agent's user or cgroup, or a proxy it is forced through with everything else denied. Its command sandbox is the wrong layer, because that governs subprocesses while the traffic an injection actually uses comes from the parent process.
Only for the commands it runs. Model API requests, MCP server connections, web search and connector calls all originate in the parent agent process, which sits outside the command sandbox by design because the agent needs network access to function. A policy written only in the sandbox config therefore governs shell commands and leaves the agent's own tool calls untouched.
Capture SNI per process across a representative working session, then take the resulting set as your starting allowlist. That surfaces the destinations nobody writes down, such as the CDN a package registry redirects to and the object storage behind it. Keep the capture attached afterwards, because in steady state the useful signal is additions to the baseline rather than the baseline itself.
No, and expecting it to is how egress policy gets written off as useless. Injection is an input-side problem: the model was convinced by content it read, and no network control addresses that. What egress policy does is bound what a convinced model can reach and produce a record that something tried, which is blast-radius reduction and detection rather than prevention.
Unless private ranges are explicitly denied, yes. A coding agent runs as your user on your network, so it inherits exactly the reachability you have, including internal APIs, admin interfaces and anything else that trusts the network position rather than a credential. That is why 10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16 belong in the default deny list.
Often not, which catches people building this from DNS logs. On a glibc host, name resolution goes through NSS to a local stub over a Unix socket, so the query frequently never reaches the wire where a DNS tap could see it. Measured on a real agent workload, SNI named nearly every connection while DNS fired almost never, so they are not interchangeable sources.
An allowlist is the stronger control and a considerably bigger project, because it requires knowing every destination the agent legitimately needs and keeping that list current as tooling changes. A small denylist covering link-local, loopback and private ranges gets most of the protection on day one and is tractable to write from memory, so it is the sensible first step rather than the final answer.
169.254.169.254 returns and what the token requirement does and does not defeat.codex-rs/linux-sandbox README. Source for what a command sandbox's network isolation covers, which is the basis for the claim that it is the wrong layer here.pktscope. The packet analyzer used for the baseline capture, which decodes TLS ClientHello records and names destinations by SNI.dialout. Outbound connections with the sending process attached, for the address-level view behind the names.