Table of Contents

The previous post walked through an agent flow inside an enterprise — a Slack bot at the front, a gateway in the middle, domain services underneath. For the gateway layer I wrote one line and moved on: authentication, caller attribution.

There was something there worth digging into: what makes the gateway matter is that it is the place where identity exchange happens. This post picks up from there — why it has to work that way, and why agent identity matters.

Back when I was designing that in-house agentic AI, the first instinct was to ship an MVP fast, which meant skipping a lot of things that mattered. One of the big ones was agent identity — how does an AI agent obtain a valid identity, and how does anything verify it?

Come back to the Slack bot. There is a question we have no way to answer: when the bot queries the status of a cloud backend service on behalf of some engineer, how does anything know who that is? Can I find out? If two different people are using the bot at the same time, can the logs tell them apart? And if the agent calls another agent, how does that line get connected?

When the agent we are building is a chat bot or does lookups (read-only), the damage it can cause is relatively contained — e.g. a wrong answer, data leaking out by accident. But once it is given write permission — restarting machines, changing configuration, sending messages, deploying — this is what this post is about.

The auth flow has a new actor in it

The traditional authorization model is easy enough to follow. A user logs in, the system knows who they are, and it decides whether they can do this. Service-to-service is much the same: a service account with a set of permissions attached, and that’s roughly it.

But once AI agents show up — durable agentic AI in particular — that model has a new role in the middle:

Alice (human engineer)  ── delegates ──>  Agent (AI assistant)  ── calls ──>  MCP tool / other tools

Consider a use case. Alice has permission to reboot a remote device, and she wants to. But in this delegation scenario, the agent should not be rebooting the device on its own standing permissions — it should be acting on the authority Alice delegated this one time.

So when the request lands at the tool, the question to ask is not:

Does this agent have permission to reboot the remote device?

It is:

Did Alice authorize this agent, within this window, to reboot this particular remote device?

Worth being careful here: those two sentences have very different effective scopes. The first grants the permission to the agent. The second is a delegation — only the slice the agent needs this time, and it expires.

That is the whole point of agent identity: we need to know where the agent’s authority actually comes from.

Why not just hand it a service account?

The most intuitive thing to do is exactly that: create a service account (svc), attach every permission the agent might need, done.

This absolutely works, and it is extremely convenient at the start. It is how I stood things up myself.

The problem is that four things grow out of it over time.

One: the audit log cannot say who. That is why I left an email/owner field in the gateway. The main reason was to answer one question — who actually authorized this action.

Two: the permission is standing, not attached to the task. To be able to reboot a remote device, the agent was given the capability “can reboot machines.” If a second later it does something completely unrelated, it still has that permission, and it can reboot the remote device at any time.

Three: the blast radius can widen to everything that account can do. When the agent is affected by prompt injection, with nothing else constraining it, the affected scope can widen to the full set of permissions the service account had all along.

Four: the policy decision can be perfectly correct while the name it was fed is fake. This is the easiest one to miss, and it gets its own section later.

Identity is not one thing — it’s four questions

I did not look at agent identity very closely at first. What turned out to be useful was taking the user > agent > tool flow apart and seeing how many distinct questions were hiding inside it.

At least four:

QuestionWho answers it
0Who connected? (which end user / agent is this)OAuth / OIDC, mTLS
1Which program is this? (is the running workload the approved one)SPIFFE / SPIRE, attestation
2Who does it represent? (is that person’s grant present, and still valid)An authorization service; RFC 8693’s act is what writes the relationship down
3Is this action approved? (is this action actually inside the granted scope)A policy engine such as OPA / Cedar

Blending them together is where designs most easily go wrong.

The most common version of this: “so do we use SPIFFE or OAuth token exchange?” The question itself is not precise enough.

SPIFFE answers question 1 — which workload this is. RFC 8693 handles how question 2 is expressed — how to write “A is acting on behalf of B” into a token. Both questions need answers, but you don’t necessarily need both technologies at once, because “I am sure which program you are” and “I am sure you are allowed to do this right now” are simply not the same thing.

The other, more common confusion is tying question 0 to question 2. These are:

  • 0 is authentication: the party talking to me right now (the agent).
  • 2 is delegation: that party says it represents Alice, and I hold trustworthy evidence that Alice really did grant it.

So what does the ideal state look like?

If all four above are genuinely answered, a single tool call should have roughly these properties.

Verified every time. Not “logged in once, trusted for the whole session.” Credentials expire and get revoked, and agent tasks often run for hours, so having verified once earlier does not mean it still holds now.

Authority is delegated, and can only narrow (least privilege). Alice grants “reboot device group A”; when the agent delegates to a sub-agent, the sub-agent can only receive something narrower or equal — it must not suddenly become “reboot every machine.” That property is called attenuation. And it has to fail closed: when you cannot tell whether the scope widened, assume it did.

Time-bounded. The grant states when it stops being valid. If the work isn’t done by then, ask again — rather than being permanently legitimate because this task was legitimate once.

Revocable. And revoke cannot just mean flipping a column in a database. Derived credentials, caches, and other delegation state all need to go away when revocation happens.

Decided at the tool boundary, by code. Not “please don’t do anything dangerous” in a system prompt, but a deterministic piece of code outside the model that only answers Yes or No.

Not bypassable. This rule matters more than all of the above. If the agent runtime has another path that reaches a tool without going through the checks described here, that is a security issue.

Audit logs must distinguish strength of evidence. “The caller on this request was X” and “this delegation chain claims it originated with Alice” are not facts of the same strength. Don’t end up stuffing both into the same user field.

What a single tool call should pass through

