diff --git a/apps/web/src/hooks.server.ts b/apps/web/src/hooks.server.ts index 324ff30..1212293 100644 --- a/apps/web/src/hooks.server.ts +++ b/apps/web/src/hooks.server.ts @@ -3,21 +3,45 @@ import type { Handle } from '@sveltejs/kit'; /** * Server-Hook für Security-Headers auf jeder ausgelieferten Response. * - * Bewusst minimalistisch: nur die well-known Header, die ohne - * Browser-Verhaltens-Test sicher sind. **CSP fehlt absichtlich** — - * eine richtige Content-Security-Policy braucht App-Inventur - * (inline-styles, externe Theme-Assets, Markdown-Renderer) und - * sollte separat mit Live-Test eingeführt werden. - * * Cloudflare-Tunnel-Transform-Rules dürfen das gerne nochmal * obendrauf setzen — die hier sind die Defense-Tiefe-Schicht für * den Fall, dass die Cloudflare-Schiene aus dem Pfad fällt. + * + * CSP läuft initial im **Report-Only-Mode**: Browser melden + * Violations in die DevTools-Console (`console.warn`), die App + * bleibt funktional. Nach 1-2 Tagen Live-Beobachtung muss die + * Policy mit echten Reports getunet und auf Enforce geflippt + * werden — `CARDS_CSP_ENFORCE=true` aktiviert den scharfen Header. */ +const CSP_POLICY = [ + "default-src 'self'", + // 'unsafe-inline' für Svelte-5-Style-Attribute + Theme-CSS-Variables; + // langfristig durch nonce-/hash-basierten Style ersetzen. + "style-src 'self' 'unsafe-inline'", + "script-src 'self'", + "img-src 'self' data: blob:", + "font-src 'self' data:", + // XHR/fetch-Ziele: eigene API + Auth-Portal + LLM/Share/MCP-Plattform. + "connect-src 'self' https://cardecky-api.mana.how https://auth.mana.how https://share.mana.how https://mcp.mana.how", + "media-src 'self' blob:", + "frame-ancestors 'none'", + "base-uri 'self'", + "form-action 'self' https://auth.mana.how", + "object-src 'none'", +].join('; '); + export const handle: Handle = async ({ event, resolve }) => { const response = await resolve(event); response.headers.set('X-Frame-Options', 'DENY'); response.headers.set('X-Content-Type-Options', 'nosniff'); response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin'); + + const cspHeader = + process.env.CARDS_CSP_ENFORCE === 'true' + ? 'Content-Security-Policy' + : 'Content-Security-Policy-Report-Only'; + response.headers.set(cspHeader, CSP_POLICY); + if (process.env.NODE_ENV === 'production') { response.headers.set('Strict-Transport-Security', 'max-age=15552000; includeSubDomains'); } diff --git a/docs/FEATURE_IDEAS.md b/docs/FEATURE_IDEAS.md index 111f311..518efe5 100644 --- a/docs/FEATURE_IDEAS.md +++ b/docs/FEATURE_IDEAS.md @@ -277,10 +277,15 @@ einiges: DENY, X-Content-Type-Options nosniff, Referrer-Policy strict-origin-when-cross-origin, plus HSTS (180 Tage, includeSubDomains) wenn `NODE_ENV=production`. - - **CSP bewusst ausgespart** — eine richtige Content-Security- - Policy braucht Browser-Test (inline-styles, Theme-Assets, - Markdown-Renderer). Eigener Sprint. SVG-Upload-Risiko (oben) - bleibt bis dahin ungemildert. + - **CSP läuft Report-Only seit 2026-05-12.** `hooks.server.ts` + setzt `Content-Security-Policy-Report-Only` mit restriktiver + Policy (default-src 'self', script-src 'self', img-src + 'self' data: blob:, connect-src whitelist auf + cardecky-api/auth.mana.how/share/mcp). Violations laufen in + die Browser-DevTools-Console. Nach 1-2 Tagen Live-Beobachtung + via `CARDS_CSP_ENFORCE=true` auf Enforce flippen. SVG-Upload- + Risiko ist dann gemildert (script-src verbietet inline-JS in + SVG-Inlinen). - **CORS lässt `localhost` in Prod durch — gebaut 2026-05-12, noch nicht deployed.** Live-Probe hatte `Access-Control-Allow-Origin: http://localhost:9999` gezeigt. @@ -291,10 +296,10 @@ einiges: `/api/v1/me/delete` (Self-Service) liefern jetzt `storage_ok: true|false` + optional `storage_error` im Response. Frontend zeigt im Account-Delete-Flow eine Fehler-Toast wenn `storage_ok=false`. -- **Service-Key-Rotation** — ein statisches `CARDS_DSGVO_SERVICE_KEY` - in env, kein Revoke. Phase F-1 ist in `service-key.ts:11` - geplant (Verifikation gegen `mana-auth.apps.app_service_keys`). - Bis dahin: Rotation-Playbook in `docs/playbooks/` festschreiben. +- **Service-Key-Rotation-Playbook — gebaut 2026-05-12.** + `docs/playbooks/SERVICE_KEY_ROTATION.md` mit 5-Schritt-Rotation, + Tabus, Phase-F-1-Übergang. Bis die mana-auth-managed Variante + fertig ist, ist das die SoT. --- diff --git a/docs/playbooks/SERVICE_KEY_ROTATION.md b/docs/playbooks/SERVICE_KEY_ROTATION.md new file mode 100644 index 0000000..468d61a --- /dev/null +++ b/docs/playbooks/SERVICE_KEY_ROTATION.md @@ -0,0 +1,121 @@ +# Service-Key-Rotation — Cards + +Stand: 2026-05-12. + +`CARDS_DSGVO_SERVICE_KEY` ist heute ein **statischer** Service-Key, +gegen den `apps/api/src/middleware/service-key.ts` constant-time- +vergleicht. mana-admin verwendet ihn für DSGVO-Fan-Out +(`POST /api/v1/dsgvo/{export,delete}`). + +Bis Phase F-1 die Verifikation gegen `mana-auth.apps.app_service_keys` +ersetzt, bleibt Rotation manuell. Dieses Playbook hält den Ablauf +festgenagelt. + +--- + +## Wann rotieren + +- **Sofort** bei Verdacht auf Leak (Logs, env-Datei, Container-Image, + alte Backups, geleakte Screenshots). +- **Routinemäßig** alle 6 Monate. +- **Nach Personalwechsel** in der Plattform-Crew. +- **Nach jeder Notfall-`docker compose exec`-Session**, in der der + Key sichtbar war. + +## Rotation in 5 Schritten + +### 1. Neuen Key generieren + +```bash +openssl rand -hex 32 +# Beispiel: msk_2cfa7… (Prefix nach Konvention msk_ für „Mana Service Key") +``` + +### 2. Neuen Key auf der Prod-Box eintragen + +```bash +ssh mana-server +cd ~/projects/cards +nano infrastructure/.env.production +# CARDS_DSGVO_SERVICE_KEY=msk_ ← ersetzen +``` + +`.env.production` ist git-ignored und liegt ausschließlich auf der +Box. Nicht ins Repo committen. + +### 3. cards-api neustarten + +```bash +docker compose -f infrastructure/docker-compose.production.yml \ + --env-file infrastructure/.env.production \ + up -d cards-api +``` + +Kein Rebuild nötig — env-only-Change. + +### 4. Neuen Key bei den Callern eintragen + +Aktueller Single-Caller: **mana-admin**. + +```bash +# mana-Plattform: env-Datei für mana-admin updaten + Service neustarten +ssh mana-server +cd ~/projects/mana +nano .env.production +# CARDS_DSGVO_SERVICE_KEY_FOR_FANOUT=msk_ ← oder wie auch immer +# die mana-admin-env-Variable benannt ist +docker compose -f infrastructure/docker-compose.production.yml \ + up -d mana-admin +``` + +Wenn weitere S2S-Caller dazukommen (z.B. mana-research): hier +ergänzen. + +### 5. Verifizieren + alten Key invalidiert + +```bash +# Alter Key MUSS jetzt 401 geben: +curl -i -H "X-Service-Key: " \ + https://cardecky-api.mana.how/api/v1/dsgvo/export?user_id=... +# Erwartet: 401 service_key_invalid + +# Neuer Key MUSS funktionieren: +curl -i -H "X-Service-Key: " \ + https://cardecky-api.mana.how/api/v1/dsgvo/export?user_id=... +# Erwartet: 200 mit JSON-Bundle +``` + +Audit-Log-Zeile prüfen: +```bash +docker logs cards-api --since 5m | grep '"event":"dsgvo.export"' | tail +``` +Sollte den letzten erfolgreichen Aufruf mit `auth_mode: "service-key"` +zeigen. + +--- + +## Was nach Phase F-1 anders wird + +Phase F-1 ersetzt die `serviceKeyAuth()`-Middleware durch +Verifikation gegen `mana-auth.apps.app_service_keys`. Damit: + +- Keys leben in der DB, nicht in env. +- Rotation = `UPDATE apps.app_service_keys SET key_hash = … WHERE app_id = 'cards'`. +- Multiple Keys parallel möglich (overlap-Rotation ohne Downtime). +- Revocation via Soft-Delete-Flag. + +Bis dahin: dieses Playbook. + +--- + +## Tabu + +- **Nie** den Service-Key in Commit-Messages, PR-Beschreibungen oder + Chat-Threads klartext hinschreiben. +- **Nie** im selben Step neuen Key generieren *und* alten Caller + weiterlaufen lassen — kurz Downtime ist OK, blindes Doppel- + Akzeptieren ist es nicht. (Die Middleware hat heute nur einen + Slot.) +- **Nie** `CARDS_DSGVO_SERVICE_KEY` im Compose-File hart vorgeben — + bleibt env-var-Reference (`${CARDS_DSGVO_SERVICE_KEY:?missing}`), + damit der Compose-Default „fail-loud" ist.