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

Header Naming Migration

As of Sprint 8100.0011.0002, the Gateway uses canonical X-StellaOps-* headers:

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:

  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)

Scope

Header & Claim Inputs

NameRequiredNotes
Authorization: Bearer <jwt>YesRS256/ES256; claims: iss, sub, aud, exp, iat, nbf, jti, optional scp/scope, stellaops:tenant. DPoP proof verified when DPoP header present.
DPoPCond.Proof-of-possession JWS for interactive clients; validated against htm/htu and access token jti. Ignored for service tokens when absent.
X-StellaOps-TraceIdOptionalIf absent the gateway issues a ULID trace id and propagates downstream.
X-Request-IdOptionalEchoed 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:

HeaderSource ClaimNotes
X-StellaOps-TenantIdstellaops:tenant (fallback: tid)Tenant identifier from token claims.
X-StellaOps-Projectstellaops:projectProject identifier (optional).
X-StellaOps-ActorsubSubject/actor from token claims.
X-StellaOps-Scopesscp (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

  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. The tenant header is emitted as X-StellaOps-TenantId; legacy branded tenant spellings are compatibility inputs, not preferred outputs.
  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)

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.

Outputs

Audit & Telemetry

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"
}