ADR-0021: Security & authorization — one acquire() at the seam, agents auth via the MCP standard
ADR-0021: Security & authorization — one acquire() at the seam, agents auth via the MCP standard
Status
Accepted
Date
2026-07-03
Context
NETW-19’s security half. The authorization machinery is decided across canon — the authority stack (P12–P15), the policy split (the-seam §IX), authz-at-scale (LLD §13.2), and the audit model (ADR-0006 + the Truth-plane log family). This ADR consolidates those into the named security decision NETW-19 asks for; it does not invent them. The one place it makes a fresh commitment is agent authentication: agents are the kernel’s primary consumers, so their auth must follow the ecosystem standard rather than a bespoke mechanism — and the current standard is the MCP authorization specification (OAuth 2.1, MCP server as resource server).
Decision
-
The authority stack — one
acquire()at the seam.acquire(action, resource)threads four ports: P15 Identity authenticate → P12 ReBAC relationship check (OpenFGA Lean / SpiceDB Scale) → P13 ABAC context decision (Cedar) → P14 Capability mint (Biscuit — attenuable, offline-verifiable).invoke()re-verifies the token and re-runs fast context predicates — two touchpoints against TOCTOU. Custom ReBAC is rejected permanently (highest-blast-radius code; a Zanzibar-shaped engine is a solved problem). -
The policy split (§IX). A policy fact lifts into the types iff changing it would already force an SDK regeneration. So only the capability vocabulary is structural (compile-time); who holds it (RBAC), on which instance (ReBAC), under what conditions (ABAC) all stay at the seam. “Coarse capabilities in the types, fine decisions at the seam.” This keeps Policy a runtime-mutable primitive — break-glass grants, hot-swapped rules, tightening mid-incident — without redeploying a client.
-
Authz at scale (§13.2). Two-tier enforcement: coarse markings compile to indexable IR predicates applied to every plan; fine ReBAC runs only on the surviving candidate set. Denormalized relationship closure (the Zanzibar “Leopard” lesson) makes checks index hits; list queries use
LookupResources/ accessible-ID sets, never N checks for N rows. Entity authority is granted through container relationships (project, region, org-node), not per-entity ACLs. Budget: pointcheckp99 < 5ms. -
Identity is OAuth 2.1 / OIDC (P15) — and agents authenticate via the MCP standard.
- Humans authenticate via OIDC → session → capability acquisition, with step-up auth for sensitive Acts.
- Agents authenticate via the MCP authorization specification: the kernel’s MCP tool
surface (projected by P18) acts as an OAuth 2.1 resource server — it validates access
tokens (OAuth 2.1 §5.2), requires each token to be audience-bound to it (RFC 8707 resource
indicators), relies on mandatory PKCE (MCP clients are public), and advertises discovery via
RFC 9728 Protected Resource Metadata (
/.well-known/oauth-protected-resource); tokens are issued by an external authorization server, never by the resource server. The authenticated principal maps to an Actor and its capabilities attenuate through P14 for sub-agents. - SPIFFE/SPIRE is an optional backend for internal service-to-service workload identity behind P15 — not the agent- or human-facing mechanism. State-of-the-art external auth is OAuth 2.1 / OIDC + the MCP resource-server model.
-
Author-blind. Human and agent are one Actor model; the seam checks both identically — there is no privileged admin channel outside the rings.
-
Governance & audit are queries, not subsystems. Everything external is a governed Resource (ADR-0006). Audit is a
Queryover the Truth-plane log family (Statement / Decision / Call / Schema / Policy logs — distinct retention and replay). Lineage is aQueryoverderivedFrom/basedOn— per-value, finer than dataset-to-dataset. Markings propagate down lineage and enforce at read as IR predicates. PII rides a value-type PII class + per-subject DEK, with crypto-shredding erasure (§16).
Consequences
- This ADR is mostly a consolidation record; the single genuinely new commitment is aligning agent auth to the MCP OAuth 2.1 resource-server model. That spec is evolving, so canon cites the spec and its role (resource server, audience-bound tokens, PKCE, RFC 9728 discovery) rather than freezing exact RFC MUST-levels that may revise.
- Ties to ADR-0006 (governed Resource / markings), ADR-0018 (credentials, per-Tenant KEK), and ADR-0020 (authorization scopes to Universe; Tenant is the security owner).
- Revisit triggers: a material revision of the MCP authorization spec; the first internal service mesh that needs workload identity (the SPIFFE/SPIRE decision moves from optional to chosen).
Alternatives Considered
- Custom ReBAC / hand-rolled authorization. Rejected: highest-blast-radius code; OpenFGA/SpiceDB are the solved answer.
- Prompt-level agent guardrails. Rejected: safety must never depend on the prompt asking nicely; authority is enforced at the seam per SDK call, and an agent is just an Actor.
- SPIFFE/SPIRE-led agent authentication. Rejected in this grill: agents authenticate via the MCP OAuth standard; SPIFFE is internal-mesh-only, optional.
- Aggressive policy-lifting into types (roles/predicates as generated types). Rejected: it freezes Policy into a build-time artifact and forces a redeploy on every grant — the §IX thin cut lifts only the capability vocabulary.