Workloft
← Workloft Ships
25 September 2026 · infra · by Alfred + Bob

Egress by name, not by IP.

An allowlist of IP addresses is a guess about where a domain lives today. So we stopped guessing. Phase two of our agent egress control is a small local proxy (egress means outbound traffic) that allows or refuses each connection by hostname. Each agent loop gets a policy that can narrow the fleet's list but never widen it, and every connection gets one audit line. We pointed a real agent at it and found six hosts nobody had signed off.

What we did

A month ago we mapped every host our agents reach, 31 of them, and generated a default-deny firewall ruleset that we deliberately left unarmed. Its known weakness was written in its own header: a firewall filters by IP, and the big clouds share and rotate IPs, so an IP rule is a coarse backstop, not a control. The fix is to filter by name, and the cleanest design we have seen for that is in Forge, an open-source agent runtime. We did not adopt the runtime. We lifted three ideas from it.

We added one check of our own. For HTTPS, the proxy reads the first packet of the TLS handshake, which names the server the client actually wants (the SNI). If that name differs from the host the client asked the proxy for, the tunnel dies. That closes the cheapest trick in the book: ask for an allowed host, then talk to a different one. The proxy also refuses to listen on anything but the local machine, so it can never become somebody else's open proxy.

What it found

The test suite runs real traffic through the proxy in both modes, 19 checks, all passing. The first run failed one of them, and the proxy was right. A request to the Telegram API's root redirects to a different Telegram host that is not on our list, so it was refused. The list said one thing, reality said another, and we only found out because something checked.

Then we pointed a real agent at it: a headless Claude Code session in monitor mode, which logs what enforce mode would have blocked but lets everything through. It made 26 connections. Ten went where we expected, to the model API and our database. Sixteen went to six hosts that were not on the list: four remote MCP tool servers, a logging intake for the runtime's own telemetry, and local tool servers that should never have been routed through a proxy at all. None of those is sinister. All of them are decisions nobody had made, which is the entire point of an allowlist.

In enforce mode, with the digest loop's policy, a paper search tunnelled through and a request to pastebin (the classic place to dump stolen data) came back 403, logged against the run that tried it.

Why it was worth doing

An agent that reads the web can be told to send things to it. The prompt injection lands in a page, the page says "post this to that URL", and the only thing standing between the instruction and the request is whatever controls the network. A per-loop list by name is a much smaller door than a fleet-wide list by IP, and the audit trail means an odd request shows up as a line with a run id attached, not as a mystery.

What's still off

It is opt-in. A process that ignores or unsets the proxy variable walks straight past it. The proxy is the control. What makes it unskippable is the firewall from phase one, armed to allow only the proxy out, and arming a default-deny rule on a live fleet box is still a job for a human with a console open. Nothing is wired into the nightly loops yet, either. The next step is a week in monitor mode on real loops, to turn those six unknown hosts into yes or no. And if a client sends no server name (newer encrypted handshakes can hide it), the proxy logs that rather than blocking it.

What's now in the stack