« 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
| Claim | Skip it and |
|---|---|
iss | a rogue issuer's token is accepted |
aud | the confused deputy — a token for us works elsewhere |
exp/nbf | expired credentials replay |
jti | one-time tokens are used twice |
scope | excessive agency |
act | you cannot tell agent-for-user from user |
cnf | a 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
- Narrow the audience. Same audience = achieved nothing.
- Subset the scope. Never widen — make it structurally impossible.
- Append the actor, refuse a cycle. A→B→A is a loop across owners nobody can see whole.
- 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
sub | act | Audit says | |
|---|---|---|---|
| Delegation | the user | the agent chain | "u-42 asked, O delegated, I acted" |
| Impersonation | the agent | erased | "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
| Removed | Why |
|---|---|
| Implicit grant | tokens in the URL fragment leak via history and referrers |
| Password grant | the client sees the password |
| Tokens in query strings | URLs are logged everywhere |
plain PKCE | the challenge is the verifier — protects against nothing |
| Prefix redirect matching | open 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 token | ID token | |
|---|---|---|
| About | what may be done | who the user is |
aud | the API | the 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
- A service account for the agent fleet.
- Forwarding the incoming token to the next hop.
- Not checking
aud. - Reading
algfrom the token. ==on a signature.- Forgetting base64url padding.
- Prefix redirect-URI matching.
plainPKCE.- Sending an ID token to an API.
- Giving an agent the user's full scope.
- Silently granting less than requested.
- An exchange that does not narrow.
- A derived token outliving its parent.
- No cycle check on the chain.
- Impersonation because it is simpler.
- Subset selector matching in attestation.
- Guessing when two registrations match.
- Implicit cross-domain trust.
- An NHI with no human owner.
- 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."