fix(auth): P0 tenant-isolering för internal-token (verifierad tenant) #494

Merged
admin merged 3 commits from fix/internal-token-tenant-20261107 into main 2026-11-07 15:55:12 +00:00
Owner

P0 — tenant-isolering för internal-token-anrop

Fynd (2026-11-07): POST /api/0pt/canonical/facts med X-Internal-Token och tenant_id=999999999999999999 gav HTTP 201 och skrev data i en annan tenant.

Orsak: middleware.ts internal-token-grenen returnerade next() utan att sätta x-0pt-verified-tenant. assertTenantMatchesVerified blev därför en no-op.

Fix: sätter x-0pt-verified-tenant från X-0pt-Internal-Tenant (eller INTERNAL_TENANT_ID), och varnar när ingen tenant kan verifieras.

Bevis: bogus-tenant-data (1 relation, 2 facts, 3 entities) städades bort och verifierades 0 kvar. Efter fix ska samma anrop nekas.

Åtgärd krävs: merge + deploy av 0pt-api. Ingen dataändring i övrigt.

## P0 — tenant-isolering för internal-token-anrop **Fynd (2026-11-07):** `POST /api/0pt/canonical/facts` med `X-Internal-Token` och `tenant_id=999999999999999999` gav **HTTP 201** och skrev data i en annan tenant. **Orsak:** `middleware.ts` internal-token-grenen returnerade `next()` utan att sätta `x-0pt-verified-tenant`. `assertTenantMatchesVerified` blev därför en no-op. **Fix:** sätter `x-0pt-verified-tenant` från `X-0pt-Internal-Tenant` (eller `INTERNAL_TENANT_ID`), och varnar när ingen tenant kan verifieras. **Bevis:** bogus-tenant-data (1 relation, 2 facts, 3 entities) städades bort och verifierades 0 kvar. Efter fix ska samma anrop nekas. **Åtgärd krävs:** merge + deploy av 0pt-api. Ingen dataändring i övrigt.
admin added 1 commit 2026-11-07 14:16:03 +00:00
fix(auth): internal-token requests får verifierad tenant (P0 tenant-isolering)
masterplan-lock / check (push) Successful in 4s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 5s
CI (SIAX Cloud) / security (pull_request) Successful in 15s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 43s
CI (SIAX Cloud) / integration (pull_request) Successful in 10m3s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m15s
849e7ae802
Bevisat: POST /api/0pt/canonical/facts med X-Internal-Token och tenant_id
999999999999999999 returnerade 201 och skrev data i annan tenant. Orsak:
internal-token-grenen returnerade next() utan x-0pt-verified-tenant, så
assertTenantMatchesVerified blev no-op.

Fix: INTERNAL_TENANT_ID (eller X-0pt-Internal-Tenant) sätts som verifierad
tenant, vilket gör guard-enforcement identisk med verifierad Bearer. Varnar
om variabeln saknas.
CODEOWNERS rules requested review from sax3l 2026-11-07 14:16:03 +00:00
admin added 1 commit 2026-11-07 14:32:57 +00:00
fix(auth): bind internal-service tenant authority to server config
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 4s
masterplan-lock / check (pull_request) Successful in 4s
CI (SIAX Cloud) / security (pull_request) Successful in 16s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 16s
CI (SIAX Cloud) / integration (pull_request) Successful in 36s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m24s
b97ad0b2f4
The first revision of this fix copied a CLIENT header (X-0pt-Internal-Tenant)
or a single INTERNAL_TENANT_ID into x-0pt-verified-tenant. That is not an
authorization source: any holder of the shared internal token could still name
any tenant, leaving the original cross-tenant write reachable through a
different header.

- INTERNAL_SERVICE_TENANTS (server env) is now the only tenant authority for
  the X-Internal-Token principal; empty => no authority (fail closed, 403).
- Middleware strips inbound x-0pt-verified-{tenant,sub} and the service scope
  header on every path, including the dev escape hatch.
- x-0pt-verified-service-tenants carries the allowlist; the tenant guard checks
  membership and denies anything outside it.
- POST /api/0pt/quality/bridge-run now runs the same tenant guard as
  canonical/facts (it auto-provisions tenant-scoped rules).
- 16 new regression tests; auth + cross-tenant + canonical + bridge suites
  87/87; tsc --noEmit clean.
Author
Owner

Säkerhetsgranskning (P0) — 2026-11-07

Verdikt på första revisionen (849e7ae): BLOCKERAD. Klientheadern var behörighetskälla.

