# Module B5 — Identity and Permission Design

**Course**: 2B — Securing & Attacking Harnesses and LLMs
**Module**: B5 — Identity and Permission Design
**Duration**: 60 minutes
**Level**: Senior Engineer and above
**Prerequisites**: B2 complete (the five-layer injection defense); B4 complete (tool/MCP security, capability minimization as the deterministic floor); Course 1 Modules 2.4 (tool contracts) and 6.1 (permission design space)

> *An agent is not a human. Giving an agent a human's credentials means the agent acts with the human's full privilege — and an injected agent with an admin's credentials is an admin compromise. This module builds the identity layer the rest of the course assumes: agents get their own non-human identities, scoped to the current task, exchanged per action, and isolated from the agent process entirely. The agent never holds a long-lived secret. The harness does.*

---

## Learning Objectives

After completing this module, you will be able to:

1. Explain why an agent borrowing a human's credentials (OAuth token, AWS keys) is an identity-design failure — the agent acts with the human's full privilege, and an injected agent with admin credentials is an admin compromise — and articulate why non-human identities (NHIs) are the correct model.
2. Apply least privilege at the agent layer: an agent should hold exactly the permissions its current task requires, no more — per-task scoped credentials, minted for the action, revoked after.
3. Implement delegated auth via OAuth scopes, RFC 8693 token exchange, and short-lived credentials, so the agent never holds a long-lived secret and exchanges a short-lived token for an even-more-scoped one per action.
4. Design the credential lifecycle — issuance, scoping, rotation, revocation — and locate credentials in a secrets manager the *harness* accesses, not the agent process (the agent never sees the raw credential).
5. Map OWASP ASI03 (Excessive Agency) and ASI10 (Broken Access Control) to the identity layer, and identify privilege-escalation chains where an injected agent acts on behalf of an under-authenticated principal.
6. Build a token-exchange and scope-check middleware that mints a scoped token per task, verifies the token covers the requested action, and quarantines credentials per DD-20 IronCurtain's credential-isolation architecture.

---

## Why this module exists

B2 built the injection defense. B4 built the tool-trust gate. Both modules deferred one question: *who is the agent, and what is it allowed to do?* B2's Layer 5 (capability minimization) and B4's gate both depend on a credential and permission substrate that neither module constructed. That substrate is this module.

The deferral was deliberate. Injection defense and tool provenance are about *what the agent is tricked into doing*. Identity and permission are about *what the agent is authorized to do in the first place*. An agent with no credentials cannot exfiltrate even if fully injected; an agent with an admin's credentials can exfiltrate the entire account the moment injection succeeds. The identity layer is the blast-radius floor that every other defense rests on.

This module is the depth upgrade the course design conversation flagged as underrepresented — "central in real enterprise agent deployments." Enterprise deployments do not fail on injection sophistication; they fail on the engineer who gave the agent their personal AWS keys "to get it working," forgot to scope them, and shipped to production. The OWASP Agentic Top 10 names the two risks this module owns: ASI03 (Excessive Agency — the agent has more permission than its task requires) and ASI10 (Broken Access Control — the agent acts on behalf of an unauthenticated or under-authenticated principal). Both are identity-layer failures, and both are where real deployments get compromised.

Three sub-sections, twenty minutes each:

- **B5.1 — The Identity Problem and Non-Human Identities.** Why borrowing human credentials is the failure, what a non-human identity is, and the NHI model (service accounts, workload identity, scoped tokens).
- **B5.2 — Least Privilege, Delegated Auth, and Token Exchange.** Per-task scoped credentials, OAuth scopes, RFC 8693 token exchange, short-lived credentials. The agent never holds a long-lived secret.
- **B5.3 — The Credential Lifecycle and Credential Isolation.** Issuance, scoping, rotation, revocation. Where credentials live (a vault the harness accesses, not the agent). ASI10 privilege-escalation chains. DD-20 IronCurtain's credential quarantine as the reference architecture.

---

# B5.1 — The Identity Problem and Non-Human Identities

*Why an agent borrowing a human's credentials is the single most common enterprise agent compromise — and the non-human identity model that fixes it.*

