Gateway Tenant Auth & ABAC Contract (Web V)
This contract defines how the Stella Ops Router Gateway authenticates requests, establishes tenant identity, enforces scopes (RBAC), and applies the ABAC overlay before proxying to downstream services. It is the authoritative reference for client integrators and for downstream service authors who consume the gateway’s signed identity envelope.
Audience: API client integrators, gateway/service developers, and security reviewers.
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-TenantId,X-StellaOps-Project,X-StellaOps-Actor,X-StellaOps-Scopes
Legacy X-StellaOps-Tenant, X-Stella-Tenant, and X-Tenant-Id tenant headers are accepted only as compatibility aliases for client tenant override/strip handling. Downstream tenant identity is emitted as X-StellaOps-TenantId (plus generic X-Tenant-Id where a downstream compatibility contract requires it). Clients should migrate to X-StellaOps-TenantId.
Security: Identity Header Hardening (v1.1)
CRITICAL: As of Sprint 8100.0011.0002, the Gateway enforces a strip-and-overwrite policy for identity headers:
- All reserved identity headers (
X-StellaOps-*,X-Stella-*,sub,tid,scope,scp,cnf) are stripped from incoming requests. - Downstream identity headers are overwritten from validated JWT claims—clients cannot spoof identity.
- For anonymous requests, explicit
anonymousidentity 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
DPoPheader is present the gateway verifies it; interactive clients SHOULD send DPoP, service tokens MAY omit it. A cluster flagGateway:Auth:RequireDpopForInteractivecan make DPoP mandatory later without changing the contract. - Scope override header:
X-StellaOps-Scopes(or legacyX-Stella-Scopes) is forbidden by default; accepted only whenGateway:Auth:AllowScopeHeader=truefor offline/pre-prod bundles. Violation returnsERR_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 evaluation deterministic.
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 <jwt> | 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-TraceId | Optional | If absent the gateway issues a ULID trace id and propagates downstream. |
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-TenantId | 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-* non-tenant identity headers may be emitted alongside canonical headers when EnableLegacyHeaders=true; the canonical branded tenant header remains X-StellaOps-TenantId.
Downstream services: prefer validated claims / envelope / accessors
Routed services MUST NOT trust raw X-StellaOps-TenantId / X-Tenant-Id / X-StellaOps-Project values pulled from HttpRequest.Headers. The gateway strips spoofable values on ingress and re-emits canonical headers from validated token claims, and then signs the full identity into the signed identity envelope (X-StellaOps-Identity-Envelope + -Signature). Downstream services hydrate HttpContext.User from that envelope via UseIdentityEnvelopeAuthentication() (StellaOps.Router.AspNet) and must resolve tenancy through IStellaOpsTenantAccessor / the stellaops:tenant claim. Direct header reads bypass the trust boundary and are blocked by TenantHeaderCallSiteConformanceTests; remaining per-module call-site cleanup is tracked in the platform header-identity-hygiene sprint (HDRCAN-004). See Identity Envelope Middleware and the RequireTenant audit.
Processing Rules
- Strip reserved headers: Remove all reserved identity headers from the incoming request (see table above).
- Validate JWT: Verify signature against offline bundle trust roots;
audmust be one ofstellaops-weborstellaops-gateway; reject onexp/nbfdrift > 60s. - Extract identity from claims:
- Tenant:
stellaops:tenantclaim, fallback totidclaim. - Project:
stellaops:projectclaim (optional). - Actor:
subclaim. - Scopes:
scpclaims (individual items) orscopeclaim (space-separated).
- Tenant:
- Write downstream headers: Overwrite
X-StellaOps-*headers from extracted claims. The tenant header is emitted asX-StellaOps-TenantId; legacy branded tenant spellings are compatibility inputs, not preferred outputs. - Anonymous handling: If unauthenticated and
AllowAnonymous=true, set explicit anonymous identity (X-StellaOps-Actor: anonymous, empty scopes). - RBAC: Check required scopes per route (matrix below). Missing scope →
ERR_SCOPE_MISMATCH(403). - 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) withreason+trace_id.
- Attributes:
- Determinism: Identity headers are always overwritten from claims; error codes are stable and surfaced in the response envelope.
Route Scope Matrix (Web V)
Status: design draft — partially implemented. The gateway resolves routes dynamically from services registered via HELLO (
StellaOpsRouteResolver); it does not carry a static literal table for the prefixes below. The prefixes are the intended Web V proxy surface; the scopes have been reconciled against the canonical catalog (StellaOps.Auth.Abstractions/StellaOpsScopes.cs). Several scopes named in earlier drafts (risk:read,risk:write,notify:emit,tenant:admin,vuln:write,vuln:export,vex:write,vex:export,policy:abac) do not exist and have been replaced with the real scopes below.
/risk/*(Risk Engine) →risk-engine:readfor GET,risk-engine:operatefor jobs/simulations. (There is norisk:read/risk:writescope, and nonotify:emitscope; severity-driven notification dispatch is owned by Notify, which uses thenotify.*scope family —notify.viewer/notify.operator/notify.escalate.)/vuln/*(Vuln Explorer) →vuln:viewfor read,vuln:investigatefor triage actions,vuln:operatefor state changes,vuln:auditfor audit exports/immutable ledgers. (Legacyvuln:readis deprecated in favor ofvuln:view; there is novuln:write/vuln:exportscope.)/signals/*→signals:read(GET) /signals:write(write APIs). (Both scopes exist.)/policy/*simulation →policy:simulate. (There is nopolicy:abacscope; the ABAC overlay below is evaluated by the gateway/Policy Engine and is not a token scope.)/vex/consensus*and read/evidence routes →vex:read. Ingestion/cache mutation usesvex:ingest; provider review/admin usesvex.admin. (Excititor guards every VEX route — including export/mirror handlers — withvex:read; there is novex:writeorvex:exportscope.)/audit/decisions,/tenant/*→ tenant administration usesauthority:tenants.read/authority:tenants.writeand the console admin scopeui.admin. (There is notenant:adminscope.)- 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-engine:read required"}, "trace_id": "01HXYZ...", "request_id": "abc" }.
- 401:
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
curl -H "Authorization: Bearer $TOKEN" \
-H "DPoP: $PROOF" \
-H "X-StellaOps-TenantId: acme-tenant" \
-H "X-StellaOps-TraceId: 01HXYZABCD1234567890" \
https://gateway.stellaops.local/risk/status
Scope/ABAC deny
{
"error": {"code": "ERR_ABAC_DENY", "message": "project scope mismatch"},
"trace_id": "01HXYZABCD1234567890",
"request_id": "req-77c4"
}
