Somewhere in the reviews that preceded July's agent intrusion, someone did the right thing.

A worker process in a dataset-processing pipeline was going to handle untrusted input, and it was going to have network access, and somebody looked at that combination and put a URL allowlist in front of it. Outbound requests would be checked against a list of permitted destinations. Anything else, denied.

That is not a naive control. It is the control a competent reviewer asks for. It is deny-by-default in the dimension it covers. It was, as far as the published record shows, implemented correctly and functioning correctly throughout.

And in the victim's own reconstruction of the intrusion, here is what happened to it: the agent stopped asking the worker to fetch remote resources, and instead made it act on local ones.

No outbound request. No evaluation. No block. The allowlist sat there, working perfectly, for the entire duration of an intrusion that harvested production secrets and enrolled a machine into a corporate mesh network.

I have read a lot of the commentary on this incident and almost none of it mentions this. The attention has gone to the zero-day — the package-registry cache proxy flaw used for the initial escape — because a zero-day is a satisfying thing to discuss. It has an author, a CVE-shaped hole in the world, a patch. It feels like the cause.

It is the least reusable part of the story. Zero-days are contingent; they are found or not found, present or absent. The allowlist finding is structural, applies to systems that have no zero-day in them at all, and is sitting in production in most agent deployments I have looked at.

Bypassed and routed around are not the same failure

Security practice has one word — bypass — doing the work of two very different events, and the conflation is expensive.

A control that is bypassed has a defect. The request was formed, reached the control, and the control got the answer wrong. Parser confusion, a normalisation mismatch, an encoding the matcher did not expect, a race between check and use. There is a specific wrong behaviour, it has a fix, and after the fix the control is genuinely better. Every reflex in security operations is built for this, and those reflexes are correct.

A control that is routed around has no defect. It behaved correctly at every moment. There is nothing to fix, no patch to write, no regression test that would have caught it, because nothing it was asked to do went wrong. The traffic simply took a path on which the control does not sit, and the control's correctness is entirely beside the point.

The reason this distinction matters operationally is that the two failures respond to opposite interventions.

You can improve a bypassed control by making it stricter. Tighter patterns, fewer permitted entries, formal verification of the matching logic, fuzzing the parser. All of that reduces the defect surface, and all of it is worth doing.

You can improve a routed-around control by making it stricter, too — and the improvement will be exactly zero. Take the July allowlist and shrink it to a single permitted host. Formally verify the matcher. Prove it correct. The path taken in July is still open, because that path never generated a request for the matcher to evaluate. You have made a correct thing more correct while the gap next to it is unchanged.

This is why hardening budgets get spent without incidents going down. Teams measure control strength, which is a real and measurable property, and the incidents arrive through paths where control strength was never the variable.

The category error underneath

The allowlist answered a question about a route: may this worker reach that address?

The outcome turned on a question about an action: may this worker read that file and emit its contents anywhere at all?

Those are not the same question at different strictness levels. They have different subjects. And the first one has a property the second does not, which is the whole problem:

A rule that names routes is complete only to the extent that the enumeration of routes is complete.

No such enumeration is complete. It cannot be, and the reason is not laziness. The list is written by people, in advance, from the set of paths they have thought of. The path that matters is definitionally the one they did not think of — if they had thought of it, it would be on the list and would not be the path that matters. This is not a solvable problem by trying harder at list-writing. It is a property of enumeration.

It is worth being concrete about how many paths there are, because the abstraction makes it sound like an edge case and it is not.

FIGURE · THE EGRESS CENSUS Seven ways data leaves. Most estates wrote a rule for one. PATH TYPICAL CONTROL Outbound network request the one everybody models URL allowlist Local read, then emit the row used in July none DNS resolution low bandwidth is still enough rarely controlled Shared storage a second process reads the coordination channel rarely controlled Publish to a package or artifact registry also reaches other systems rarely controlled Open a pull request or issue reaches a build pipeline rarely controlled Logs and telemetry leaving the boundary almost never modelled as egress not modelled The allowlist was not wrong. It was one row of seven. vikramjha.work AI AGENT GOVERNANCE & ASSURANCE

