feat(auth): dual-accept CL0UD workload identity on the machine routes #525

Merged
admin merged 1 commits from feat/workload-identity-dual-accept-20261108 into main 2026-11-09 17:40:31 +00:00
Owner

Problem

The internal machine routes trusted a shared X-Internal-Token with no tenant authority: middleware forwarded the request without a verified tenant, so assertTenantMatchesVerified was a no-op and the caller-controlled tenant_id in the body decided which tenant was written. That is the P0 class already fixed for canonical/facts in #494, but the mechanism moved from a hard-coded allowlist to a verified identity model instead of a client header.

Change

  • src/auth/internal-service-tenant.ts — server-configured INTERNAL_SERVICE_TENANTS single-tenant scope + middleware-only scope header (client values stripped).
  • src/auth/cl0ud-workload.ts — real jose/JWKS verification of CL0UD workload JWTs (issuer, audience 0pt, tenant/capability claims). Tenant and capabilities come from verified claims only; fail-closed otherwise.
  • middleware.ts — strips all x-0pt-verified-* inbound headers on every forwarded path. Legacy internal token → single configured tenant as scope; >1 configured tenant → 401 (ambiguous, not widened). Bearer workload token → verified claims become verified tenant/sub/capabilities; invalid → 401. Session/Zitadel/dev paths unchanged.
  • src/http/tenant-guard.ts — scope-enforcing tenant guard + assertWorkloadCapabilities.
  • app/api/0pt/quality/bridge-run/route.ts — now guarded like canonical/facts.

Tests

tests/auth/workload-identity-dual-accept.test.ts — 10 cases: forged header stripping (legacy + workload path), ambiguous multi-tenant refusal, wrong aud, missing tenant claim, cross-tenant write denial, missing capability denial. Full suite: 133 files / 1376 tests passing.

Still external

CL0UD service-account + workload-token provisioning (/account/service-accounts requires an operator session) and JWKS publication. Until those exist, the legacy token path is the only one that authenticates — now tenant-scoped, so it is strictly safer than before.

## Problem The internal machine routes trusted a shared `X-Internal-Token` with no tenant authority: middleware forwarded the request without a verified tenant, so `assertTenantMatchesVerified` was a no-op and the caller-controlled `tenant_id` in the body decided which tenant was written. That is the P0 class already fixed for `canonical/facts` in #494, but the mechanism moved from a hard-coded allowlist to a verified identity model instead of a client header. ## Change - **`src/auth/internal-service-tenant.ts`** — server-configured `INTERNAL_SERVICE_TENANTS` single-tenant scope + middleware-only scope header (client values stripped). - **`src/auth/cl0ud-workload.ts`** — real `jose`/JWKS verification of CL0UD workload JWTs (issuer, audience `0pt`, tenant/capability claims). Tenant and capabilities come from verified claims only; fail-closed otherwise. - **`middleware.ts`** — strips all `x-0pt-verified-*` inbound headers on every forwarded path. Legacy internal token → single configured tenant as scope; >1 configured tenant → 401 (ambiguous, not widened). Bearer workload token → verified claims become verified tenant/sub/capabilities; invalid → 401. Session/Zitadel/dev paths unchanged. - **`src/http/tenant-guard.ts`** — scope-enforcing tenant guard + `assertWorkloadCapabilities`. - **`app/api/0pt/quality/bridge-run/route.ts`** — now guarded like `canonical/facts`. ## Tests `tests/auth/workload-identity-dual-accept.test.ts` — 10 cases: forged header stripping (legacy + workload path), ambiguous multi-tenant refusal, wrong `aud`, missing tenant claim, cross-tenant write denial, missing capability denial. Full suite: 133 files / 1376 tests passing. ## Still external CL0UD service-account + workload-token provisioning (`/account/service-accounts` requires an operator session) and JWKS publication. Until those exist, the legacy token path is the only one that authenticates — now tenant-scoped, so it is strictly safer than before.
CODEOWNERS rules requested review from sax3l 2026-11-09 17:30:37 +00:00
admin added 1 commit 2026-11-09 17:35:38 +00:00
feat(auth): dual-accept CL0UD workload identity on the machine routes
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 7s
masterplan-lock / check (push) Successful in 7s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 13s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 17s
CI (SIAX Cloud) / security (pull_request) Successful in 18s
CI (SIAX Cloud) / integration (pull_request) Successful in 27s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m5s
d40360aabc
The internal machine routes (`quality/bridge-run`, `canonical/facts`,
`internal/evidence-outbox/dispatch`, `internal/er/candidates`,
`search/definitions`) trusted a shared `X-Internal-Token` with NO tenant
authority: middleware let it through without setting any verified tenant, so
`assertTenantMatchesVerified` was a no-op and a caller-chosen `tenant_id` in
the body was authoritative.

This closes that gap while keeping the engine working during the transition
to CL0UD workload tokens:

- `src/auth/internal-service-tenant.ts` — the server-configured
  `INTERNAL_SERVICE_TENANTS` scope, a scope header that only middleware may
  set, and a parser that strips/ignores client values.
- `src/auth/cl0ud-workload.ts` — a real `jose`/JWKS verifier for CL0UD
  workload JWTs (`iss`/`aud`/`exp`), deriving `tenant_id` and `capabilities`
  from verified claims only. Fail-closed when a token is presented but the
  issuer/JWKS/audience/tenant claim does not verify.
- `middleware.ts` — strips every `x-0pt-verified-*` header on all forwarded
  paths. Legacy internal tokens get the single configured tenant as scope
  (an ambiguous multi-tenant list is refused with 401 instead of widened);
  a Bearer CL0UD workload token is verified and its claims become the
  verified tenant/subject/capabilities.
- `src/http/tenant-guard.ts` — enforces the service scope on tenant-scoped
  writes (deny when the tenant is outside the scope) and adds
  `assertWorkloadCapabilities` for capability-scoped principals.
- `quality/bridge-run` now calls `assertTenantMatchesVerified` like
  `canonical/facts` already did.

Tests: `tests/auth/workload-identity-dual-accept.test.ts` (10 cases) covers
forged-header stripping, ambiguous-scope refusal, wrong audience, missing
tenant claim, cross-tenant denial and missing-capability denial.

Real CL0UD provisioning (service accounts + published JWKS) is still
required before a workload token exists in production; until then the
legacy token path is the only one that authenticates, now tenant-scoped.
admin force-pushed feat/workload-identity-dual-accept-20261108 from eec63a2040 to d40360aabc 2026-11-09 17:35:38 +00:00 Compare
admin merged commit d81325f27c into main 2026-11-09 17:40:31 +00:00
admin deleted branch feat/workload-identity-dual-accept-20261108 2026-11-09 17:40:32 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: admin/0pt#525