Identity and Permission Design

Module B5 · Course 2B — Securing & Attacking Harnesses and LLMs

60 minutes · An agent is not a human. The agent never holds a long-lived secret — the harness does.

Giving an agent a human's credentials means the agent acts with the human's full privilege. An injected agent with admin credentials is an admin compromise. This module builds the identity layer every other defense rests on.

Pillar 2 — Trust Surfaces

What B2 and B4 deferred

B2 built the injection defense. B4 built the tool-trust gate. Both 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 substrate neither built. That substrate is this module.

The identity layer is the blast-radius floor. An agent with no credentials cannot exfiltrate even if fully injected. An agent with an admin's credentials can exfiltrate the account the moment injection succeeds.

Two OWASP risks this module owns

RiskThe identity-layer failure
ASI03 — Excessive AgencyThe agent has more permission than its task requires. Over-provisioned NHI, broad scopes. The credential permits more than the task needs.
ASI10 — Broken Access ControlThe agent acts on behalf of an under-authenticated principal, or a scope-check gap lets it exceed its intended privilege. Privilege-escalation chains.
Both are identity-layer failures. Both are where real enterprise deployments get compromised — not on injection sophistication, but on the engineer who pasted their AWS keys "to get it working."

B5.1 — The identity problem & non-human identities

Why borrowing human credentials is the failure

The borrowed-credential problem

The default prototype shortcut: paste the developer's AWS key / OAuth token / Slack token into the agent's env var. "I have the credentials, the agent needs to call the API."
What happens: the agent inherits the human's full privilege — including everything the human can do that the task does not require. An agent that reads one bucket, given keys that read every bucket, can exfiltrate every bucket.
Now add injection. B2: ~50% injection success against undefended agents. An injected agent with borrowed admin credentials is an admin compromise — and it looks legitimate in the access logs because the credential was never stolen, it was handed over.

The NHI model — three properties

PropertyBorrowed human keyNon-human identity
Scoped to workloadNo — human's full accessYes — task's requirement only
Independently revocableNo — disables the humanYes — rotate the NHI, human unaffected
Auditable as non-humanNo — "engineer@corp did X"Yes — "order-lookup-agent did X"
LifetimeMonthsSeconds to minutes, per-task
NHIs are the fastest-growing identity category in cloud security. Enterprise identity teams facing an NHI population that outnumbers humans 10:1 — and every agent you deploy adds to it.

B5.2 — Least privilege, delegated auth, token exchange

The agent never holds a long-lived secret

Per-task scoped credentials via token exchange

AGENT (session token, NHI-scoped, TTL: minutes)
  │
  ▼  "I need to call lookup_order"
HARNESS intercepts the tool call (B2 L3 / B4 gate)
  │
  ├── retrieves session token from VAULT (agent has no ref to vault)
  ├── POST token-exchange (RFC 8693)
  │     grant_type=token-exchange
  │     scope=orders:read   audience=orders-api
  ▼
ACTION TOKEN (scope: orders:read, TTL: 60s)
  │
  ├── scope_check(token, "orders:read", "orders-api")  ← deterministic gate
  ▼
AUTHENTICATED CALL → result returned to agent (token NOT in result)
The agent never holds a long-lived secret. The session token is short-lived; the action token is shorter and narrower. An injected agent that captures the action token gains, at most, one action against one audience for seconds.

Workload identity — no static credential at all

SPIFFE / SPIRE: a cryptographically-attested workload identity. The agent process does not possess a key — it requests a signed SVID from the SPIRE agent on the host, which vouches for the workload based on platform attestation.
Cloud equivalentMechanism
AWS IRSAIAM Roles for Service Accounts — pod assumes a role via OIDC
GCP Workload IdentityFederated token exchange (RFC 8693 native)
Azure Managed IdentityPlatform-managed identity, no secret in code
The property: the agent cannot steal "its" credential because it does not have one. The identity is re-attested on a short loop; when the platform stops vouching, the identity ceases to be valid.

B5.3 — Credential lifecycle & isolation

The agent never sees the raw credential

Where credentials live — the load-bearing principle

The failure mode: an agent that can read its own credentials (env var, config file, system prompt) can exfiltrate them. An injected agent that reads AWS_SECRET_ACCESS_KEY and posts it to an attacker endpoint has handed over a long-lived, broadly-scoped credential.
The defense: the credential lives in a vault the harness accesses, not in the agent process. The agent has no env var, no config file, no system-prompt API key. The harness retrieves the credential, mints the action token, makes the call, and returns only the result.

The agent sees: "I called lookup_order and got this result." It does not see: the token, the key, the secret, the authorization header.

DD-20 IronCurtain — the reference architecture

IronCurtain's credential quarantine: the agent runs with only fake API keys. A TLS-terminating MITM proxy, controlled by the harness, hot-swaps fake for real at the boundary. The agent never holds a real credential.
IronCurtain (DD-20): agent holds fake keys; proxy swaps for real. Purest expression of "the agent never holds real credentials."
This module's realization: agent holds no keys; harness retrieves from vault + token exchange. Same property, no proxy needed.
The property is identical: the agent cannot exfiltrate what it does not possess. Whether via proxy (IronCurtain), vault (this module), or attestation (SPIFFE) — the agent never holds a long-lived secret.

The privilege escalation chain (ASI03 + ASI10)

LayerThe failure (red) / the defense (green)
ASI03Over-provisioned NHI — token has orders:* instead of orders:read
ASI10Missing scope check — harness assumes the model wouldn't request unauthorized action
ASI10Missing API-side enforcement — the API doesn't verify scope
Defense 1Per-task scoped credential — narrow at issuance
Defense 2Scope-check middleware — deterministic gate before the call
Defense 3Credential isolation — agent cannot steal a broader token
Defense 4API-side scope enforcement — last line
The conjunction holds. Any single layer can fail; the identity layer is secure when an injection must defeat all four to escalate.

The load-bearing code

def execute_tool_call(vault, tool_name, required_scope, audience, args):
    # 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 authenticated call (token used and discarded)
    result = call_api(action_token, tool_name, args)
    # 4. Return the result — the token is NOT in the return value
    return result
The agent requests orders:delete during a lookup task → the action token was minted with orders:read only → scope_check fails → call blocked. No model judgment, no bypass rate. This is determinism on the privilege boundary.

Lab & what's next

Lab (07): build the Credential Isolation Layer — (a) token-exchange flow that mints a scoped action token per task, (b) scope-check middleware that verifies the token covers the action, (c) credential vault the harness accesses (not the agent). Includes an injection scenario: injected agent attempts orders:delete, scope-check blocks it.

Next — B6: Inter-Agent Trust and Communication Security. When agents talk to agents, the identity layer from B5 becomes the trust substrate for inter-agent channels. B6 builds the authentication, authorization, and message-integrity controls for multi-agent systems.