Zum Inhalt

Keycloak Service-Account (kora-platform-audit)

Der Audit-Poller liest Keycloak-Events über einen dedizierten Service-Account statt über den Master-Admin-Pfad (TODO-B2-03). Dieser Runbook beschreibt Bootstrap, Rotation, Re-Import und Troubleshooting.

Rolle im System

Der Client kora-platform-audit im kora-platform-Realm ist ein Client-Credentials-Client ohne User-Login-Flow:

  • serviceAccountsEnabled: true
  • publicClient: false, standardFlowEnabled: false, directAccessGrantsEnabled: false, implicitFlowEnabled: false
  • Rechte (strikt minimal): realm-management.view-events
  • Keine view-users, view-realm oder anderen Management-Rollen. Der Poller braucht sie nicht (Event-Payload enthält userId + details.username), und jede zusätzliche Rolle würde den Scope erweitern.

Der ehemalige Master-Admin-Pfad aus dem laufenden API-Container ist mit TODO-B2-03 eliminiert. Der KORA_KEYCLOAK_ADMIN_USERNAME- und KORA_KEYCLOAK_ADMIN_PASSWORD-Eintrag im API-Container existiert nicht mehr; die Bootstrap-CLI nutzt Master-PW nur ad-hoc via docker compose run --env-file .env.bootstrap.

Initiales Setup

Das Init-Script infra/keycloak/init-scripts/create-audit-service-account.sh legt den Client idempotent an und weist die Rolle zu. Einmalig beim ersten Deployment ausführen:

# Master-PW aus .env.platform sourcen (KC_BOOTSTRAP_ADMIN_PASSWORD)
set -a && source .env.platform && set +a

./infra/keycloak/init-scripts/create-audit-service-account.sh

Das Script gibt den automatisch generierten Client-Secret aus. Diesen Wert in .env.platform eintragen:

KORA_KEYCLOAK_AUDIT_CLIENT_ID=kora-platform-audit
KORA_KEYCLOAK_AUDIT_SECRET=<generated-value>

Danach: make redeploy-platform — der Poller nutzt die neuen ENVs.

Re-Run-Verhalten: Das Script erkennt einen existierenden Client und lässt den Secret unverändert. Re-Runs sind idempotent und sicher.

Bootstrap-CLI (einmalig pro Deployment)

Der initiale Operator-Admin wird nicht über den Service-Account angelegt — dafür bräuchte der Account manage-users + manage-realm-Rollen, was den Scope breit öffnen würde. Stattdessen nutzt die Bootstrap-CLI weiterhin den Master-PW, aber nur ad-hoc via Makefile-Wrapper (siehe Compose-Invocations):

# Empfohlen: tmpfs-Pfad (weg nach Reboot)
mkdir -p /run/user/$UID
cp .env.bootstrap.example /run/user/$UID/.env.bootstrap
$EDITOR /run/user/$UID/.env.bootstrap   # Master-PW eintragen

# ENVs in die aktuelle Shell laden:
set -a && source /run/user/$UID/.env.bootstrap && set +a

# Makefile-Wrapper: scheitert früh bei fehlenden Credentials,
# forwarded KC_BOOTSTRAP_ADMIN_* ephemer an einen docker-exec-Aufruf
# gegen den laufenden kora-platform-api-Container:
make platform-bootstrap cmd="bootstrap-operator-admin --email <admin-email>"

# Nach Nutzung: auf tmpfs-Pfad ist das nach Reboot eh weg. Explizit
# löschen für sofortige Bereinigung:
shred -u /run/user/$UID/.env.bootstrap 2>/dev/null || \
  rm -f /run/user/$UID/.env.bootstrap

Der Wrapper nutzt intern docker exec -e VAR (ohne =value) — forwardet die aktuelle Shell-Env nur an den einen exec-Prozess. Der Master-PW erscheint nicht auf der Kommandozeile (nicht in docker inspect oder ps aux sichtbar) und ist ausschließlich im Kontext dieses exec-Aufrufs verfügbar, nicht container-weit — docker exec kora-platform-api env zeigt den Wert nicht.

Alternative für frisches Deployment ohne laufenden API-Container: make dev-platform starten, dann obigen Flow.

Alternativen zum tmpfs-Pfad:

  • ./env.bootstrap im Projektverzeichnis — .gitignore schützt vor versehentlichem Commit, shred -u nach Nutzung
  • Secrets-Manager (Bitwarden, pass, 1Password) statt File

Bei Multi-User-Hosts oder strikten Ops-Umgebungen ist tmpfs zu bevorzugen — keine persistierten Master-Credentials auf der Platte.

Betroffene Bootstrap-Subcommands

Stand TODO-B2-03: genau ein Subcommand nutzt Master-PW:

  • kora-platform bootstrap-operator-admin — legt Operator-Admin-User im kora-platform-Realm an, weist operator-admin-Rolle zu, versendet UPDATE_PASSWORD-Mail (48h).

Neue Bootstrap-Subcommands, die Master-PW brauchen würden, müssen analog auf KC_BOOTSTRAP_ADMIN_* + .env.bootstrap-Pfad umgestellt werden.

