# Gateway Tenant Auth & ABAC Contract (Web V) ## Status - Final v1.0 (2025-12-01); aligns with Policy Guild checkpoint for Sprint 0216. ## Decisions (2025-12-01) - Proof-of-possession: DPoP is **optional** for Web V. If a `DPoP` header is present the gateway verifies it; interactive clients SHOULD send DPoP, service tokens MAY omit it. A cluster flag `Gateway:Auth:RequireDpopForInteractive` can make DPoP mandatory later without changing the contract. - Scope override header: `X-Stella-Scopes` is accepted only in pre-prod/offline bundles or when `Gateway:Auth:AllowScopeHeader=true`; otherwise the request is rejected with `ERR_SCOPE_HEADER_FORBIDDEN`. - ABAC overlay: evaluated on every tenant-scoped route after RBAC success; failures are hard denies (no fallback). Attribute sources are frozen for Web V as listed below to keep determism. ## Scope - Gateway header/claim contract for tenant activation and scope validation across Web V endpoints. - ABAC overlay hooks with Policy Engine (attributes, evaluation order, failure modes). - Audit emission requirements for auth decisions (RBAC + ABAC). ## Header & Claim Inputs | Name | Required | Notes | | --- | --- | --- | | `Authorization: Bearer ` | Yes | RS256/ES256; claims: `iss`, `sub`, `aud`, `exp`, `iat`, `nbf`, `jti`, optional `scp` (space-delimited), `ten` (tenant). DPoP proof verified when `DPoP` header present. | | `DPoP` | Cond. | Proof-of-possession JWS for interactive clients; validated against `htm`/`htu` and access token `jti`. Ignored for service tokens when absent. | | `X-Stella-Tenant` | Yes | Tenant slug/UUID; must equal `ten` claim when provided. Missing or mismatch → `ERR_TENANT_MISMATCH` (400). | | `X-Stella-Project` | Cond. | Required for project-scoped routes; otherwise optional. | | `X-Stella-Scopes` | Cond. | Only honored when `Gateway:Auth:AllowScopeHeader=true`; rejected with 403 otherwise. Value is space-delimited scopes. | | `X-Stella-Trace-Id` | Optional | If absent the gateway issues a ULID trace id and propagates downstream. | | `X-Request-Id` | Optional | Echoed for idempotency diagnostics and response envelopes. | ## Processing Rules 1) Validate JWT signature against offline bundle trust roots; `aud` must be one of `stellaops-web` or `stellaops-gateway`; reject on `exp/nbf` drift > 60s. 2) Resolve tenant: prefer `X-Stella-Tenant`, otherwise `ten` claim. Any mismatch → `ERR_TENANT_MISMATCH` (400). 3) Resolve project: from `X-Stella-Project` when route is project-scoped; otherwise null. 4) Build scope set: start from `scp` claim; if `X-Stella-Scopes` is allowed and present, replace the set with its value. 5) RBAC: check required scopes per route (matrix below). Missing scope → `ERR_SCOPE_MISMATCH` (403). 6) ABAC overlay: - Attributes: `subject`, `roles`, `org`, `tenant_id`, `project_id`, route vars (e.g., `finding_id`, `policy_id`), and request body keys explicitly listed in the route contract. - Order: RBAC allow → ABAC evaluate → deny overrides → allow. - Fail closed: on evaluation error or missing attributes return `ERR_ABAC_DENY` (403) with `reason` + `trace_id`. 7) Determinism: tenant header is mandatory; anonymous/implicit tenants are not allowed. Error codes are stable and surfaced in the response envelope. ## Route Scope Matrix (Web V) - `/risk/*` → `risk:read` for GET, `risk:write` for POST/PUT; severity events additionally require `notify:emit`. - `/vuln/*` → `vuln:read` for GET, `vuln:write` for mutations; exports require `vuln:export`. - `/signals/*` → `signals:read` (GET) / `signals:write` (write APIs). - `/policy/*` simulation/abac → `policy:simulate` (read) or `policy:abac` (overlay hooks). - `/vex/consensus*` → `vex:read` (stream/read) or `vex:write` when mutating cache. - `/audit/decisions`, `/tenant/*` → `tenant:admin`. - Gateway health/info endpoints remain unauthenticated but include `trace_id`. ## Outputs - Success: downstream context includes `tenant_id`, `project_id`, `subject`, `scopes`, `abac_result`, `trace_id`, `request_id`. - Failure envelope (deterministic): - 401: `ERR_TOKEN_INVALID`, `ERR_TOKEN_EXPIRED`, `ERR_DPOP_INVALID`. - 400: `ERR_TENANT_MISSING`, `ERR_TENANT_MISMATCH`. - 403: `ERR_SCOPE_MISMATCH`, `ERR_SCOPE_HEADER_FORBIDDEN`, `ERR_ABAC_DENY`. Body: `{ "error": {"code": "ERR_SCOPE_MISMATCH", "message": "scope risk:read required"}, "trace_id": "01HXYZ...", "request_id": "abc" }`. ## Audit & Telemetry - Emit DSSE-wrapped audit record: `{ tenant_id, project_id, subject, scopes, decision, reason_code, trace_id, request_id, route, ts_utc }`. - Counters: `gateway.auth.success`, `gateway.auth.denied`, `gateway.auth.abac_denied`, `gateway.auth.tenant_missing`, labeled by route and tenant. ## Examples ### Successful read ```bash curl -H "Authorization: Bearer $TOKEN" \ -H "DPoP: $PROOF" \ -H "X-Stella-Tenant: acme-tenant" \ -H "X-Stella-Trace-Id: 01HXYZABCD1234567890" \ https://gateway.stellaops.local/risk/status ``` ### Scope/ABAC deny ```json { "error": {"code": "ERR_ABAC_DENY", "message": "project scope mismatch"}, "trace_id": "01HXYZABCD1234567890", "request_id": "req-77c4" } ```