IV.6IV · The protocol layerdefense
Agent identity & access (NHI)
An agent is a non-human identity that acts with real authority, so its permissions are its blast radius - this page treats each agent as a managed, least-privilege identity across the whole provision-to-deprovision lifecycle.
Concretely, that non-human identity (NHI) holds tokens, calls APIs, touches data, and triggers actions. OWASP puts it bluntly: an AI agent is an execution principal, closer to a privileged workload than a conversational interface. NHIs already vastly outnumber human identities and are the least-governed credentials in most estates; agents make it acute because they are numerous, dynamic, and act autonomously on untrusted input. The OWASP Agentic Top 10 (III.4 · Persistence & propagation) cross-maps directly to the OWASP Top 10 for Non-Human Identities - over-privileged NHIs, secret exposure, long-lived credentials, and reused identities are the root causes that turn agent risks into incidents.
# 1. OAuth token request with resource indicator (RFC 8707) -> aud-bound, 5-min TTLcurl -s -X POST https://<idp>/oauth2/token \ -d grant_type=client_credentials \ -d client_id=<agent-client-id> \ -d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \ -d client_assertion=<signed-jwt> \ -d resource=https://crm.internal/api \ # RFC 8707: binds the aud claim -d scope=records:read# -> access_token has aud=https://crm.internal/api, exp=+300s; unusable at any other API
# 2. On anomaly: revoke the identity, do NOT restart the host (III.3 containment)curl -s -X POST https://<idp>/oauth2/revoke -d token=<access_token> \ -d token_type_hint=access_token# and disable the workload identity so re-issue is blocked, e.g.:aws iam update-assume-role-policy --role-name agent-<id> --policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Principal":"*","Action":"sts:AssumeRole"}]}'aws iam delete-role-policy --role-name agent-<id> --policy-name standing-access{ "sub": "agent://crm-summarizer", "aud": "https://crm.internal/api", "scope": "records:read", "act": { "sub": "user:alice" }, "exp": 1751371500, "iat": 1751371200 }flowchart TB PROV["Provision: per-agent NHI<br/>not a shared / static key"] --> AUTH["Authenticate<br/>mTLS + OIDC /<br/>workload identity"] AUTH --> AUTHZ["Authorize: least-privilege,<br/>task-scoped +<br/>on-behalf-of user"] AUTHZ --> ACT["Act + audit every action"] ACT --> DEPROV["Rotate and de-provision<br/>kill orphaned identities"] DEPROV -.->|"no standing super-credentials"| PROV classDef d fill:#0f1a18,stroke:#5bd1c5,color:#bdeee2; class PROV,AUTH,AUTHZ,ACT,DEPROV d;
On-behalf-of is the control everything hinges on: when an agent acts for a user it should borrow the user’s scoped authority, not wield its own standing super-credentials - so an injection can’t reach everything the agent could ever touch.
Core controls
- One identity per agent. Never a shared human’s credentials or a static, broadly-scoped API key. Isolate agent identities from user identities.
- Authenticate strongly. mTLS + OIDC / workload identity; for A2A (agent-to-agent), signed and verified Agent Cards (IV.4 · Multi-agent systems, A2A & the seams).
- Authorize least-privilege, task-scoped. The agent’s permissions are its blast radius (ASI03, from the OWASP Agentic Security Initiative); deny dangerous tool combinations (IV.2 · Model Context Protocol (MCP) - MCP, the Model Context Protocol - capability chaining).
- On-behalf-of, not super-creds. When acting for a user, use the user’s delegated, scoped authority - the single most effective limit on injection impact.
- Short-lived, JIT credentials. No long-lived static keys; audience-bound tokens (RFC 8707); secrets in a manager, never in prompts or memory (secrets + memory poisoning = ASI06).
- Non-transitive delegation. Authority must not accumulate across A2A hops (IV.4 · Multi-agent systems, A2A & the seams); re-scope at each boundary.
- Lifecycle & de-provisioning. Orphaned NHIs and identity sprawl are where forgotten identities become breach vectors (V.2 · Cloud security & red-teaming) - decommission aggressively.
The mechanisms, named
The controls above are principles; these are the concrete standards that implement them. The page name-drops “audience-bound” and “on-behalf-of” - here is what actually enforces each.
| Control principle | Mechanism (standard) | What it does |
|---|---|---|
| Stop bearer-token theft | DPoP (RFC 9449) or mTLS-bound tokens (RFC 8705) | Sender-constrains the token: binds it to a client-held key (DPoP proof JWT) or client cert (cnf.x5t#S256). A stolen token is useless without the private key - the direct answer to Salesloft/Drift. |
| On-behalf-of / delegation | Token exchange (RFC 8693) | Trades a subject token for a downstream one, stamping the act (actor) claim so the delegation chain is explicit and re-scopeable at each hop. |
| Bootstrap workload identity | SPIFFE / SPIRE, issuing SVIDs | Platform attestation (node + workload) mints a short-lived X.509 or JWT SVID with no injected “secret zero” - solves the bootstrap credential the injection would otherwise steal. |
| Enforce aud/scope at runtime | Policy-as-code: OPA (Rego) or Cedar | The resource server checks aud, scope, and delegation against a policy at the point of use - not just at token mint. This is where least-privilege is actually decided. |
| Gate high-impact actions | Step-up auth / human-in-the-loop | A fresh, stronger factor (or explicit human approval) before a destructive or irreversible action - so a hijacked session can’t silently escalate. |
Verify the controls
Preaching least-privilege NHIs is cheap; proving your estate has none of the anti-patterns this page names is the work. Run the failure-hunt, then walk the checklist.
# long-lived static keys (>90d) - the anti-pattern (cutoff computed, not hardcoded)CUTOFF=$(date -u -d '90 days ago' +%Y-%m-%d)aws iam list-users --query 'Users[].UserName' --output text | \ xargs -I{} aws iam list-access-keys --user-name {} \ --query "AccessKeyMetadata[?CreateDate<=\`$CUTOFF\`].[UserName,AccessKeyId,CreateDate]"# orphaned / unused roles (no activity 90d) via Access Advisor or:aws iam generate-service-last-accessed-details --arn <role-arn># wildcard / admin NHI policies - resolve each policy's DEFAULT version, don't assume v1aws iam list-policies --scope Local --query 'Policies[].Arn' --output text | \ xargs -I{} sh -c 'V=$(aws iam get-policy --policy-arn {} --query Policy.DefaultVersionId --output text); \ aws iam get-policy-version --policy-arn {} --version-id "$V" \ --query "PolicyVersion.Document" | grep -q "\"\\*\"" && echo {}'- One identity per agent - no shared human creds, no static broad-scoped keys.
- Strong auth: mTLS + OIDC / workload identity; signed Agent Cards for A2A.
- Least-privilege, task-scoped entitlements; no wildcard / admin NHI policies.
- On-behalf-of for user actions; short-TTL, audience-bound tokens (RFC 8707).
- Sender-constrained tokens (DPoP RFC 9449 or mTLS-bound RFC 8705) for sensitive downstream APIs.
- Delegation via token exchange (RFC 8693) with an explicit
actclaim; aud/scope enforced at the resource by policy (OPA / Cedar). - Revocation is real: introspection (RFC 7662) or workload-identity disable, not TTL alone; step-up / human approval on high-impact actions.
- No orphaned or unused NHIs; rotate and de-provision aggressively.
The on-behalf-of exchange, made concrete. On-behalf-of is named as the control everything hinges on above, in the mermaid, and in the checklist - but the call that actually stamps the act chain is the one artifact left theoretical. This is it: RFC 8693 token exchange trades the user’s token plus the agent’s NHI for a single scoped, audience-bound token carrying act.
# The agent presents the user's token (subject) + its own NHI (actor) to borrow the user's scoped authoritycurl -s -X POST https://<idp>/oauth2/token \ -d grant_type=urn:ietf:params:oauth:grant-type:token-exchange \ -d subject_token=<user-access-token> \ # on whose behalf: user:alice -d subject_token_type=urn:ietf:params:oauth:token-type:access_token \ -d actor_token=<agent-nhi-token> \ # who acts: agent://crm-summarizer -d actor_token_type=urn:ietf:params:oauth:token-type:access_token \ -d resource=https://crm.internal/api \ # RFC 8707: binds aud, same target as the client-creds token above -d scope=records:read# -> returns a NEW short-lived token stamped with the act (actor) claim (the decoded token above):# the call to crm.internal is bound to user:alice, aud=https://crm.internal/api, scope=records:read, exp=+300s.# SCOPED: the agent borrows only records:read for this user - not its own standing rights.# AUDITABLE: the act chain makes every crm.internal action attributable to user:alice, not to a faceless NHI.Re-scope this token at each hop - never blindly forward it - so authority does not accumulate across delegation boundaries (IV.4 · Multi-agent systems, A2A & the seams).