Client-Secret-Rotation

Empfohlene Frequenz: alle 90 Tage oder bei Personalwechsel im Ops-Team. Kalender- oder Cron-Reminder sinnvoll — ohne Erinnerung wird ein manueller Rotationsprozess in der Regel nicht eingehalten.

Rotation-Prozedur

  1. Keycloak-Admin-UI öffnen: https://auth.kora.luki-net.org/admin/master/console/#/kora-platform/clients
  2. Client kora-platform-audit → Tab "Credentials" → "Regenerate Secret"
  3. Neuen Wert in .env.platform unter KORA_KEYCLOAK_AUDIT_SECRET eintragen
  4. make redeploy-platform — API-Container startet neu, Poller holt Token mit neuem Secret
  5. Erfolg verifizieren: docker logs kora-platform-api --since 90s | grep -i audit — keine invalid_client-Fehler

Alternativ: Init-Script mit Force-Regenerate (optional)

Das aktuelle Init-Script regeneriert bewusst nicht. Für automatisierte Rotation könnte ein --rotate-Flag ergänzt werden — aktuell out-of-scope (v1.x).

Realm-Re-Import-Verhalten

Das Keycloak-Image importiert Realm-JSONs nur beim ersten Realm-Create (nicht bei Service-Restart). Der Service-Account-Client wird nicht aus dem Realm-JSON importiert — das Init-Script legt ihn nachträglich via Admin-API an.

Bei vollständigem Realm-Re-Import (manuelles Löschen + Neuanlage des Realms):

  1. Realm wird mit Default-Clients aus kora-platform-realm.json importiert (ohne kora-platform-audit)
  2. Init-Script erneut laufen lassen: ./infra/keycloak/init-scripts/create-audit-service-account.sh
  3. Neuer Secret wird generiert und in .env.platform eintragen
  4. make redeploy-platform

In Produktion sind Realm-Re-Imports zu vermeiden — alle User- und Consent-Daten gehen verloren. Bei tatsächlichem Bedarf: Backup + geplante Downtime + Smoke-Test-Runbook.

Rollback bei Problemen

Falls der Service-Account-Pfad nach einem Keycloak-Upgrade oder einer anderen Änderung ausfällt:

  1. Master-PW-Pfad vorübergehend wiederherstellen:
    git revert 0337458
    # Master-PW als ENV wieder aufnehmen — siehe .env.platform-Historie
    make redeploy-platform
    
  2. Ursache analysieren (Keycloak-Version, Realm-Config, Secret-Drift)
  3. Fix implementieren, re-mergen

Der Master-Admin-User in Keycloak bleibt existent — er ist nur aus dem Dauer-ENV des API-Containers entfernt. Rollback re-aktiviert ihn in unter 5 Minuten.

Troubleshooting

Poller: HTTP 401 mit invalid_client oder unauthorized_client

Das Client-Secret in .env.platform passt nicht zum Wert in Keycloak — oder der Client wurde gelöscht.

# Secret-Drift prüfen:
set -a && source .env.platform && set +a
docker exec kora-platform-api curl -sX POST \
  http://kora-platform-keycloak:8080/realms/kora-platform/protocol/openid-connect/token \
  -d "grant_type=client_credentials" \
  -d "client_id=$KORA_KEYCLOAK_AUDIT_CLIENT_ID" \
  -d "client_secret=$KORA_KEYCLOAK_AUDIT_SECRET" | python3 -m json.tool

Fix: Rotation-Prozedur (oben) durchlaufen oder Init-Script erneut ausführen (bei gelöschtem Client).

Poller: HTTP 403 auf /events

Rollen-Mapping fehlt. Via Admin-UI:

  1. Realm kora-platform → Clients → kora-platform-audit
  2. Tab "Service accounts roles" → "Assign role"
  3. Filter: realm-managementview-events auswählen → "Assign"

Alternativ: Init-Script erneut laufen lassen.

Prometheus-Counter-Spikes

Der Poller exportiert audit_poller_auth_failures_total{reason="..."}. Häufige Werte:

  • reason="invalid_client" — Secret-Drift, siehe oben
  • reason="invalid_grant" — Client-ID falsch, Client deaktiviert
  • reason="forbidden" — Rolle fehlt, siehe oben
  • reason="network" — DNS/Connect-Probleme zum Keycloak-Container
  • reason="missing_secret"KORA_KEYCLOAK_AUDIT_SECRET leer
  • reason="unknown" — alles andere; Logs prüfen

Grafana-Alert empfohlen: increase(audit_poller_auth_failures_total[5m]) > 3.

Referenzen

  • Konzept: Fundament §2.2 (Vendor-Audit), §15.4 (Keycloak-Init)
  • Code: src/kora_platform/services/audit_poller.py
  • Init-Script: infra/keycloak/init-scripts/create-audit-service-account.sh
  • Settings: src/kora_platform/config.pykeycloak_audit_client_id, keycloak_audit_secret
  • Integration-Test: tests/integration/test_audit_service_account.py