im·a·cto

From the log

Back to the log

A Door, Not a Keyring

Tests, CI, types, review: every rail you trust governs what gets written. None of them governs what gets run, and the fleet doesn't need a pull request to change your Tuesday.

The deploy has been stuck for eleven minutes and nobody can say why. You ask the agent. It reads the pipeline config, reasons about it correctly, and tells you the thing it can’t do: it can’t see the cluster. So you paste a token into the config, because there’s one sitting right there in your shell and it works on everything. Five seconds. The agent finds the wedged pod in under a minute, and the whole exchange feels like the future arriving on schedule.

Nobody ever scoped that token, including you. It’s the one that always works. That’s exactly why it was in your shell and not somewhere you’d have to go get it.

the rails only ever covered the diff

I’ve written down how I operate more than once, and I still believe it: autonomy with guardrails. Give people and agents room to move without asking permission, and build rails good enough that the speed is survivable. Tests, CI, types, review, a merge train that proves every commit before it lands, a canary that catches what the tests didn’t.

Now look at what those rails are actually made of. Every one of them is a check on a diff. They read a proposed change to a file and decide whether it’s allowed to become real. That’s a genuinely strong model, and it covered the entire attack surface back when the only way for an engineer to change production was to change the code that ran there.

There is no diff for a command.
Every rail you built reviews what gets written. Nothing you built reviews what gets run.

The whole point of GitOps is closing that gap on purpose. Declare the desired state in git, let a controller reconcile reality to match it, and the properties fall out: deploys are merges, rollbacks are reverts, the audit log writes itself. Terraform for the building, git for the lights. I build toward that because it means the system can’t get into a state nobody can explain or undo.

Then you hand an agent a console credential, and you’ve opened a second entrance to the same building. Nothing declared, nothing reviewed, nothing written down. The reconciler will eventually notice the drift and quietly put it back, which is either the save or the second outage depending on what got changed.

the risk didn’t change, the hesitation did

A person holding your admin token uses it a handful of times a week, and pauses before the ugly ones. Not because policy told them to. Because a human being typing a command against production at 4:50 on a Friday has a small physical reaction to it that has saved more systems than any control I’ve ever written.

That pause is not a real safeguard. You can’t put it on an architecture diagram, and I’d never design around it. But it was carrying more load than anyone admitted, and an agent doesn’t have one. By design. Not hesitating is the feature you asked for when you stopped approving every step.

So the failure mode here isn’t a rogue agent. It’s ordinary competence pointed at the wrong environment. The agent asked to clean up unused resources does exactly that, correctly, in the project where “unused” meant “used once a quarter by finance.” The diff for that operation doesn’t exist, the review that would have caught it never ran, and unlike a migration, you won’t even get a git history to read afterward.

the deputy has your name on it

There’s a second thing, and it’s the one I think most teams are still filing under “later.”

Your agent reads. That’s the job. It reads issues, logs, error payloads, a vendor’s changelog, the README of a package four levels down the tree. To the model, everything it reads arrives as tokens in the same window as your instructions, and the boundary between “content I was asked to consider” and “instruction I should follow” is a convention, not a wall.

This is the confused deputy problem with a much better vocabulary. A program with more authority than the party asking it to act, doing the asking party’s bidding with its own permissions. Norm Hardy named it in 1988 with an example about a compiler that had write access to a billing file, and a user who got it to overwrite that file by handing it the path as an output name. The compiler wasn’t compromised. It did its job, with its own rights, on someone else’s instruction.

Simon Willison’s framing is the one I’d hand to a team today: the lethal trifecta is an LLM system with access to private data, exposure to untrusted content, and a way to send data back out. All three, and the system is exploitable by anyone who can get text in front of it. His read is that exfiltration is usually the cheapest leg to cut without gutting the tool. In my own systems I’d add that the first leg is the one I have the most direct control over, because I decide what the credential can reach, and I decide it once, at design time, for every task the fleet will ever run.

An agent working in your repo with your token has all three legs before breakfast.

a door, not a keyring

