Read-only holds. Four other things do not.
Hey đź‘‹
I gave a talk at fwd:cloudsec Europe in London on Tuesday. Twenty minutes, main room, on a deployment that has become routine: an AI agent with read access to a production Kubernetes fleet, and what that read-only grant does and does not bound.
Two honest notes about how it went.
The delivery was better than my last one, because I rehearsed ideas instead of sentences. The content was worse than it should have been, because I had twenty minutes and I spent them on the argument rather than the examples. Watching the room, I could see where I lost people. It was every place I stated a finding without showing the mechanism underneath it.
So this issue is the talk with the mechanisms put back in. If you were in the room, this is the version you should have got. If you were not, the written form suits this material better than twenty minutes on a stage did.
One connection first, because it decides how much weight to put on any of this. Last week’s issue covered four disclosures in six weeks where AI agents took unsanctioned action against real systems, at a government institute, two frontier labs and an evaluation vendor. In every one, the sandbox held and the control that failed was a sentence in a prompt. That is the same conclusion the talk arrives at, reached from the architecture of an ordinary production deployment rather than from an evaluation harness. I did not know the lab disclosures were coming when I submitted the abstract in June, and arriving at the same place by two different routes is why I trust it more now than I did then.
The MCP issue I promised lands Tuesday, ahead of Amsterdam. This one first.
The deployment
The shape is the same wherever this turns up. A team wants faster incident triage, so an AI agent is given read access across a multi-cluster estate: cluster state, metrics, deployment history. No write permissions. It goes to review, and the absence of write permissions is what carries it through. Approved, deployed.
Worth saying plainly, because everything after this reads as criticism otherwise: the pattern works. Teams running it do resolve incidents faster, and that is exactly why it keeps getting approved. The question is not whether to allow it. The question is what read-only actually bounds once it is running.
Everything below is written at pattern level on purpose. It carries the argument and the controls, not an incident report, and it describes no particular system. The failure classes are what travel, because they follow from the shape of the deployment rather than from anyone’s infrastructure.
Read-only does exactly what it says. Nothing gets mutated. Four other things break anyway, and not one of them needs a write permission.
Failure one: confidentiality
Here is the mechanism I skipped on stage.
An agent doing infrastructure troubleshooting reads logs. Logs contain tokens, connection strings, customer identifiers and whatever an application decided to print at debug level. That is not a misconfiguration, that is what logs are. The agent reads them because you asked it to find out why a service is failing, and finding out why a service is failing means reading what the service said.
Then the agent puts what it read into a context window, reasons about it, and produces an answer. The answer leaves your infrastructure. That is what you deployed it to do.
So the permission set says read. The data flow says read and then transmit. Nobody wrote a transmit permission because nobody had to.
Put it in the terms most people already know. The lethal trifecta is private data, untrusted input and a channel out. A read-access troubleshooting agent on a Kubernetes fleet has all three by construction. The private data is your logs and secrets. The untrusted input is anything the agent reads, and logs are attacker-writable in any system that logs user-supplied strings. The channel out is the reply.
On stage I called this exfiltration and moved on. The sentence underneath it is the one that matters: an agent that can read everything and talk to the internet is an exfiltration path with a permission set that passes review.
Failure two: attribution
This is the one nobody asks about, and the one that hurts most at the moment you need it.
The agent authenticates as a service account. One service account for the whole deployment. Every API call it makes arrives in the audit log as that identity.
So the audit log answers the wrong question. It tells you the agent read a secret. It cannot tell you which agent instance read it, which investigation it was part of, which human asked for that investigation, or whether the read was part of the task or the agent wandering.
Two things fall over when attribution fails, and you find out during an incident.
Revocation gets coarse. If one agent behaves badly, your only lever is the shared identity, so you take the capability away from everything using it. That is an availability decision forced on you by an identity design.
Forensics gets guesswork. “Was this read authorised?” is not answerable from a log line that says a service account read a secret. You have to reconstruct intent from correlated timestamps, the kind of work that takes a week and produces a maybe.
Failure three: confused deputy
The agent is asked to investigate one namespace. Its credentials reach the fleet.
Nothing in the permission model connects the scope of the request to the scope of the capability. The task says one namespace. The token says everything. The only thing holding the agent inside the namespace is the instruction to stay there.
That is the confused deputy, and it is the oldest problem in this list wearing new clothes. The agent holds authority it is not currently entitled to exercise, and the thing deciding whether to exercise it is a model reading text. Any input the agent reads during the investigation gets a vote on that decision.
Last week’s issue had the paper that measures how well this works out. Ting Yan matched the security rules written in 481 public CLAUDE.md files against Claude Code’s documented built-in controls, and under the strictest standard 4.4% of those rules had a control that would enforce them. “Only look at this namespace” is one of those rules almost every time.
The fix is a token scoped to this investigation that expires when the investigation does. A better instruction will not get you there.
Failure four: availability
The least glamorous and the one most likely to page you first.
An agent that decides to enumerate a fleet will enumerate a fleet. It does not get tired, it does not notice it is on the fortieth cluster, and nothing in a read-only permission set stops it. Read calls cost API server capacity, and an agent in a reasoning loop can generate them faster than any human operator ever could.
You already know how to handle this. It is rate limits, quotas, timeouts and a hard cap on tool calls per task. The reason it gets missed is that it sits in the availability column while everyone is arguing about data. Put it on the list. It is the cheapest thing here to fix and the most likely to bite in month one.
The Containment Ladder
Four rungs. Every one is an infrastructure control. The ordering is what I would keep if you forget everything else, so here it is stated plainly enough to take straight to your own agent.
1. Scope the capability at the server, not in the prompt. Read-only is enforced by the tool server refusing to expose write operations, not by the model choosing not to call them. If deleting the instruction that says “read-only” changes nothing about what the agent can do, you have a control. If it changes behaviour, you have documentation.
2. Give the agent its own identity, and give that identity a human owner. Per-agent workload identity, its own RBAC, its own audit trail. Never a human’s borrowed credentials, and never one shared service account across instances. The owner matters as much as the identity: every agent identity should resolve to a named person who is accountable for what it does.
3. Treat egress as a capability grant. The agent reaches what you explicitly wired it to reach. Nothing else. Default-deny, with an allowlist of the hosts the task requires. This is the rung that contains failure one, and it is the same control that would have caught most of what the labs disclosed last month.
4. Put a human gate on every write path. Anything that mutates state leaves agent autonomy and enters an approved workflow. Not a confirmation prompt the agent can reason its way past. A separate system with a separate approval.
Notice what is not on this ladder. No prompt engineering, no guardrail model, no system message. Those are useful and they are not rungs. Every control that holds is infrastructure. Every control that fails is a prompt.
The gap I asked the room to help with
The one thing on this list I cannot hand you a control for.
There is no redaction layer between tool output and model context. When the agent reads a log, the whole log enters the context window. Every control on the ladder above governs what the agent can reach. None of them govern what happens to data after it arrives and before the model sees it.
I wrote up what that layer would have to do, why existing hooks and gateways stop short, and where I want the design attacked: The Missing Control: A Design Proposal for Tool-Output Redaction in AI Agents →
The timing on this improved while I was in London. MCP’s roadmap, replaced on 22 August, now lists tool result shape as a deliverable for this period, redesigning tools/call to sort out the mismatch between structured and unstructured output. That is exactly the boundary this proposal sits on, and it is open right now. If you have built something that does this, or you think the design is wrong, tell me. Reply, or find me in Amsterdam next week.
Tooling worth knowing
- SPIFFE/SPIRE for rung two. Per-workload cryptographic identity with short-lived credentials, which is what makes per-agent identity practical rather than a spreadsheet of service accounts. If you are standing up agents in Kubernetes and you already run a service mesh, most of this is already in your cluster. spiffe.io →
- SEP-1913 and SEP-3140, the two open MCP proposals on context provenance. 1913 adds trust and sensitivity annotations that escalate and never decrease It carries
roadmap/securityand it is the one moving. Both still unmerged. SEP-1913 → · SEP-3140 → within a session, the closest thing in the spec to a redaction primitive.
One thing to check this week
The attribution test. It takes ten minutes and it is the failure class nobody expects to fail.
Pick an agent you run. Open your audit log. Find an action it took yesterday.
Now answer four questions from the log alone. Which agent instance took this action. Which task it was part of. Which human asked for that task. Whether the action was in scope for it.
If you cannot answer all four, your audit log records that something happened and not who did it or why. That is not an audit trail, it is a timestamp. And the day you need it, you will need it under time pressure with somebody asking whether customer data moved.
The fix is rung two, and you can start it before you fix anything else on the ladder.
What I’m watching
→ The recording. fwd:cloudsec publishes talks after the event. I will link it when it is up, and I will be watching my own delivery more critically than the content.
→ Amsterdam, 17 to 18 September. AGNTCon and MCPCon Europe at the RAI, where I am speaking on what a year of breaking MCP tells builders. Different talk, same spine: every trust check I looked at compared a string to something nobody had issued, verified or bound to an identity. Say hello if you are there.
→ Next issue, Tuesday. MCP, ahead of Amsterdam. The 91.8% figure everyone is about to misquote, what the NSA actually wrote in May, and the STDIO argument nobody wants to take a side on.
If you run the attribution test and the log cannot answer all four questions, reply and tell me which one it failed on. I read everything, and the pattern across estates is more useful than any single answer.
Cheers, Amine