← The ADLC library
Governance & security · 4

Least privilege for agents, and what they should never be able to do

Most agent deployments inherit the permissions of whoever set them up, which is the single most consequential unexamined decision in the whole stack. Some capabilities should be structurally unavailable regardless of how well the agent behaves.

Ask a team how their coding agent authenticates and you will usually get one of three answers. It uses a personal access token belonging to whoever set it up. It uses a service account created for CI several years ago that has accumulated permissions the way a garage accumulates boxes. Or, occasionally and more honestly, nobody is quite sure.

This is not carelessness. It is the ordinary result of adopting a capability quickly, with the permissions that were nearest to hand, in an environment where getting a properly scoped credential requires a ticket and four days. Everyone does it, including people who know better.

It is also the decision that determines the ceiling on how bad your worst day can be, and it deserves to be made deliberately.

Reasoning about capability rather than trust

The instinct is to frame this as a trust question: how much do we trust the agent? That framing produces bad answers because it invites a judgement about competence, and competence is not the relevant variable.

The useful frame is the one used for any other automated actor: what is the worst outcome reachable from this set of permissions, and is that outcome acceptable? The answer does not depend on how good the agent is. It depends on what it can reach, and it holds equally when the agent is behaving correctly, when it is confused, and when someone has manipulated it through content it consumed.

That last case is the one that changes the calculus, and it is worth being precise about it. An agent reads things: issue descriptions, code comments, documentation, dependency READMEs, error output, web pages. Any of those can contain text written by someone who is not your colleague. The security question is therefore not “will the agent misbehave” but “what happens if some of its input was written by an adversary,” and the answer must be acceptable, because you cannot reliably prevent the input.

Read the repositoryBroadly safe and necessary. The residual risk is exfiltration, not modification.
Write to a branchSafe by design. A branch is a proposal, and proposals are reversible and reviewable.
Open a pull requestSafe and desirable. This is the intended output shape of the whole system.
Merge to a protected branchRequires the same controls as a human doing it. No exception for automation.
Deploy to productionOnly through the existing pipeline with the existing gates. Never as a direct capability.
Touch production dataNo. There is no version of this that is worth the exposure.
A capability ladder. Everything above the middle line is normal engineering. Everything below needs a specific argument, and the last rung does not have one.

The list that should be structurally impossible

Some capabilities should not be denied by policy, configuration or instruction, but by the credential simply not existing. The distinction matters: a policy is a thing that can be misconfigured, an instruction is a thing that can be overridden by cleverer input, and an absent permission is a thing that cannot be talked into existence.

Production data access. Not read, not write. The value of an agent reading production data is almost always achievable with synthetic or anonymised data, and the downside is a category of breach that ends careers. If a debugging workflow genuinely requires production data, that workflow needs a human with an audited session, which is what it needed before agents existed.

Credential and secret retrieval. An agent with access to a secrets manager can retrieve, and anything it retrieves can appear in output, logs, a commit, or a message to a model provider. Secrets should be injected into runtime environments the agent does not read from, and the agent should work with references rather than values.

Modifying its own guardrails. The pipeline configuration, the criteria definitions, the branch protection rules, the required checks: an agent that can edit these can remove the constraints on itself. This one is frequently violated by accident, because the pipeline configuration lives in the repository the agent is allowed to modify. It requires a deliberate carve-out and most teams have not made one.

Approving its own work. Whatever review or approval step exists, the identity that produced the change must not be the identity that approves it. This is ordinary segregation of duties and it is the control auditors will look for first.

Direct infrastructure mutation. Deleting a bucket, scaling a cluster, rotating a key, changing a security group. If infrastructure is managed as code, the agent proposes a change to the code and the existing pipeline applies it, with the existing approvals. The agent never holds the cloud credential.

Outbound communication to arbitrary destinations. An agent that can make network calls to anywhere can send anything anywhere. Egress should be allow-listed. This is the control that turns a prompt injection from a breach into a failed attempt.

Denied by instruction

  • "Do not access production"
  • Depends on the agent following it
  • Defeated by adversarial input
  • No evidence it held
  • Fails silently and invisibly

