Gunnar

nordisk/holger-ui

SECURITY.md

# Security policy — Holger console (holger-ui)

## Report a vulnerability privately

Write to **holger@holger.rs**, and start the subject line with `SECURITY:`. Do not open a
public issue for a vulnerability. The console follows Holger's security policy: what to
put in a report, the acknowledgement within 14 days, the fix or mitigation target, the
supported versions and how fixes are announced are all in `SECURITY.md` in the
repository `nordisk/holger`. Anyone can clone it with
`git clone ssh://anonymous@git.gunnar.rs:2222/nordisk/holger.git`.

The rest of this file is the console's security model (in Swedish).

---

## Säkerhetsmodell — holger console

> Underhållet beslutdokument. Ändra inte tokenarkitekturen, CORS-posturen
> eller transportvägen utan att uppdatera det här dokumentet först.

## Arkitekturinvarianter (bevaras av varje ändring)

- **Backend är auktoritativ.** Varje operation kontrolleras av serverns
  auth + RBAC + audit vid anropet. Konsolen har ingen klient-RBAC; capability-
  information (`WhoAmI`, `writable`-flaggor) används enbart för upptäckbarhet.
- **Avslag visas ordagrant.** RBAC-, quarantine-, promote- och policyfel
  renderas med serverns exakta mening — aldrig omskrivna, aldrig maskerade som
  framgång. Partiella resultat (`failed > 0`, `!complete`) presenteras aldrig
  som full framgång.
- **gRPC-web direkt, same-origin.** Webbläsaren talar gRPC-web med servern via
  `/grpc` på samma origin (dev: Trunk-proxy; prod: nginx). Ingen REST, ingen
  BFF, ingen CORS-öppning på servern.
- **Inga cookies, inga client secrets, ingen WebAuthn.** OIDC är authorization
  code + PKCE (publik klient, S256, engångs-verifier, state-eko). WebAuthn är
  medvetet inte runtime-anslutet i backend och får inte "färdigställas" härifrån.
- **Fail-closed-egenskaperna** (preview/dry-run, seal-once, append-only audit,
  read-gate) ägs av servern och konsolen speglar dem: replikering är
  preview-först med låst Execute, promotion bär seal-once-konsekvensen i en
  explicit bekräftelse.

## Tokenhantering och hotmodell

**Beslut:** bearer-tokenen ligger i `sessionStorage` och bifogas av EN
interceptor (`grpc.rs`) som `authorization: Bearer` på varje anrop.

- **Varför inte cookies:** en cookie skulle införa CSRF-yta och kräva
  serverförändringar; sessionStorage + explicit header har ingen ambient
  credential — en främmande sida kan inte få webbläsaren att skicka tokenen.
- **Risken:** allt sidans JS kan läsa kan en XSS läsa. Mitigering i lager:
  1. **Minimal beroendeyta** — inga externa script, ingen CDN, inga
     tredjeparts-JS-paket; hela appen är egen Rust→wasm.
  2. **Strikt CSP** (genereras per release av `cargo xtask publish`):
     `default-src 'none'`, script enbart `'self'` + hash på trunks enda
     inline-bootstrap + `'wasm-unsafe-eval'`, och **`connect-src 'self'`** —
     även om skript injiceras kan det inte exfiltrera till främmande origin.
     OIDC-deployer lägger explicit till IdP-origin (`IDP_ORIGIN=`).
  3. **Ingen token i URL, logg eller diagnostik** — verifierat: koden har inga
     `console.log`; felmodellen (`grpc::Fail`) bär endast statuskod, serverns
     meddelande och tidsstämpel; tokenfältet är `type="password"`; gRPC-statusar
     ekar aldrig credentials (serverns felmeddelanden är statiska meningar).
  4. **Livslängd:** sessionStorage dör med fliken; Log out raderar explicit;
     OIDC-verifier/state är engångs och förbrukas även vid misslyckande.
- **Vid misstänkt läcka:** rotera hemligheten (t.ex.
  `~/.config/holger-console/token`, 0600, läses via auth-stegens `token_file`)
  och starta om servern — konfigen bär ingen inline-hemlighet.

## Transport och drift

- Produktionsexponering sker **alltid bakom TLS** — referensdeployen
  terminerar i `tailscale serve` (ts.net-cert); nginx och holger-server
  lyssnar enbart på loopback. En annan LB/proxy ska ge samma garantier
  (TLS + SPA-fallback till `index.html` + `/grpc`-montering).
- Airgap: konsolen renderar helt utan internet — inga fonter, script eller
  bilder utifrån (CSP:n förbjuder det dessutom aktivt).

## Kända avvägningar

- `style-src 'unsafe-inline'`: temat ligger som ett `<style>`-element i
  `index.html`. Ofarligt i sig (style-injektion kräver redan DOM-access) men
  kan skärpas till hash om stilen flyttas till en fil.
- `WhoAmI` svarar även anonyma anrop (identitet är inte hemlig för den som
  frågar om sig själv) och ekar aldrig credentialen — endast subject/metod/roll.
- Quarantine-orakel: konsolen visar serverns avslag ordagrant men listar inte
  quarantine-styrda artefakter annorlunda än andra — ingen existens-läcka
  utöver vad servern själv säger.