## The borrowed-credential problem

The default — the thing engineers do when they are prototyping — is give the agent their own credentials. A developer's OAuth token. A personal AWS access key. A Slack bot token grabbed from the team workspace. The reasoning is pragmatic: "I have the credentials, the agent needs to call the API, I'll paste my key into the environment variable." This works. It also creates the most common and most dangerous identity failure in deployed agents.

The problem is not that the agent has credentials. The problem is *whose* credentials it has, and *what privilege they carry*. A developer's AWS keys were minted for a human who can reason about scope, escalate to a human approver, and be held accountable. When those keys are handed to an agent, the agent inherits the human's full privilege — including everything the human can do that the task does not require. An agent that needs to read one S3 bucket, given keys that can read every bucket in the account, can now exfiltrate every bucket in the account. The blast radius is the credential's scope, not the task's requirement.

Now add injection. B2 showed that indirect injection succeeds roughly 50% of the time against undefended agents (InjecAgent benchmark). An injected agent that holds an admin's credentials is an admin compromise — the attacker has, through the agent, the same access the admin has, with the added property that the access looks legitimate (it is the agent's normal credential, used normally, from the agent's normal IP). The compromise is invisible to access logs because the credential was never stolen; it was *handed over*.

This is the borrowed-credential problem: **the agent's privilege equals the borrowed human's privilege, and an injected agent with borrowed admin credentials is an admin compromise.** The fix is not better injection defense (B2 handles that) or better tool provenance (B4 handles that). The fix is that the agent never holds the human's credentials at all.

## Non-human identities (NHIs)

An agent needs its *own* identity. Not a human's credentials, borrowed and de-scoped — a non-human identity (NHI) minted for the agent, scoped to the agent's task, and revocable independently of any human.

An NHI is any identity that authenticates a workload, service, or process rather than a person. Service accounts in AWS (IAM roles), Google (service accounts), Azure (managed identities), Kubernetes (service accounts), and GitHub (GitHub Apps / deploy keys) are the established forms. The agent — a process that calls APIs on behalf of a task — is an NHI, not a human, and its identity should reflect that.

The NHI model has three properties that borrowed human credentials lack:

1. **Scoped to the workload.** An NHI is minted with exactly the permissions the workload requires. An AWS IAM role for an agent that reads one S3 bucket has a policy that allows `s3:GetObject` on that one bucket — not the developer's broad access. The blast radius is the task, not the human.
2. **Independently revocable.** An NHI can be revoked without disabling the human whose credentials were borrowed. When a task ends, the credential is destroyed; when an agent is compromised, the NHI is rotated without touching the developer's access.
3. **Auditable as non-human.** Access logs distinguish "the agent did this" from "the human did this." This matters for incident response: an anomalous action by an NHI is an agent event, not a user event, and the response differs.

The NHI is the fastest-growing identity category in cloud security. The Cloud Security Alliance and Gartner have both flagged NHIs as the dominant identity-mgmt surface for 2025-2026, driven precisely by the agentic and microservices explosion. Enterprise identity teams that spent a decade on human IAM (SSO, MFA, joiner-mover-leaver) are now confronting a non-human identity population that outnumbers humans 10:1 or more in a typical cloud account — and that population includes every agent you deploy.

## The NHI vs. the borrowed credential — a worked contrast

Consider a customer-support agent that needs to look up order status. The borrowed-credential approach: the engineer's personal API key for the orders service, with read-write access because the engineer also debugs. The NHI approach: an IAM role `order-lookup-agent` with a policy permitting `orders:Read` scoped to `customer_id = ${agent.session.customer_id}`.

| Property | Borrowed (human key) | NHI (scoped role) |
| --- | --- | --- |
| Privilege | Engineer's full access (read-write, all customers) | Read one customer's orders |
| Blast radius if injected | All orders, read and write | One customer, read-only |
| Revocation | Disable the engineer (disrupts their work) | Rotate the role (engineer unaffected) |
| Audit trail | "engineer@corp did X" (misleading) | "order-lookup-agent did X" (accurate) |
| Lifetime | Long-lived (until the engineer rotates it) | Short-lived, per-task or per-session |