Vad som var fel

  1. req.headers.get('x-0pt-internal-tenant') ?? process.env.INTERNAL_TENANT_ID kopierades rakt in i x-0pt-verified-tenant. Vem som helst med den delade X-Internal-Token kunde därmed välja tenant i en request-header — samma cross-tenant-skrivning som fixen skulle stänga, bara via ett annat fält. POST canonical/facts tenant_id=999999999999999999 hade fortfarande gett 201.
  2. X-0pt-Internal-Tenant är klientkontrollerad och därmed inte en behörighetskälla (krav 2 och 4).
  3. INTERNAL_TENANT_ID som global fallback band inte tjänsten till en verifierad identitet eller ett per-route-scope; en enda miljövariabel blev "alla tenants".
  4. Internal-pathen skickade vidare req.headers orörda — en klient-konstruerad x-0pt-verified-tenant/x-0pt-verified-sub kunde passera på alla vägar som inte skrev över dem (bl.a. dev-läget).
  5. POST /api/0pt/quality/bridge-run tar tenant_id ur body och auto-provisionerar tenant-scopade regler — den saknade helt tenant-guard.

Rättad implementation (b97ad0b, samma PR)

  • INTERNAL_SERVICE_TENANTS (server-env, kommaseparerad allowlist) är nu enda källan till tenant-auktoritet för X-Internal-Token-principalen. Tom/osatt ⇒ ingen tenant-auktoritet ⇒ tenant-scopade skrivningar nekas med 403 (fail closed). Tenant-lösa maskinrutter (evidence-outbox/dispatch, er/candidates, search/definitions) påverkas inte.
  • Middleware strippar inkommande x-0pt-verified-tenant, x-0pt-verified-sub och x-0pt-verified-service-tenants på alla vägar (intern token, session, Bearer, dev-läge) innan den sätter dem själv.
  • x-0pt-verified-service-tenants bär allowlisten; assertTenantMatchesVerified kontrollerar medlemskap och nekar allt utanför scope. En (1) allowlistad tenant sätts även som x-0pt-verified-tenant så befintliga single-tenant-kontroller fortsätter fungera.
  • Bridge-run fick samma guard som canonical/facts, före ensureBaselineRules.
  • X-0pt-Internal-Tenant är uttryckligen inte ett kontrakt och läses aldrig.
  • 16 nya regressionstester (tests/auth/internal-token-tenant-scope.test.ts): förfalskad header ignoreras/stripas, tom scope ⇒ 403, cross-tenant ⇒ 403, inom scope ⇒ OK, Bearer/dev oförändrat.
  • Sviter: tests/auth + 0pt-cross-tenant-routes + canonical-facts + quality-bridge = 87/87 gröna; tsc -p tsconfig.check.json --noEmit ren.

Krav 1–10

Krav Status
1. Tenant från verifierad serverkontext ✅ server-env allowlist, aldrig request-fält
2. Klientstyrd tenant kan inte höja rättigheter ✅ medlemskapstest, annars 403
3. Interna anrop begränsade till tillåtna tenants/resurser ✅ INTERNAL_SERVICE_TENANTS + ruttlista
4. Förfalskade verifieringsheaders avvisas ✅ strippas på alla vägar
5. Cross-tenant-test ⇒ 403 ✅
6. Samma-tenant-test ⇒ OK ✅
7. Befintlig ingestion fungerar ✅ tenant-lösa rutter opåverkade; canonical kräver allowlist
8. RLS + API-auktorisation tillsammans ⏳ RLS-test kräver DB-miljö (utanför denna PR)
9. Inga utvecklingsundantag i prod ✅ assertAuthConfigSafe orörd; strip även i dev
10. Gitea-CI + säkerhetskontroller ⏳ körs på b97ad0b

Rekommendation

Merge först efter ägargodkännande, och sätt INTERNAL_SERVICE_TENANTS=379976258701823235 (t_0pt) i prod-secreten innan deploy. Utan den variabeln nekar den interna principalen tenant-scopade skrivningar (avsiktligt fail closed) — det ska vara ett medvetet driftval, inte en överraskning.

Ingen merge har utförts av agenten. Ingen produktionspåstående utan deploy-SHA + positiva/negativa HTTP-tester efter release.

