« Phase 08 · Warmup · Track Overview

Hitchhiker's Guide — Agent & Workload Identity

The 30-second mental model

A service account produces this audit line:

actor=svc-ai-platform  action=payments.release  amount=250000

"The platform did it." Nobody asked, nothing was bounded by a user's entitlement, and a leak is permanent and unlimited.

The alternative is four words, each a control an examiner will ask you to demonstrate:

Derived — from a verified assertion, never asserted by the caller. Narrowed — audience and scope shrink at every hop. Chained — the actor list is append-only and visible. Short-lived — seconds, so revocation is a timeout.

Which produces this instead:

sub=u-42  act=[orchestrator, payments-investigator]  aud=core-banking
scope=[payments.read]  exp=+90s

The claims, and the attack each one stops

ClaimSkip it and
issa rogue issuer's token is accepted
audthe confused deputy — a token for us works elsewhere
exp/nbfexpired credentials replay
jtione-time tokens are used twice
scopeexcessive agency
actyou cannot tell agent-for-user from user
cnfa stolen bearer string is sufficient

The claims nobody checks are the vulnerabilities.

The two classic JWT breaks, one fix

  • alg: none — the token says "unsecured", the verifier obeys.
  • Algorithm confusion — an RS256 verifier handed an HS256 token signed with the public key.

The verifier decides the algorithm. The token never does.

Plus: hmac.compare_digest, never == — a short-circuiting compare leaks the signature byte by byte.

The four rules of a token exchange

  1. Narrow the audience. Same audience = achieved nothing.
  2. Subset the scope. Never widen — make it structurally impossible.
  3. Append the actor, refuse a cycle. A→B→A is a loop across owners nobody can see whole.
  4. Shorten the life: min(requested, policy, parent's remaining). A derived credential outliving its parent is escalation in time.

Plus: not every token may be exchanged (may_delegate).

Delegation vs impersonation

subactAudit says
Delegationthe userthe agent chain"u-42 asked, O delegated, I acted"
Impersonationthe agenterased"the agent did it"

RFC 8693 nests act with the most recent actor outermost — the opposite of what most people assume. Check that before reading a chain in an incident.

OAuth 2.1 in one table

RemovedWhy
Implicit granttokens in the URL fragment leak via history and referrers
Password grantthe client sees the password
Tokens in query stringsURLs are logged everywhere
plain PKCEthe challenge is the verifier — protects against nothing
Prefix redirect matchingopen redirect → code interception

Added: PKCE mandatory for all clients; refresh tokens sender-constrained or one-time with rotation.

PKCE: challenge = base64url(sha256(verifier)). Mandatory for confidential clients too, because a client secret and PKCE defend against different attackers.

Refresh-token reuse is evidence of theft — revoke the whole family, not just that token.

ID token vs access token

Access tokenID token
Aboutwhat may be donewho the user is
audthe APIthe client

Sending an ID token to an API fails audience validation. This confusion is why people think OIDC and OAuth are the same thing.

SPIFFE in one paragraph

The workload presents nothing. The platform observes properties it can verify — node, namespace, service account, image — and issues a short-lived SVID carrying a spiffe://<trust-domain>/<path> identity. No secret to provision, rotate, protect or steal.

Two rules: all registered selectors must match (a subset lets any workload in the namespace claim the identity), and ambiguity is refused, not guessed. Federation across trust domains is always explicit.

One-liners

  • Restore base64url padding before decoding, or two thirds of tokens fail.
  • An agent gets the task's scope, not the user's.
  • Refuse, don't silently drop — a caller believing it has authority it lacks fails far from the cause.
  • Every NHI has a named human owner. The standard audit finding is credentials belonging to nobody.
  • Revocation latency = credential TTL. That is why 60 seconds is a design decision.
  • Sender-constraint makes theft insufficient — and it is nearly free once you have mTLS.
  • Entra's on-behalf-of is RFC 8693 delegation. You will configure it, not invent it.
  • Count your long-lived secrets and drive it to zero.

Vocabulary

AuthN / AuthZ · who / may they. Principal · the identity a decision is about — composite for agents. NHI · non-human identity. JWS / JWT / JOSE · signing, the token, the family. kid · which key signed it. act · the delegation chain. cnf · proof-of-possession binding. jti · unique id for replay detection. PKCE · proof key for code exchange. OBO · Entra's on-behalf-of flow. STS · security token service — the thing that performs exchanges. SPIFFE ID / SVID / trust domain / attestation / selector · workload identity. DPoP · demonstrating proof of possession. Bearer · whoever holds it, wields it.

War stories

"The platform released the payment." One service account for the agent fleet. Six weeks of actions attributed to svc-ai-platform, and remediation meant reconstructing user context from application logs.

The forwarded token. An agent passed its incoming token to core banking. Core banking did not check aud, so it worked — for eight months, until a penetration test noticed that any service holding a platform token could act against any other.

alg: none. A hand-rolled verifier read the algorithm from the header. A forged token with no signature at all verified successfully.

The credential that outlived the session. An exchange minted a one-hour token from a token with four minutes left. The user logged out; the derived credential kept working for fifty-six minutes.

The invisible loop. Agent A delegated to B, which honestly delegated back to A. No cycle check on the chain. Six hours, two owners, neither able to see the whole thing, and the bill was the first symptom.

The subset selector. A SPIRE registration keyed only on namespace. Every workload in that namespace could obtain the payments agent's identity — including a debugging pod someone left running.

The NHI nobody owned. An identity created during a proof of concept, still active two years later, with permissions to a production system, belonging to a team that had been reorganized twice.

The impersonation shortcut. Delegation was "too complicated", so the platform impersonated the user. Every downstream record showed the user acting directly, and there was no way to tell which actions a human had taken and which an agent had.

Beginner mistakes

  1. A service account for the agent fleet.
  2. Forwarding the incoming token to the next hop.
  3. Not checking aud.
  4. Reading alg from the token.
  5. == on a signature.
  6. Forgetting base64url padding.
  7. Prefix redirect-URI matching.
  8. plain PKCE.
  9. Sending an ID token to an API.
  10. Giving an agent the user's full scope.
  11. Silently granting less than requested.
  12. An exchange that does not narrow.
  13. A derived token outliving its parent.
  14. No cycle check on the chain.
  15. Impersonation because it is simpler.
  16. Subset selector matching in attestation.
  17. Guessing when two registrations match.
  18. Implicit cross-domain trust.
  19. An NHI with no human owner.
  20. Assuming suspension is instant.

What "good" sounds like

"Four properties: derived, narrowed, chained, short-lived. The user's token is audienced to the platform; every hop after that is an RFC 8693 exchange rather than a forward, because forwarding is the confused deputy and a service account erases the user. Each exchange narrows the audience, subsets the scope to what this task needs rather than what the user may do, appends the actor — refusing a cycle — and takes the minimum of requested, policy, and the parent's remaining lifetime. Workloads get SPIFFE identities from attested properties instead of secrets, and credentials are bound to a key so theft alone isn't enough. Revocation latency is the TTL, which is why I'd run sixty seconds rather than an hour. And the chain is always derived from a verified token, never from the request — a callee that can assert its own position can erase a hop, and it would be the interesting one."