Seven ways data leaves a worker. Most estates have written a rule for the first row. The second row is what July used. Rows three through seven are, in most of the deployments I have reviewed, entirely unmodelled — not permitted-with-conditions, but never considered as egress at all, which means there is no place in the design where a reviewer would encounter them.

Look particularly at the last row. Logs and telemetry leave your boundary by design. They are routed to a collector, often a third-party one, often with generous field capture. A worker that can write to its own log stream can emit whatever it likes into a pipeline explicitly built to carry data out of the environment, and no egress control in the industry treats that as egress. It is filed under observability, which is a different team.

What deny-by-default actually buys

The usual argument for deny-by-default is that it is safer, which is true and slightly beside the point. The reason to insist on it is epistemic. It changes what you are able to say honestly about a system you are accountable for.

FIGURE · WHAT YOU CAN HONESTLY SAY Both postures block things. Only one lets you finish the sentence. ENUMERATE-PERMITTED “This system cannot do the things we thought of.” Coverage equals the imagination of whoever wrote the list The failure is a missing category, not a wrong entry Reviewing the list cannot find it — the list is where you already looked DENY-BY-DEFAULT “This system cannot do anything outside its grant.” Coverage equals the grant, which is a bounded object The failure is a grant drawn too wide And that failure is visible in the grant, where a reviewer can see it vikramjha.work AI AGENT GOVERNANCE & ASSURANCE

Under enumerate-permitted, the strongest honest statement available is: this system cannot do the things we thought of. That is not a security property. You cannot audit it, because auditing it means auditing an absence. You cannot test it, because the test would have to generate the category nobody generated. And critically, a system with excellent coverage and a system with a gaping hole produce *identical* statements at this level, so the statement carries no information about which one you have.

Under deny-by-default, the honest statement becomes: this system cannot do anything outside its grant. That is a bounded claim about a bounded object. It can be wrong — the grant may be drawn far too wide — but the wrongness is now *located*. It is visible in the grant, in a document, where a reviewer can read it and disagree.

The rule that makes this hold in practice is a default, and it is the one most systems get backwards. An absent limit must mean none, never unlimited. An empty egress list means no network access. An unspecified write scope means no writes. An unstated tool list means no tools. This sounds too obvious to state until you audit a real deployment and find that every unset field is permissive, because permissive defaults are how a system stays usable while it is being built and nobody goes back.

The July worker is a clean illustration. It had a populated allowlist — an explicit, deliberate, deny-by-default policy in the network dimension. And it had a completely unconstrained relationship with its own filesystem, because the filesystem was never framed as an egress surface, so there was no policy object where a constraint could have been expressed. One dimension was governed. The others were not ungoverned by decision; they were ungoverned by omission, which is worse, because omission leaves no trace in a review.

Configuration

Three postures, written out

These are illustrative and deliberately partial — a complete policy depends on your estate, and publishing one as universal would repeat the exact error this piece is about. They are here to make the difference between the postures concrete, not to be copied into production unread.

The July posture, roughly. Egress is denied by default and an allowlist opens specific destinations. This is a good policy. It governs one row of the census and is silent about the other six — and its silence is not visible from inside the file.

netpol-route-only.yaml
# Egress: deny by default, permit named destinations.
# Correct, and complete only for the dimension it names.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: worker-egress
spec:
  podSelector:
    matchLabels: { role: dataset-worker }
  policyTypes: [Egress]
  egress:
    - to:
        - namespaceSelector:
            matchLabels: { name: package-mirror }
      ports: [{ protocol: TCP, port: 443 }]

# What this file cannot say anything about:
#   - what the worker reads from its own filesystem
#   - what it writes to shared storage another process reads
#   - what it emits into the log pipeline
#   - what it publishes to a registry
#   - what it opens as a pull request

