cards/docs/playbooks/SERVICE_KEY_ROTATION.md
Till JS 5a29dd9a8c
Some checks are pending
CI / validate (push) Waiting to run
security(cards): CSP report-only + service-key rotation playbook
Folge-Hardening zu e1ddbf3, Cluster A2+A3 aus FEATURE_IDEAS.

* hooks.server.ts: restriktive CSP im Report-Only-Modus
  (default-src 'self', script-src 'self', connect-src whitelist
  auf cardecky-api/auth.mana.how/share/mcp). CARDS_CSP_ENFORCE=true
  flippt auf den scharfen Header.
* docs/playbooks/SERVICE_KEY_ROTATION.md: 5-Schritt-Rotation für
  CARDS_DSGVO_SERVICE_KEY bis Phase F-1 (mana-auth-managed Keys).

Forensik der Bypass-Periode 2026-05-08 → 2026-05-12 ist abgeschlossen:
nur 2 user_ids in der Cards-DB, beide legitim (tills95@gmail.com +
Smoke-Test-Sentinel c1a5, letztere via DSGVO-Endpoint aufgeräumt).
Kein ausgenutzter Bypass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:40:29 +02:00

3.4 KiB

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

openssl rand -hex 32
# Beispiel: msk_2cfa7…  (Prefix nach Konvention msk_ für „Mana Service Key")

2. Neuen Key auf der Prod-Box eintragen

ssh mana-server
cd ~/projects/cards
nano infrastructure/.env.production
# CARDS_DSGVO_SERVICE_KEY=msk_<neu>   ← ersetzen

.env.production ist git-ignored und liegt ausschließlich auf der Box. Nicht ins Repo committen.

3. cards-api neustarten

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.

# 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_<neu>   ← 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

# Alter Key MUSS jetzt 401 geben:
curl -i -H "X-Service-Key: <alter-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: <neuer-key>" \
  https://cardecky-api.mana.how/api/v1/dsgvo/export?user_id=...
# Erwartet: 200 mit JSON-Bundle

Audit-Log-Zeile prüfen:

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.