Skip to content
Go back

One Token for Everything Is Still One Token

Eiffel Tower, Paris
Eiffel Tower, Paris

TL;DR


The third post in this series made the case for scoping a service account token to the workflow’s actual purpose rather than to “the project” or “everything I’ve got” — a ci-deploy token that can see two items, not a master grant that happens to be wearing a service-account costume. The argument is right. Here’s what it looks like to actually live inside it — on a box that runs agents, not a CI runner that exits when the job finishes.

The difference matters because the failure mode is different. A CI token’s exposure window is the length of a job. An agent box’s service account sits there, live, for as long as the box is up — reachable by anything the agent can be talked into doing, on every run, indefinitely. The scoping argument isn’t theoretical there. It’s the only thing standing between “an agent got confused once” and “an agent got confused once, near a token that could read everything.”

What scoping actually requires you to give up

The instinct, even after you’ve accepted the scoping argument in principle, is to scope generously “to be safe” — one vault per box rather than one per purpose, on the theory that fewer vaults means less to manage. That’s the same mistake at one remove: a box that runs five different kinds of agent work ends up with one token that can read all five kinds of secret, and the blast radius is back to “everything this box touches,” just relabeled.

The harder, correct version: scope to the purpose, even when the purpose is small and the setup feels like overkill for it.

Scope the vault, then scope the token

Putting the principle into practice is structural, not procedural. Don’t build a token with broad reach and trust it to behave — build a vault with narrow contents, and a token that can only ever see that vault. The token stops being something you have to trust, and becomes something that’s simply incapable of the wider thing.

The concrete version of this: a flat secrets file on a remote box held five mixed-purpose values — a search API key, an email API key, two internal dispatch secrets — sitting alongside config values that weren’t secrets at all. Anything that could read that file could read all five, regardless of which one a given task actually needed. The file was the vault, and it had no concept of scope.

The fix: a new 1Password vault holding exactly those five items and nothing else. A service account scoped to read-only access on that one vault — not “all vaults,” not “everything I have.” The flat file reduced to a single line: the token that opens that one narrow door, with a comment explaining what it can and can’t do and where the actual secrets live now.

Compromise that token, and an attacker gets five bounded values scoped to one box’s workflows — not the keys to everything the vault’s owner has ever stored.

Testing the boundary — on purpose

The first attempt to populate the new vault came from the scoped token itself, and it failed:

[ERROR] You do not have permission to perform this action

That’s the system working exactly as configured. Read-only means read-only — including for the person who set it up, if they’re using the scoped credential to do it. Population had to happen from a separate session, on a separate machine, with a human signed into the full account behind it.

That friction is the point, not a bug to engineer around. It draws a hard line between two roles that are easy to blur when one person is doing both: the place that runs agents, which only ever needs to consume credentials, and the place that authors credentials, which needs write access and a human in the loop. Collapse those into the same session and you’ve quietly rebuilt the single point of failure you just finished scoping away. Keep them separate, and a compromised agent runtime literally cannot rewrite its own permissions — there’s no code path for it.

Making the tool vault-transparent

None of this required touching the tool that actually uses the search API key. It already checked an environment variable first and fell back to a local config file second — written months before any of this existed. The sixth post in this series covers exactly this shape: wrap the launch command in op run --env-file=..., and the vault reference resolves into the process environment at the moment the tool starts. The tool reads process.env.API_KEY exactly like it always did. The only thing that changed is where that value originates — and the tool has no way to know, or care.

Wrap what you didn’t write. Don’t fork it to teach it about the vault.

What’s still true after all of this

Scoping the vault and the token shrinks the blast radius from “everything” to “exactly what this one workflow needs.” It’s a real reduction — five bounded values instead of an open door to the whole account.

It does not make the secret disappear. It still lands in the tool’s process environment for the life of that process, the same as it always did — just for a narrower set of values, reachable by a narrower set of things. Smaller blast radius. Not zero. That’s the next question worth sitting with, and one worth coming back to.

So what

Agreeing that a token should be scoped to its purpose costs nothing. Building the vault that makes the wider grant impossible — rather than merely discouraged — costs an afternoon, a permission error you have to sit with instead of route around, and the discipline to keep “the box’s vault” from quietly becoming “the project’s vault” the next time something new needs a credential.

That’s what the principle looks like once it has to survive contact with a box that’s up at 3am running something you didn’t watch it start. Five bounded values, one read-only token, a permission error that fired exactly when it should have. Boring. Which is the point — boring is what it looks like when the blast radius was decided in advance instead of discovered during incident response.


This post extends the third and sixth posts in the eight-post series on 1Password as infrastructure — applying the service-account and op run patterns with a narrower scope than “one token for everything.” The eighth post goes further: a scoped, read-only token is still a value that lands in the tool’s process. Brokering removes it from that process entirely. Start from the first post.


Share this post on:


Previous Post
Go-to-Market Was a Relay Function. AI Makes It a Decision Function.
Next Post
Your Agent Doesn't Need the Secret. It Needs the Result.