106 lines
6.8 KiB
Markdown
106 lines
6.8 KiB
Markdown
# 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 <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-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"
|
|
}
|
|
```
|