Denied by absent capability

  • No credential exists to use
  • Independent of agent behaviour
  • Adversarial input changes nothing
  • Attempt is logged and refused
  • Fails loudly at the boundary
The same restriction, enforced two ways. Only one of them is a control, and only one of them produces evidence.

The identity question

A recurring argument: should an agent have its own identity, or act as the human who invoked it?

Acting as the human is easier and it is wrong. It makes the audit trail useless, because every action appears to be performed by a person who may not have known it was happening. It grants the agent the union of that person’s permissions, which for a senior engineer is very close to everything. And it makes revocation impossible to do surgically, because disabling the agent means disabling the person.

Distinct identity per agent, or per agent workflow, costs more to set up and gives you three things you cannot otherwise have. Permissions scoped to the task rather than inherited from a person. An audit trail where the actor is unambiguous. And a revocation switch you can pull at three in the morning without locking out an engineer.

The subtlety is that you also want to keep the human association, because “the agent did it” is not an accountability answer. The record should say: this agent identity, acting on this instruction, from this human, against this ticket. Two identities in the chain, not one.

actor:        agent://build-bot/refactor-worker
on_behalf_of: user://dpatel
instruction:  ticket PLAT-4471
scope:        repo:payments-api  branch:write  pr:create
denied:       secrets:read  deploy:*  data:production
chain:        intent -> generation -> criteria -> review -> merge
Both identities are recorded. The agent is the actor, a named human is accountable, and the denied set is stated explicitly so an auditor can see what was structurally impossible rather than merely discouraged.

Scoping to the task, not the role

The next refinement, and the one most teams are not ready for yet: permissions bounded by the work rather than by the actor.

A human engineer holds broad standing access because re-granting it per task would be intolerable friction. An agent has no such constraint. It is invoked to do a specific thing against a specific ticket, and its credential can be minted for that invocation, scoped to the repositories and paths the work touches, and expired when it finishes.

This is a genuine improvement over the human baseline rather than a compensation for a new risk, and it is worth saying so. Short-lived, narrowly scoped, task-bound credentials are what we always wanted and could never impose on people. The infrastructure for it exists in most modern platforms and is under-used because the friction never justified it before.

Where this breaks down

Scoped permissions require knowing the scope in advance, and you often do not. A change that turns out to need a touch in a shared library will fail against a narrowly scoped credential. The failure is safe but it is friction, and friction produces the predictable response of widening the scope permanently. The workable pattern is a narrow default with an explicit, logged escalation path, and the escalation must be genuinely fast or it will be routed around.

Egress allow-listing is harder than it sounds. Package registries, documentation sites, and internal services form a long tail, and the list is never complete. Teams that start strict often loosen to a point where the control is nominal. Being honest that you have a partial control is better than believing you have a complete one.

The infrastructure-as-code path assumes discipline you may not have. “The agent proposes and the pipeline applies” is only a control if the pipeline actually has gates and if there is no manual side channel. In most organisations there is a manual side channel, and it is usually held by the people most likely to be under time pressure.

None of this addresses the model provider. Everything here is about what the agent can do in your systems. It says nothing about what leaves the building in a prompt, which is a separate control surface with separate answers and is the subject of the next two pieces.

Over-restriction has a real cost and it is paid in shadow tooling. An agent constrained to uselessness leads engineers to run unconstrained ones on their laptops with their own credentials, which is strictly worse than a slightly over-permissioned managed agent. The security posture that matters is the one people actually use.

The takeaway

The permission set is the highest-leverage security decision in an agentic pipeline, and in most organisations it was made implicitly by whoever moved first.

Reason about reachable outcomes rather than trust. Give agents their own identities with a named human in the chain. Make the dangerous capabilities structurally absent rather than discouraged, because instructions are defeated by adversarial input and missing credentials are not. Scope to the task where you can, since that is the one place agents can be held to a standard humans never could.

If you take one thing into next week: find out what credential your agents are actually using and what that credential can reach. In most organisations that is a twenty minute investigation and an uncomfortable afternoon.

The next piece takes up the other half of the problem, which is not what the agent can touch in your systems but what leaves them: data residency when the model lives somewhere else.