## Säkerhetsgranskning (P0) — 2026-11-07 **Verdikt på första revisionen (849e7ae): BLOCKERAD.** Klientheadern var behörighetskälla. ### Vad som var fel 1. `req.headers.get('x-0pt-internal-tenant') ?? process.env.INTERNAL_TENANT_ID` kopierades rakt in i `x-0pt-verified-tenant`. Vem som helst med den delade `X-Internal-Token` kunde därmed välja tenant i en request-header — samma cross-tenant-skrivning som fixen skulle stänga, bara via ett annat fält. `POST canonical/facts tenant_id=999999999999999999` hade fortfarande gett 201. 2. `X-0pt-Internal-Tenant` är klientkontrollerad och därmed inte en behörighetskälla (krav 2 och 4). 3. `INTERNAL_TENANT_ID` som global fallback band inte tjänsten till en verifierad identitet eller ett per-route-scope; en enda miljövariabel blev "alla tenants". 4. Internal-pathen skickade vidare `req.headers` orörda — en klient-konstruerad `x-0pt-verified-tenant`/`x-0pt-verified-sub` kunde passera på alla vägar som inte skrev över dem (bl.a. dev-läget). 5. `POST /api/0pt/quality/bridge-run` tar `tenant_id` ur body och auto-provisionerar tenant-scopade regler — den saknade helt tenant-guard. ### Rättad implementation (b97ad0b, samma PR) - **`INTERNAL_SERVICE_TENANTS`** (server-env, kommaseparerad allowlist) är nu enda källan till tenant-auktoritet för `X-Internal-Token`-principalen. Tom/osatt ⇒ ingen tenant-auktoritet ⇒ tenant-scopade skrivningar nekas med **403** (fail closed). Tenant-lösa maskinrutter (evidence-outbox/dispatch, er/candidates, search/definitions) påverkas inte. - **Middleware strippar** inkommande `x-0pt-verified-tenant`, `x-0pt-verified-sub` och `x-0pt-verified-service-tenants` på **alla** vägar (intern token, session, Bearer, dev-läge) innan den sätter dem själv. - **`x-0pt-verified-service-tenants`** bär allowlisten; `assertTenantMatchesVerified` kontrollerar medlemskap och nekar allt utanför scope. En (1) allowlistad tenant sätts även som `x-0pt-verified-tenant` så befintliga single-tenant-kontroller fortsätter fungera. - **Bridge-run** fick samma guard som canonical/facts, före `ensureBaselineRules`. - `X-0pt-Internal-Tenant` är uttryckligen **inte** ett kontrakt och läses aldrig. - 16 nya regressionstester (`tests/auth/internal-token-tenant-scope.test.ts`): förfalskad header ignoreras/stripas, tom scope ⇒ 403, cross-tenant ⇒ 403, inom scope ⇒ OK, Bearer/dev oförändrat. - Sviter: `tests/auth` + `0pt-cross-tenant-routes` + `canonical-facts` + `quality-bridge` = **87/87 gröna**; `tsc -p tsconfig.check.json --noEmit` ren. ### Krav 1–10 | Krav | Status | |---|---| | 1. Tenant från verifierad serverkontext | ✅ server-env allowlist, aldrig request-fält | | 2. Klientstyrd tenant kan inte höja rättigheter | ✅ medlemskapstest, annars 403 | | 3. Interna anrop begränsade till tillåtna tenants/resurser | ✅ `INTERNAL_SERVICE_TENANTS` + ruttlista | | 4. Förfalskade verifieringsheaders avvisas | ✅ strippas på alla vägar | | 5. Cross-tenant-test ⇒ 403 | ✅ | | 6. Samma-tenant-test ⇒ OK | ✅ | | 7. Befintlig ingestion fungerar | ✅ tenant-lösa rutter opåverkade; canonical kräver allowlist | | 8. RLS + API-auktorisation tillsammans | ⏳ RLS-test kräver DB-miljö (utanför denna PR) | | 9. Inga utvecklingsundantag i prod | ✅ `assertAuthConfigSafe` orörd; strip även i dev | | 10. Gitea-CI + säkerhetskontroller | ⏳ körs på b97ad0b | ### Rekommendation **Merge först efter ägargodkännande, och sätt `INTERNAL_SERVICE_TENANTS=379976258701823235` (t_0pt) i prod-secreten innan deploy.** Utan den variabeln nekar den interna principalen tenant-scopade skrivningar (avsiktligt fail closed) — det ska vara ett medvetet driftval, inte en överraskning. Ingen merge har utförts av agenten. Ingen produktionspåstående utan deploy-SHA + positiva/negativa HTTP-tester efter release.
Author
Owner