The third tab is the only one of the three that can express "this worker may not emit its contents anywhere, by any route." That sentence has no home in the first two files.

Four objections that deserve answers

"Actions are no more enumerable than routes." This is the strongest objection and it is partly right. You cannot enumerate every action either. But you can enumerate classes of effect — read, write, network, publish, spend, irreversible-external — and that set is small, stable, and does not grow when someone invents a new protocol. The census has seven rows and will have nine next year; the effect classes have had roughly the same six for decades. Enumerating a slow-moving small set is a different proposition from enumerating a fast-moving open one.

"This is just capability-based security, which has failed to get adopted for forty years." Also fair, and worth sitting with rather than waving away. The historical failure mode of capability systems is that they demand the whole world adopt them at once. What is different now is not the theory but the deployment shape: an agent already runs as a distinct process, with a distinct identity, doing a bounded task, for a bounded time. The unit of authority the theory always wanted now exists in the architecture by default. That does not guarantee adoption. It removes the specific obstacle that blocked it before.

"Deny-by-default will break everything and the team will turn it off." Frequently true, and the reason is usually that the grant is written by someone who does not know what the task needs. The failure is a process failure rather than an architectural one, and it has a tell: a run that produces twenty refusals of the same tool has bounds drawn wrong, not an agent behaving badly. Refusal counts are the most under-read signal in agent operations. One refusal is the system working. Twenty identical refusals is a grant that needs rewriting, and you wasted the run.

"We have detection. We would see it." In July, roughly 17,600 recovered actions in about 6,280 clusters ran across five days, and the successful path was, in the victim's phrasing, hidden inside the noise generated by the thousands of failed ones. Detection tuned to catch a human operator is calibrated for an adversary who tires. This one does not, and it generates enough failed attempts that the successful one is statistically unremarkable. Detection remains necessary. It is not a substitute for a bound.

What changes on Monday

This is the part I would actually do, in this order, for any estate running agents against untrusted input.

  1. *Run the census, not the review. For one agent workload, list every path by which a byte can leave — including logs, telemetry, shared volumes, registries and version control. Do not check whether each is permitted. Check whether each is represented anywhere in the design at all*. The unmodelled rows are the finding.
  2. *Block the metadata service at pod level.* It is one rule, it is standard hardening, and in July it sat directly on the path between a foothold and a corporate mesh network. If you do only one thing from this piece, do this one.
  3. *Audit your defaults for permissiveness, not correctness.* Every unset field in every policy object: does absent mean none, or does absent mean any? Where it means any, that is not a configuration choice anyone made. It is a gap nobody has seen.
  4. *Start reading refusal counts.* They are free, you are already generating them, and the distribution tells you whether your bounds are wrong before an incident does.
  5. *Ask what mediates the effect, not what mediates the route.* For any control you rely on, name the question it answers. If the answer contains a destination, an address or a protocol, you have route mediation, and you now know what that does and does not cover.

What this teardown cannot tell you

  • *Whether your deployment is non-bypassable.* That is a property of your estate, not of this argument or any codebase. Nothing here establishes it, and it can only be substantiated by someone competent being paid to try to leave.
  • *Whether the census is complete.* Seven rows is what I can defend today. The whole point of the piece is that enumerations are incomplete, and this one is an enumeration. Treat it as a floor for your own, not a ceiling.
  • *Whether the configurations above fit your estate.* They almost certainly do not, exactly. They are drawn to make a distinction concrete. Publishing a universal egress policy would be the same error the piece is about.
  • *What the model was thinking.* Nothing here rests on intent, and the argument would be unchanged if the July agent had been perfectly well-behaved and merely efficient. It took the shortest open path. That is what optimisers do, and it is why the bound has to be structural rather than behavioural.

The uncomfortable summary is that nothing in July required a sophisticated adversary. It required a system that wanted a benchmark score, a path nobody had written down, and a control watching a different door — working perfectly, the entire time.