Mapped onto the request path, it looks roughly like this. Some checks can be completed up front by a trusted authorization service, with the tool verifying what that service issued:

 Client (agent or app)
       |
       |  presenting its own credential
       v
 --------------------------------------------------
  1. Authenticate the caller            <-- question 0
       verify signature / issuer / audience
       verify algorithm (against the server's allow-list,
         never just what the token claims)
       verify validity window
       is this subject someone I serve
 --------------------------------------------------
  2. Verify the workload credential     <-- question 1
       is this program the approved one
       is the attestation evidence sound
 --------------------------------------------------
  3. Resolve the authority chain         <-- question 2
       which grant does this caller correspond to
       is the grant from a trusted source, is the
         delegation relationship still valid
       for designs that verify hop by hop, was the
         whole chain actually checked
       does each hop's effective authority stay within
         the previous hop's (attenuation)
       are this action and this resource inside the scope
 --------------------------------------------------
  4. Policy decision                     <-- question 3
       allowed(who, action, resource)
 --------------------------------------------------
  5. Actually call the tool
 --------------------------------------------------
  6. Write the audit log
       verified caller / claimed principal / tool / result

One thing here I had wrong myself: RFC 8693’s act can record who is currently acting for whom, and nested act can preserve a whole chain of history. I assumed that chain of history was the answer to step 3 — then went and read the spec:

For the purpose of applying access control policy, the consumer of a token MUST only consider the token’s top-level claims and the party identified as the current actor by the act claim. Prior actors identified by any nested act claims are informational only and are not to be considered in access control decisions.

So that chain is there for humans to read afterwards, not for making the decision in the moment. The attenuation mentioned in step 3 is something you have to design and enforce yourself.

Three places in here deserve particular care.

Authentication goes first

The order of the checks is itself part of the defense.

Suppose a request comes in, you parse the tool name first, return “no such tool” when it isn’t found, and only start authenticating when it is. Those two responses together are an oracle: someone who has not even passed authentication can try one name after another and slowly enumerate which capabilities this system exposes. So even “does this tool exist” is sometimes not something to reveal before authentication.

The policy decision

This is where I think it goes wrong most easily. Everyone now agrees authorization cannot be left to the model, so it ends up written as a deterministic decision:

allowed(
  who      = "alice",
  action   = "reboot machine",
  resource = "group A"
)

Entirely the right direction. But by the time execution reaches that line — where did that "alice" come from?

If it came from conversation history, session state, or anywhere else the model can write to, then it is an input that may have been altered long before this point — the source is untrusted. Prompt injection does not need to defeat the door; it only needs to change the name on the pass.

So step 1 establishes the caller and step 3 establishes who it represents. Both identities fed into policy have to come from trusted authentication and authorization data, and be escorted all the way to the moment the tool is actually called, with no chance for the model to rewrite them in between.

”Narrower permission” is harder to define than it sounds

When an agent delegates to a sub-agent, the sub-agent should not receive more authority. Conceptually trivial.

Writing the enforcement is not, because first you have to define what narrowing even means:

  • Resource: does https://api.example/projects/app include .../app/issues? What about .../apple? The second one clearly should not count, so a string prefix isn’t enough — you have to compare on path separators.
  • Action: is update_issue narrower than issue.*? Then how is the action namespace defined?
  • Condition: the parent says “business hours only”; the child says nothing. Is that the same, or wider?

If the child does not inherit the parent’s conditions, the answer to the last one is wider. What has to be compared is the effective authority in the end, not just what the child’s document says.

So attenuation does not end with “the child scope must be a subset of the parent scope” — you have to actually define what subset means. And the principle stays the same: fail closed — if you cannot tell, refuse.

What this does not solve

A few boundaries, stated deliberately:

A stolen bearer token still works. The most intuitive way to do step 1 is to accept a signed token and verify it, and that only solves half of it — a stolen token, before it expires, presented from another machine, may well pass the same checks. Actually binding the credential to its holder needs a sender-constrained mechanism such as DPoP (RFC 9449) or mTLS (RFC 8705).

A task dies halfway through — how does it get its authority back? Agent tasks often run for hours, so everyone wants durable execution in the runtime. But if the runtime’s recovery path assumes authority is something the environment provides, and is relatively stable, this is where it runs into trouble. Once authority becomes a document that expires, that assumption no longer holds. After a restart, the document in hand may already be expired, and the person who granted it is no longer online. Asking for authority again, extending automatically, or stopping to wait for a human — all three paths carry different costs.

The audit log is written by the agent itself. If the runtime is compromised, it is simultaneously the author of its own history. No amount of identity-chain completeness solves that.

There is still no complete standard for agent identity. Every piece described above has a mature building block: OAuth, SPIFFE, policy engines, sender-constrained tokens, workload attestation. But assembling them into “agent identity” as a whole — how delegation is represented, how attenuation is compared, what to do about long-running tasks, how trust crosses runtimes, and so on — has no single accepted standard in the industry today.

The middle two — authority expiring mid-task, and the audit log’s author being the thing under audit — are not solvable at the identity layer. The next post picks up from here, on why the layer that executes and the layer that authorizes have to be separate.

References

Foundations of identity and authorization:

Workload identity:

Policy enforcement:

  • Open Policy Agent: externalizes authorization decisions into testable policy
  • AWS Cedar: another policy language, designed with analyzability in mind

Between agents and tools:

  • MCP Authorization specification: the HTTP transport has an OAuth 2.1-based authorization spec (OPTIONAL, explicitly not applicable to stdio). It governs the client-to-server leg, forbids token passthrough outright, and names the confused deputy. Full workload identity, multi-hop delegation, and per-task policy still need to be designed on top
  • MCP 2026-07-28 spec notes: the reasoning behind moving the protocol core toward stateless

Further reading: