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.
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.
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
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
The internal machine routes trusted a shared
X-Internal-Tokenwith no tenant authority: middleware forwarded the request without a verified tenant, soassertTenantMatchesVerifiedwas a no-op and the caller-controlledtenant_idin the body decided which tenant was written. That is the P0 class already fixed forcanonical/factsin #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-configuredINTERNAL_SERVICE_TENANTSsingle-tenant scope + middleware-only scope header (client values stripped).src/auth/cl0ud-workload.ts— realjose/JWKS verification of CL0UD workload JWTs (issuer, audience0pt, tenant/capability claims). Tenant and capabilities come from verified claims only; fail-closed otherwise.middleware.ts— strips allx-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 likecanonical/facts.Tests
tests/auth/workload-identity-dual-accept.test.ts— 10 cases: forged header stripping (legacy + workload path), ambiguous multi-tenant refusal, wrongaud, 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-accountsrequires 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.eec63a2040tod40360aabc