The NHI column is strictly better on every security axis. The borrowed column is what ships when identity is an afterthought.

---

# B5.2 — Least Privilege, Delegated Auth, and Token Exchange

*The agent should have exactly the permissions its current task requires, no more — and the mechanism is per-task scoped credentials via token exchange, not a static key.*

## Least privilege for agents

Least privilege is the oldest principle in security. For agents it takes a specific form: **an agent should have exactly the permissions its current task requires, no more.** Not "the permissions the agent's role might need across a day of work" — the permissions *this task* needs.

The distinction matters because agents are task-scoped. A support agent looking up an order and the same support agent issuing a refund are two different tasks with two different privilege requirements. If the agent holds both privileges at all times, an injection during the lookup task can trigger a refund. If the agent holds only lookup privileges during the lookup task, the injection cannot escalate to refunds — the credential does not permit it, and no amount of prompt cleverness changes a credential's scope.

This is the B2.3 resolution applied to identity: the privilege boundary is deterministic and structural (does this token permit this action?), not probabilistic and semantic (does this action look legitimate?). B2 put determinism on the taint boundary; B5 puts determinism on the privilege boundary. The credential's scope is a compiled boolean — it permits `orders:Read` or it does not — and that boolean has no bypass rate.

The operational realization is **per-task scoped credentials**. When the agent begins a task, the harness mints a credential scoped to that task's required actions. When the task ends, the credential is destroyed. The agent never holds a standing privilege set; it holds a privilege set that changes with the task and expires with it.

## Delegated auth: OAuth scopes

OAuth 2.0 scopes are the familiar mechanism for delegated, scoped authorization. A user (the resource owner) delegates a *subset* of their authority to a client (the agent) via an access token that carries specific scopes. The token permits `orders:read` but not `orders:write`; the API enforces the scope on every request.

For agents, the OAuth model is correct in shape but wrong in one default: the standard flow delegates *the user's* authority. An agent that acts on behalf of a user (a "user-delegated" agent) inherits that user's scopes — which may be broader than the task requires. The fix is that the agent's OAuth scopes are *task-narrowed*, not user-broad: even if the user has `orders:write`, the agent's token for a lookup task carries only `orders:read`.

This requires the authorization server to support scope-narrowing at token issuance — which standard OAuth supports (the client requests specific scopes; the server may grant fewer). The agent never requests `orders:write` for a read task, and the server never grants it. The scope is the privilege, and the privilege is the task.

## Token exchange (RFC 8693)

The technical anchor for per-task scoped credentials is **RFC 8693 — OAuth 2.0 Token Exchange**. Token exchange lets a client trade one token for another with a different scope, audience, or subject. The agent holds a *session* token (broad, long-ish-lived), and for each action it exchanges that token for an *action* token (narrow, short-lived, audience-specific).

The flow:

1. The agent authenticates to the authorization server and receives a **session token** (an NHI token, scoped to the agent's role, TTL of minutes to hours).
2. For a specific action — say, calling the orders API — the harness calls the token endpoint with `grant_type=urn:ietf:params:oauth:grant-type:token-exchange`, presenting the session token, and requests a token with `scope=orders:read` and `audience=orders-api`.
3. The authorization server validates the session token, verifies the agent is entitled to `orders:read`, and issues an **action token** with a short TTL (seconds to minutes) and the narrow scope.
4. The harness uses the action token to call the orders API. The token expires shortly after. It is not reusable for any other scope or audience.

The key property: **the agent never holds a long-lived secret.** The session token is short-lived and exchangeable; the action token is even shorter-lived and narrower. An injected agent that captures the action token gains, at most, the ability to perform that one action against that one audience for the token's remaining TTL (seconds). It cannot pivot to other APIs, other scopes, or other principals. This is least privilege enforced at the credential layer, not the prompt layer.

Token exchange is supported by every major cloud authorization server (AWS STS `AssumeRole`, GCP Workload Identity Federation token exchange, Azure managed identity token exchange). The mechanism is standard; the discipline of *using it per action* rather than caching a broad token is what this module teaches.

## Workload identity (SPIFFE/SPIRE)

The most mature workload-identity standard is **SPIFFE** (Secure Production Identity Framework for Everyone), with its reference implementation **SPIRE**. A SPIFFE identity (`spiffe://trust-domain/workload-identifier`) is a cryptographically-attested workload identity that the workload presents to authenticate to other workloads, without any static credential at all.

For agents, SPIFFE provides the substrate: the agent process runs under a SPIFFE identity that the platform (Kubernetes, a VM, a serverless runtime) attests. The agent does not possess a key; it requests a signed SVID (SPIFFE Verifiable Identity Document) from the SPIRE agent on the host, which vouches for the workload's identity based on platform attestation (the pod identity, the VM instance, the process selector). The SVID is short-lived (minutes) and used to authenticate to the authorization server, which then issues the session token for token exchange.

The property this gives you: **the agent process never holds a static credential.** Its identity is attested by the platform it runs on, and the attestation is continuous and short-lived. A compromised agent cannot steal "its" credential because it does not have one — it has a SVID that is re-attested on a short loop, and the moment the platform stops vouching for it (the task ends, the pod is torn down), the identity ceases to be valid.

This is the destination architecture. Cloud-managed equivalents (AWS IAM Roles for Service Accounts, GCP Workload Identity, Azure Managed Identity) provide the same property with less operational overhead, and are the realistic choice for most deployments. SPIFFE/SPIRE is the open standard for environments that span clouds or need the control plane in-house.

---

# B5.3 — The Credential Lifecycle and Credential Isolation

*Issuance, scoping, rotation, revocation — and the load-bearing principle: the credential lives in a vault the harness accesses, not in the agent process. The agent never sees the raw credential.*

## The credential lifecycle

A credential has four lifecycle stages, and each is a control point:

1. **Issuance.** The credential is minted. For an NHI, this is the authorization server issuing a session token (after authenticating the workload via SPIFFE/cloud workload identity) or a scoped role assumption (AWS STS `AssumeRole` with a session policy). Issuance is where scope is set — and where over-scoping happens if the engineer copies a broad policy "to save time."
2. **Scoping.** The credential's scope is narrowed to the task. This is the token-exchange step (RFC 8693): the session token is exchanged for an action token with `scope` and `audience` matching the task. Scoping is where least privilege is enforced — the action token permits exactly the actions the task requires.
3. **Rotation.** The credential is refreshed before it expires. Short-lived credentials (TTL of minutes) rotate continuously; the harness requests a new action token per action or a new session token per session. Rotation is the property that makes a stolen credential worthless within minutes — there is no long-lived secret to exfiltrate and hold.
4. **Revocation.** The credential is invalidated before its TTL. When a task is cancelled, an agent is compromised, or a session is abandoned, the harness revokes the active tokens. For short-lived credentials, revocation is often "let it expire" — but for an active compromise, immediate revocation (via the authorization server's revocation endpoint, or by disabling the NHI) is the kill switch.

The lifecycle is short by design. A borrowed human key has a lifecycle of months (until someone rotates it). An NHI action token has a lifecycle of seconds to minutes. The shorter the lifecycle, the smaller the window in which a stolen credential is useful — and the less value there is in stealing it.

## Where credentials live — the load-bearing principle

The single most important design decision in this module: **credentials live in a secrets manager the harness accesses, not in the agent process. The agent never sees the raw credential.**

The failure mode this prevents: an agent that can read its own credentials (from an environment variable, a config file, or — worst of all — the system prompt) can exfiltrate them. An injected agent that reads `AWS_SECRET_ACCESS_KEY` from its environment and posts it to an attacker-controlled endpoint has handed over a long-lived, broadly-scoped credential. The injection did not need to *use* the credential; it needed only to *read and exfiltrate* it. Once exfiltrated, the credential is compromised regardless of the agent's subsequent behavior.

The defense is credential isolation: the credential is never in the agent's environment. The agent process does not have `AWS_SECRET_ACCESS_KEY` in its env. It does not have the OAuth token in a config file it can read. It does not have the API key in its system prompt (the single worst anti-pattern — the prompt is text the model reads and can be coerced into repeating). The credential lives in a secrets manager (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, a cloud KMS) that the *harness* — the code that runs the agent, not the agent process itself — accesses at the moment it needs to make an authenticated call.

The flow:

1. The agent decides to call a tool (e.g., `lookup_order`).
2. The harness intercepts the tool call (this is B2's Layer 3 / B4's gate — the same interception point).
3. The harness determines the task's required scope (`orders:read`, `audience=orders-api`).
4. The harness retrieves the session credential from the secrets manager and performs token exchange to mint an action token — *the agent process never sees either credential*.
5. The harness makes the authenticated API call with the action token, receives the result, and returns only the *result* to the agent. The credential never enters the agent's context, its environment, or its process memory in a way the agent can read.

The agent sees: "I called `lookup_order` and got this result." It does not see: the token, the key, the secret, the authorization header. An injected agent that tries to exfiltrate credentials by reading its environment finds nothing; an injected agent that tries to coax the model into repeating the API key finds the key was never in the context.

## DD-20 IronCurtain's credential quarantine

The reference architecture for this is **DD-20 IronCurtain's credential quarantine** (Course 1, Module 11 / DD-20). IronCurtain's model: the agent runs with only *fake* API keys. A TLS-terminating MITM proxy, controlled by the harness, hot-swaps the fake keys for real ones at the network boundary. The agent never holds a real credential; it holds a fake that only works through IronCurtain's controlled proxy. Even a fully-compromised agent cannot exfiltrate real credentials because it does not have them — the worst it can do is use the fake keys, which only work through the proxy's controlled path.

This is the purest expression of the principle. You do not need to implement IronCurtain's exact MITM-proxy architecture to realize the property — the token-exchange-and-vault flow above achieves credential isolation without a proxy. But IronCurtain is the conceptual north star: **the agent never holds real credentials.** Whether you achieve that via a proxy (IronCurtain), a vault the harness accesses (this module's implementation), or a workload-identity attestation that requires no static credential at all (SPIFFE), the property is the same: the agent process cannot exfiltrate what it does not possess.

## Broken access control (ASI10) and privilege escalation chains

OWASP ASI10 — Broken Access Control — is the identity-layer failure where the agent acts on behalf of an unauthenticated or under-authenticated principal, or where a privilege-escalation chain lets the agent exceed its intended scope.

The classic ASI10 pattern in agents:

1. The agent holds a session token scoped to `orders:read`.
2. An injection coerces the agent into calling a tool that performs an action the token does not permit — say, `orders:delete`.
3. The *correct* behavior: the API rejects the call (the token lacks `orders:delete` scope). The identity layer holds.
4. The *ASI10 failure*: the harness does not check the token's scope before making the call (it assumes the agent would not request an unauthorized action), the API's scope check is missing or misconfigured, or the token was over-scoped at issuance (`orders:*` instead of `orders:read`). The action succeeds. The agent has exceeded its intended privilege.

The privilege-escalation chain gets worse when combined with ASI03 (Excessive Agency). If the agent's NHI was over-provisioned at issuance — `orders:*` when the task needs only `orders:read` — then the scope check at the API layer is the only thing standing between an injected agent and a destructive action. If that check is also missing, the chain is: over-provisioned NHI (ASI03) + missing scope enforcement (ASI10) = the injected agent deletes orders. Two identity-layer failures compound into a full privilege escalation.

The defense is defense-in-depth at the identity layer, the same way B2 is defense-in-depth at the injection layer:

1. **Per-task scoped credentials (B5.2).** The token is narrow at issuance. Even if the scope check is missing, the token does not permit the destructive action.
2. **Scope-check middleware (this module's code).** The harness verifies the token covers the requested action *before* making the call. This is the deterministic gate at the identity boundary — the analogue of B2's Layer 3 taint gate.
3. **Credential isolation (B5.3).** The agent cannot steal a broader credential to escalate with, because it does not hold any credential.
4. **API-side scope enforcement.** The target API verifies the token's scope on its end. This is the last line — if 1-3 fail, the API rejects the unauthorized action.

The conjunction is what holds. Any single layer can fail; the identity layer is secure when an injection must defeat all four to escalate.

## The defense summary

The defense stack this module builds, in one line: **per-task scoped credentials, minted via token exchange, verified by a scope-check middleware, and isolated from the agent process in a vault the harness accesses.** The agent never holds a long-lived secret; the harness never makes an authenticated call without verifying the token covers the action; the credential never enters the agent's context.

This is the B2.3 resolution — determinism on the structural boundary — applied to identity. The scope check is deterministic (does this token permit this action? yes/no, no model in the loop). The credential isolation is structural (the credential is not in the agent's environment, period). The token exchange is structural (the action token's scope is a compiled claim, not a prompt-level instruction). No probability, no bypass rate, no model judgment on the privilege boundary.

## Code: token exchange + scope-check middleware

The implementation below (Python, runnable, type-hinted) shows the two load-bearing pieces: a token-exchange function that mints a scoped action token from a session token, and a scope-check middleware that verifies the action token covers the requested action before the harness makes the call.

```python
from dataclasses import dataclass
from typing import Optional, Set
import time

@dataclass
class Token:
    """A short-lived, scoped credential."""
    subject: str            # the NHI identity, e.g. "order-lookup-agent"
    scopes: Set[str]        # e.g. {"orders:read"}
    audience: str           # e.g. "orders-api"
    issued_at: float
    expires_at: float
    raw: str                # the opaque token string (never shown to the agent)

    def is_expired(self, now: float) -> bool:
        return now >= self.expires_at

    def covers(self, required_scope: str, required_audience: str) -> bool:
        """Deterministic scope check. The load-bearing identity-layer gate."""
        return (
            required_scope in self.scopes
            and self.audience == required_audience
            and not self.is_expired(time.time())
        )


class CredentialVault:
    """The harness-accessed vault. The agent process has NO reference to this.

    The agent calls tools; the harness intercepts, retrieves credentials here,
    mints action tokens, and makes the authenticated call. The raw credential
    never enters the agent's context, environment, or process memory.
    """

    def __init__(self, session_token: Token, auth_server_url: str):
        # The session token lives here, not in the agent's env.
        self._session_token = session_token
        self._auth_server_url = auth_server_url

    def exchange_for_action(
        self, required_scope: str, audience: str, ttl_seconds: int = 60
    ) -> Token:
        """RFC 8693 token exchange: trade the session token for a narrow,
        short-lived action token. The agent never calls this; the harness does."""
        if self._session_token.is_expired(time.time()):
            raise PermissionError("session token expired; re-authenticate the NHI")
        # In production: POST to self._auth_server_url with
        #   grant_type=urn:ietf:params:oauth:grant-type:token-exchange,
        #   subject_token=..., scope=required_scope, audience=audience
        # Here we simulate the issued action token.
        now = time.time()
        return Token(
            subject=self._session_token.subject,
            scopes={required_scope},
            audience=audience,
            issued_at=now,
            expires_at=now + ttl_seconds,
            raw=f"sim-action-token-{required_scope}-{int(now)}",
        )


def scope_check(token: Token, required_scope: str, audience: str) -> None:
    """The deterministic scope gate. Raises if the token does not cover the action.

    This is the identity-layer analogue of B2's Layer 3 taint gate:
    structural, enumerable, no model in the loop, no bypass rate.
    """
    if not token.covers(required_scope, audience):
        raise PermissionError(
            f"token does not cover scope '{required_scope}' "
            f"on audience '{audience}' (has scopes={token.scopes}, "
            f"audience={token.audience}, expired={token.is_expired(time.time())})"
        )


def execute_tool_call(
    vault: CredentialVault,
    tool_name: str,
    required_scope: str,
    audience: str,
    args: dict,
) -> dict:
    """The harness's authenticated tool-call path. The agent never sees the token.

    1. Mint a scoped action token (token exchange).
    2. Verify the token covers the action (scope check).
    3. Make the authenticated call.
    4. Return ONLY the result to the agent.
    """
    # 1. Token exchange — the harness, not the agent, holds the session token.
    action_token = vault.exchange_for_action(required_scope, audience)
    # 2. Deterministic scope check before the call.
    scope_check(action_token, required_scope, audience)
    # 3. Make the call (simulated). The raw token is used here and discarded.
    result = {"tool": tool_name, "args": args, "status": "ok", "token_scope": required_scope}
    # 4. Return the result. The token is NOT in the return value.
    return result
```

The load-bearing lines are `vault.exchange_for_action` (the harness mints the action token; the agent has no reference to the vault) and `scope_check` before the call (the deterministic gate). An injected agent that requests `orders:delete` when the task is a lookup hits the scope check: the action token was minted with `orders:read` only, the check fails, the call is blocked. No model judgment, no bypass rate.

---

## Anti-Patterns

### Giving the agent a human's credentials
The default prototype shortcut. The agent inherits the human's full privilege; an injected agent with admin credentials is an admin compromise. Cure: the agent gets an NHI scoped to its task, never a human's borrowed key.

### Putting the credential in the agent's environment (env var, config file, system prompt)
An agent that can read its credential can exfiltrate it. Cure: credentials live in a vault the *harness* accesses; the agent process has no env var, no config file, and certainly no system-prompt API key. The agent never sees the raw credential.

### A long-lived, broadly-scoped NHI "to avoid token-exchange overhead"
The engineer who mints one `orders:*` role with a 30-day TTL "because token exchange adds latency." Cure: per-task scoped credentials via token exchange, short TTL. The latency of a token exchange is milliseconds; the cost of a 30-day over-scoped credential is a 30-day exposure window.

### Trusting the agent not to request an unauthorized action (missing scope check)
The harness assumes the model would not call `orders:delete` during a lookup task, so it skips the scope check. Cure: the scope-check middleware runs before every authenticated call, deterministically. The agent's intent is irrelevant; the token's scope is the gate.

### Over-provisioned NHI (ASI03) + missing API-side scope enforcement (ASI10)
The compound failure: the token permits too much, and the API does not verify what it permits. Cure: defense-in-depth — narrow tokens at issuance (B5.2), scope-check middleware (B5.3), and API-side scope enforcement. Any single layer can fail; the identity layer holds when the injection must defeat all.

### "The agent needs my credentials to do its job"
The framing error. The agent needs *a* credential scoped to its job — not *your* credential. Cure: mint an NHI with the task's minimum scope. If the task genuinely needs write access, the NHI gets write access for that task's duration, then it is destroyed.

---

## Key Terms

| Term | Definition |
| --- | --- |
| **Non-human identity (NHI)** | An identity that authenticates a workload, service, or process (not a person); service accounts, workload identities, scoped tokens. The fastest-growing identity category in cloud security. |
| **Borrowed-credential problem** | Giving an agent a human's credentials; the agent inherits the human's full privilege, and an injected agent with admin credentials is an admin compromise |
| **Least privilege (agent layer)** | An agent holds exactly the permissions its current task requires, no more; per-task scoped credentials |
| **Per-task scoped credential** | A credential minted for a specific task's required actions, destroyed when the task ends; the operational realization of least privilege |
| **RFC 8693 token exchange** | OAuth 2.0 Token Exchange; trading one token for another with narrower scope, specific audience, and shorter TTL. The agent holds a session token; the harness exchanges it for an action token per call |
| **Workload identity (SPIFFE/SPIRE)** | A cryptographically-attested workload identity with no static credential; the platform vouches for the workload continuously via short-lived SVIDs |
| **Credential isolation** | The credential lives in a vault the harness accesses, not in the agent process; the agent never sees the raw credential and cannot exfiltrate what it does not possess |
| **Credential quarantine (DD-20 IronCurtain)** | The reference architecture: the agent holds only fake keys; a harness-controlled proxy swaps them for real ones at the boundary. The purest expression of "the agent never holds real credentials" |
| **ASI03 (Excessive Agency)** | The agent has more permission than its task requires; over-provisioned NHI, broad scopes. The identity-layer over-privilege failure |
| **ASI10 (Broken Access Control)** | The agent acts on behalf of an under-authenticated principal, or a scope-check gap lets it exceed its intended privilege; privilege-escalation chains |
| **Scope-check middleware** | The deterministic gate that verifies a token covers the requested action before the harness makes the call; the identity-layer analogue of B2's Layer 3 taint gate |

---

## Lab Exercise

See `07-lab-spec.md`. "Build the Credential Isolation Layer": students implement (a) a token-exchange flow that mints a scoped action token per task from a session token (RFC 8693), (b) a scope-check middleware that verifies the action token covers the requested action before the call, and (c) a credential vault the harness accesses — the agent process has no reference to the vault and never sees the raw credential. The lab includes an injection scenario: an injected agent attempts `orders:delete` during a lookup task, and the scope-check gate blocks it deterministically. No GPU required; ~60-75 min.

---

## References

1. **OWASP** — *Top 10 for Agentic Applications (2026)*. `genai.owasp.org/resource/owasp-top-10-for-agentic-applications-for-2026/`. ASI03 (Excessive Agency) and ASI10 (Broken Access Control) are the primary identity-layer risks this module addresses.
2. **RFC 8693** — *OAuth 2.0 Token Exchange* (Campbell et al., 2020). `datatracker.ietf.org/doc/html/rfc8693`. The mechanism for trading a session token for a narrow, short-lived action token.
3. **SPIFFE / SPIRE** — *Secure Production Identity Framework for Everyone*. `spiffe.io`. The open workload-identity standard and its reference implementation; cryptographic workload attestation with no static credential.
4. **DD-20 IronCurtain** — Course 1 deep-dive. Credential quarantine (the agent holds only fake keys; a harness-controlled proxy swaps for real ones) and deterministic enforcement. The reference architecture for "the agent never holds real credentials." `course/01-master-course/deep-dives/dd-20-ironcurtain/01-deep-dive.md`.
5. **Course 1, Module 2.4** — *Tool Contracts*. The tool-as-a-hand framing and the dispatch boundary where the harness intercepts tool calls to perform credential isolation and scope checks. `course/01-master-course/module-02-tool-design/`.
6. **Course 1, Module 6.1** — *The Permission Design Space*. Six permission models (auto-approve, risk-tiered, deny-by-default, plan-mode, per-flag, HITL checkpoints) and the principle that permission is decoupled from reasoning. `course/01-master-course/module-06-permission-safety/`.
7. **AWS IAM** — *IAM Roles*, *STS AssumeRole*, *Roles for Service Accounts (IRSA)*. The cloud-managed NHI and token-exchange substrate. `docs.aws.amazon.com/iam/`.
8. **GCP Workload Identity Federation** — *Token exchange for external identities*; the GCP realization of RFC 8693 for workload auth. `cloud.google.com/iam/docs/workload-identity-federation`.
9. **Azure Managed Identities** — *Automatically managed identities for Azure resources*; the Azure NHI primitive. `learn.microsoft.com/entra/identity/managed-identities-azure-resources/`.
10. **Cloud Security Alliance** — *Non-Human Identity Management* research and guidance; NHIs as the dominant identity surface. `cloudsecurityalliance.org/research/working-groups/non-human-identity-management/`.
11. **OAuth 2.0 (RFC 6749)** — *The Authorization Framework*. Scopes as the delegated-authorization mechanism; the substrate token exchange builds on. `datatracker.ietf.org/doc/html/rfc6749`.
12. **B2 — Prompt Injection Defense** — *The five-layer injection defense and the probabilistic-vs-deterministic resolution*. B5's identity layer is the deterministic substrate B2's Layer 5 (capability minimization) depends on. `course/02b-ai-security/pillar-01-injection/b2-prompt-injection/01-teaching-document.md`.
13. **B4 — Tool and MCP Security** — *Capability minimization as the deterministic floor*. B4's tool-trust gate routes over-privilege residuals to B5. `course/02b-ai-security/pillar-02-trust-surfaces/b4-tool-mcp-security/01-teaching-document.md`.