Here’s the shift. Access stopped being a checklist item you handle near the end, next to the pen test and the SOC 2 questionnaire. What the fleet can do is a design decision at the same altitude as where the state lives, and it’s made in exactly the same way: on purpose, early, by someone who understands the consequences, or by accident and forever.

At Phoneware I put MCP servers over the platforms underneath so the fleet can drive them. I’ve talked about that as an API argument, one surface for humans and agents both, and that’s true. It’s also where the permission model got decided, and I don’t think I appreciated that clearly enough when I started. The tools I wrote are the operations the fleet can perform. Everything else does not exist to it, not because a policy denies it, but because there’s no shape in the interface that produces it. That’s a door. It has a width, I chose the width, and I can tell you what fits through it.

A shell plus my credentials is a keyring. A keyring has no shape. You find out what’s on it when something opens.

The clearest public illustration I know is CircleCI’s January 2023 incident report, and the useful part isn’t the breach. Malware on one engineer’s laptop stole a valid, 2FA-backed SSO session. That employee could generate production access tokens as part of their regular duties, which is to say the access was legitimate, in scope, and doing its job. The attacker used it to exfiltrate customer environment variables, tokens, and keys, and every customer on the platform had to rotate every secret they’d stored there.

Read their remediation list. Alongside the detection work, they committed to making system permissions more ephemeral, “severely restricting the target value of any tokens gained from a similar incident.” That’s the sentence. They didn’t promise to be harder to phish. They shrank what a stolen credential is worth. Do that work before the incident and it’s architecture. Do it after and it’s a line item in a postmortem you’re reading out loud to customers.

what it costs, because it isn’t free

Scoping is real work with an invisible payoff, which puts it in the same unlovable category as the outage that didn’t happen. Nobody sends a note thanking you for the blast radius that stayed small.

And there’s a failure mode worse than not doing it. Scope too tight, ship no path for the legitimate exception, and at 11pm somebody who needs to get unstuck pastes the wide credential into a config file to get through the night. Now you have an unscoped token in a repo and a written policy you believe in. A door too narrow gets propped open with a rock, and the rock stays.

Read-only isn’t a free pass either. An agent that can read everything and reach the network is two legs of the trifecta and a plausible third. The question is never just whether it can write.

  1. Inventory what the fleet can already reach. Every credential in its environment, what it opens, in which environment, issued to whom. This is tedious graph work the fleet is genuinely good at, so have it produce the list and then read the list yourself. The surprise is never the token you thought about.
  2. Give the fleet its own identity. Its own account, its own short-lived credentials, one set per environment. If the audit log answers “who did this” with your name, you don’t have an audit log. You have an alibi.
  3. Make capabilities arrive through a surface you wrote. A tool that exposes the four operations you meant is a better boundary than a shell that exposes all of them, and it’s a boundary you can read, test, and version. The interface is the permission model. You get to design it.
  4. Keep production changes on the front door. If a change can be a merge, make it a merge: reviewed, canaried, revertible, logged by construction. Reserve direct access for reads and for the genuine emergency, and make the emergency path loud enough that using it is a decision somebody remembers.

Go back to the stuck deploy. It’s still stuck, the agent still can’t see the cluster, and pasting the token is still the fastest way through. I’m not going to pretend I’ve never done it. What I’d ask before the paste has nothing to do with trusting the agent, because I mostly do, and trust was never the variable here. The question is what that token still opens at 3am on a Thursday, while I’m asleep and it’s reading an issue filed by somebody neither of us has met.

What’s on the keyring you handed over? Could you name it right now, without looking?

Jason Waldrip is a fractional CTO and CAIO through The Bushido Collective, working with founders drowning in AI-generated code and teams scaling past the leadership that got them here. If your agents are running on a human’s credentials and nobody can say what those credentials reach, that’s a good afternoon’s work and a better conversation. Work with me.

Written with AI assistance; the frame and the judgments about what matters are mine. The CircleCI details come from their own published incident report rather than a summary of it, and the trifecta framing is Simon Willison’s, linked above.