Uppföljning — rättad implementation pushad (b97ad0b)

Den blockerade klientheader-varianten är ersatt. Sammanfattning av läget nu:

Vad som ändrats

  • INTERNAL_SERVICE_TENANTS (server-env) är enda tenant-auktoriteten för X-Internal-Token. Tom lista ⇒ ingen auktoritet ⇒ tenant-scopade skrivningar 403.
  • Middleware strippar inkommande x-0pt-verified-tenant, x-0pt-verified-sub och x-0pt-verified-service-tenants på alla vägar innan den sätter dem själv.
  • assertTenantMatchesVerified kontrollerar medlemskap i service-scope och failar stängt på tom scope.
  • POST /api/0pt/quality/bridge-run fick samma guard (saknades helt).
  • X-0pt-Internal-Tenant läses aldrig och är inte ett kontrakt.

Verifierat lokalt

  • Ny svit tests/auth/internal-token-tenant-scope.test.ts: 16 tester (förfalskad header ignoreras/stripas, tom scope 403, cross-tenant 403, inom scope OK, Bearer/dev oförändrat).
  • tests/auth + 0pt-cross-tenant-routes + canonical-facts + quality-bridge = 87/87.
  • tsc -p tsconfig.check.json --noEmit ren.

CI: körningar 422444–422447 är köade/pågående på b97ad0b (runners upptagna). Ingen merge utförd — väntar på ägargodkännande och grön CI.

Driftföreberedelse: sätt INTERNAL_SERVICE_TENANTS=379976258701823235 (t_0pt) i 0pt-prod innan deploy. Utan den nekar den interna principalen tenant-scopade skrivningar (avsiktligt fail-closed).

### Uppföljning — rättad implementation pushad (`b97ad0b`) Den blockerade klientheader-varianten är ersatt. Sammanfattning av läget nu: **Vad som ändrats** - `INTERNAL_SERVICE_TENANTS` (server-env) är enda tenant-auktoriteten för `X-Internal-Token`. Tom lista ⇒ ingen auktoritet ⇒ tenant-scopade skrivningar 403. - Middleware strippar inkommande `x-0pt-verified-tenant`, `x-0pt-verified-sub` och `x-0pt-verified-service-tenants` på **alla** vägar innan den sätter dem själv. - `assertTenantMatchesVerified` kontrollerar medlemskap i service-scope och failar stängt på tom scope. - `POST /api/0pt/quality/bridge-run` fick samma guard (saknades helt). - `X-0pt-Internal-Tenant` läses aldrig och är inte ett kontrakt. **Verifierat lokalt** - Ny svit `tests/auth/internal-token-tenant-scope.test.ts`: 16 tester (förfalskad header ignoreras/stripas, tom scope 403, cross-tenant 403, inom scope OK, Bearer/dev oförändrat). - `tests/auth` + `0pt-cross-tenant-routes` + `canonical-facts` + `quality-bridge` = **87/87**. - `tsc -p tsconfig.check.json --noEmit` ren. **CI**: körningar 422444–422447 är köade/pågående på `b97ad0b` (runners upptagna). Ingen merge utförd — väntar på ägargodkännande och grön CI. **Driftföreberedelse**: sätt `INTERNAL_SERVICE_TENANTS=379976258701823235` (t_0pt) i 0pt-prod innan deploy. Utan den nekar den interna principalen tenant-scopade skrivningar (avsiktligt fail-closed).
admin added 1 commit 2026-11-07 15:39:50 +00:00
fix(auth): en (1) tenant per intern serviceprincipal — multi-tenant nekas
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 4s
masterplan-lock / check (pull_request) Successful in 8s
CI (SIAX Cloud) / security (pull_request) Successful in 35s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 44s
CI (SIAX Cloud) / integration (pull_request) Successful in 9m48s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m14s
1a581fde29
Den delade X-Internal-Token identifierar EN tjänsteprocess, inte flera
oberoende principaler. En allowlist med flera tenants skulle låta varje
tokeninnehavare välja mellan dem — samma klientstyrda auktoritet som guarden
ska ta bort. Multi-tenant-tjänsteåtkomst kräver verifierad
workload-identity-binding (CL0UD-delegation); tills den finns failar en
ambig konfiguration stängt (401) och ingen identitet forwardas.

Tester: multi-tenant nekas utan side effect, en tenant fungerar, 75/75 i
auth+ cross-tenant-sviterna, tsc ren.
admin merged commit c91d2aaca6 into main 2026-11-07 15:55:12 +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#494