# Gateway Tenant Auth & ABAC Contract (Web V) ## Status - v1.1 (2025-12-24); updated for identity header hardening (Sprint 8100.0011.0002). - v1.0 (2025-12-01); aligns with Policy Guild checkpoint for Sprint 0216. ## Header Naming Migration As of Sprint 8100.0011.0002, the Gateway uses canonical `X-StellaOps-*` headers: - `X-StellaOps-Tenant`, `X-StellaOps-Project`, `X-StellaOps-Actor`, `X-StellaOps-Scopes` Legacy `X-Stella-*` headers are emitted when `Gateway:Auth:EnableLegacyHeaders=true` (default) for backward compatibility. Clients should migrate to `X-StellaOps-*` headers. ## Security: Identity Header Hardening (v1.1) **CRITICAL:** As of Sprint 8100.0011.0002, the Gateway enforces a **strip-and-overwrite** policy for identity headers: 1. All reserved identity headers (`X-StellaOps-*`, `X-Stella-*`, `sub`, `tid`, `scope`, `scp`, `cnf`) are **stripped** from incoming requests. 2. Downstream identity headers are **overwritten** from validated JWT claims—clients cannot spoof identity. 3. For anonymous requests, explicit `anonymous` identity is set to prevent ambiguity. This replaces the legacy "set-if-missing" behavior which allowed header spoofing. ## 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-StellaOps-Scopes` (or legacy `X-Stella-Scopes`) is forbidden by default; accepted only when `Gateway:Auth:AllowScopeHeader=true` for offline/pre-prod bundles. Violation returns `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`/`scope`, `stellaops: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-StellaOps-Trace-Id` | Optional | If absent the gateway issues a ULID trace id and propagates downstream. Legacy: `X-Stella-Trace-Id`. | | `X-Request-Id` | Optional | Echoed for idempotency diagnostics and response envelopes. | ### Reserved Identity Headers (Client-Provided Values Ignored) The following headers are **stripped** from client requests and **overwritten** from validated claims: | Header | Source Claim | Notes | | --- | --- | --- | | `X-StellaOps-Tenant` | `stellaops:tenant` (fallback: `tid`) | Tenant identifier from token claims. | | `X-StellaOps-Project` | `stellaops:project` | Project identifier (optional). | | `X-StellaOps-Actor` | `sub` | Subject/actor from token claims. | | `X-StellaOps-Scopes` | `scp` (individual) or `scope` (space-separated) | Scopes from token claims, sorted deterministically. | Legacy `X-Stella-*` headers are emitted alongside canonical headers when `EnableLegacyHeaders=true`. ## Processing Rules 1) **Strip reserved headers:** Remove all reserved identity headers from the incoming request (see table above). 2) **Validate JWT:** Verify signature against offline bundle trust roots; `aud` must be one of `stellaops-web` or `stellaops-gateway`; reject on `exp/nbf` drift > 60s. 3) **Extract identity from claims:** - Tenant: `stellaops:tenant` claim, fallback to `tid` claim. - Project: `stellaops:project` claim (optional). - Actor: `sub` claim. - Scopes: `scp` claims (individual items) or `scope` claim (space-separated). 4) **Write downstream headers:** Overwrite `X-StellaOps-*` headers from extracted claims. Legacy `X-Stella-*` headers written when enabled. 5) **Anonymous handling:** If unauthenticated and `AllowAnonymous=true`, set explicit anonymous identity (`X-StellaOps-Actor: anonymous`, empty scopes). 6) **RBAC:** Check required scopes per route (matrix below). Missing scope → `ERR_SCOPE_MISMATCH` (403). 7) **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`. 8) **Determinism:** Identity headers are always overwritten from claims; 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" } ```