meldestelle/docs/99_Journal/2026-03-09_Session_Log_Keycloak_Haertung.md

23 KiB
Raw Blame History

type status owner last_update
Journal ACTIVE Curator 2026-03-09

Journal - 2026-03-09

📝 Zusammenfassung

Keycloak-Härtung der Realm-Konfiguration (meldestelle-realm.json). Aufbauend auf dem Infrastruktur-Fix vom 2026-03-06 (korrektes start --optimized, Healthcheck) wurden nun die Client-Konfigurationen und Sicherheitseinstellungen auf einen produktionsreifen Stand gebracht.

🛠️ Änderungen

1. config/docker/keycloak/meldestelle-realm.json

Wildcard aus webOrigins entfernt

  • Vorher: api-gateway und web-app hatten "*" in webOrigins (CORS-Sicherheitslücke).
  • Nachher: Nur explizit erlaubte Origins (localhost:*, app.meldestelle.at).

Neuer Client: frontend-client (KMP Desktop & Mobile)

  • Public Client mit PKCE S256 (kein Client-Secret nötig, sicher für native Apps).
  • Redirect URIs: meldestelle://callback (Custom URI Scheme für Desktop), http://localhost:* (Dev), https://app.meldestelle.at/* (Prod).
  • Kein directAccessGrantsEnabled — verhindert Password-Grant-Flow (unsicher für native Apps).

Password-Policy gestärkt

  • Vorher: length(8) — zu schwach.
  • Nachher: length(10) and digits(1) and upperCase(1) and specialChars(1) and notUsername(undefined).

post.logout.redirect.uris konfiguriert

  • Alle relevanten Clients (api-gateway, web-app, frontend-client) haben nun korrekte Logout-Redirect-URIs gemäß OIDC Back-Channel Logout Standard.

📚 Gelerntes / Entscheidungen

  • meldestelle://callback als Custom URI Scheme: Für KMP Compose Desktop ist ein eigenes URI-Schema der sichere Standard (kein offener HTTP-Port nötig). Muss im OS registriert werden.
  • PKCE S256 ist Pflicht für Public Clients: Verhindert Authorization Code Interception Attacks. Keycloak 26.x unterstützt dies nativ.
  • frontend-client vs. web-app: Klare Trennung: web-app für Browser-basierte Web-App, frontend-client für native KMP Desktop/Mobile-App.

🐛 Bugfix: Keycloak Import-Fehler invalidPasswordMinSpecialCharsMessage (2026-03-09)

Root Cause

Beim Keycloak Härtungs-Step wurde die Password-Policy auf length(10) and digits(1) and upperCase(1) and specialChars(1) and notUsername(undefined) gestärkt. Der Test-User admin in meldestelle-realm.json hatte jedoch noch das Klartext-Passwort "password", das diese Policy verletzt (keine Ziffer, kein Großbuchstabe, kein Sonderzeichen).

Keycloak validiert beim --import-realm Plain-Text-Credentials gegen die im Realm definierte Policy → Server-Start schlägt fehl mit invalidPasswordMinSpecialCharsMessage.

Fix

  • config/docker/keycloak/meldestelle-realm.json: User admin Credential "password""Admin1234!" (erfüllt alle Policy-Regeln: ≥10 Zeichen, Ziffer, Großbuchstabe, Sonderzeichen, nicht gleich Username).

Gelerntes

Keycloak validiert Plain-Text-Credentials im Realm-JSON beim Import gegen die Realm-Policy. Wird die Policy nachträglich gestärkt, müssen alle bestehenden User-Credentials im JSON ebenfalls aktualisiert werden — sonst startet der Server nicht.


Micrometer Upgrade (2026-03-09, gleiche Session)

Verifiziert: micrometer = "1.16.1" und micrometerTracing = "1.6.1" waren bereits korrekt in gradle/libs.versions.toml gesetzt — kein Code-Change erforderlich. Archiv-Roadmaps entsprechend abgehakt.

Zipkin Integration (2026-03-09, gleiche Session)

Analyse ergab: Infrastruktur war bereits zu ~90% korrekt aufgebaut. Einzige Lücke war eine falsche Propagation-Konfiguration im Gateway.

Befund

  • dc-infra.yaml: Zipkin-Container (openzipkin/zipkin:3, Port 9411) bereits vorhanden.
  • monitoring-client-Bundle: enthält micrometer-tracing-bridge-brave, zipkin-reporter-brave, zipkin-sender-okhttp3 — von allen Services eingebunden.
  • monitoring-defaults.properties: setzt management.zipkin.tracing.endpoint=http://localhost:9411/api/v2/spans als Default, überschreibbar via MANAGEMENT_ZIPKIN_TRACING_ENDPOINT.
  • dc-backend.yaml: setzt MANAGEMENT_ZIPKIN_TRACING_ENDPOINT=http://zipkin:9411/api/v2/spans für alle Services inkl. Gateway.

Fix: Gateway application.yaml

  • management.tracing.propagation.type: w3cb3 (B3 ist das native Format von Brave/Zipkin; W3C wäre für OpenTelemetry — Mismatch hätte Trace-Korrelation zwischen Gateway und Services gebrochen).
  • management.zipkin.tracing.endpoint: Explizit mit lokalem Default + ENV-Override ergänzt.
  • TRACING_SAMPLING_PROBABILITY: Konsistent mit Services via ENV-Variable.

Fix: Gateway build.gradle.kts

  • Redundante direkte micrometer-tracing-bridge-brave-Dependency entfernt (bereits transitiv via monitoring-client vorhanden).

OIDC Client im Frontend (2026-03-09, gleiche Session)

Login-Flow mit PKCE Authorization Code Flow (S256) für frontend-client implementiert.

Neue Dateien

  • frontend/core/auth/src/commonMain/.../Sha256.kt: Pure Kotlin SHA-256 (FIPS 180-4) + Base64URL-Encoding — kein expect/actual, läuft auf JVM/JS/Wasm.
  • frontend/core/auth/src/commonMain/.../PkceHelper.kt: Code Verifier, Code Challenge (S256), State Generator.
  • frontend/core/auth/src/commonMain/.../OidcCallback.kt: OidcCallbackResult sealed class + expect launchOidcFlow(), consumePendingOidcCallback(), getOidcRedirectUri().
  • frontend/core/auth/src/jvmMain/.../OidcCallback.jvm.kt: Eingebetteter HttpServer (Port 18080) + Desktop.browse() + CompletableDeferred (Timeout 5 min).
  • frontend/core/auth/src/jsMain/.../OidcCallback.js.kt: window.location.href Redirect + URL-Parameter-Parsing beim App-Start + History-Bereinigung via replaceState.

Geänderte Dateien

  • frontend/core/domain/.../AppConstants.kt: KEYCLOAK_CLIENT_IDfrontend-client, OIDC-Konstanten ergänzt.
  • frontend/core/auth/src/commonMain/.../AuthApiClient.kt: buildAuthorizationUrl() (PKCE URL-Builder) + exchangeCodeForToken() (Code → Token).
  • frontend/core/auth/src/commonMain/.../LoginViewModel.kt: isOidcLoading-State, startOidcFlow(), handleOidcCallbackResult(), JS-Init-Callback-Check.
  • frontend/core/auth/src/commonMain/.../LoginScreen.kt: Divider + OutlinedButton „Mit Keycloak anmelden" mit Spinner bei laufendem Flow.

Architektur-Entscheidungen

  • Kein ktor-client-auth: Der OIDC-Flow wird manuell implementiert — ktor-client-auth unterstützt Authorization Code + PKCE nicht nativ für KMP.
  • Pure Kotlin SHA-256: Kein expect/actual nötig — kotlin.math + reine Bitoperationen reichen aus.
  • JVM-Callback-Server auf localhost:18080: Standard-Muster für Desktop-Apps (RFC 8252 „OAuth 2.0 for Native Apps").
  • JS-Redirect-Flow: Kein Popup — volle Seitenweiterleitung. Code Verifier wird in sessionStorage gespeichert (nur aktueller Tab).
  • State-Validierung: CSRF-Schutz via State-Parameter-Vergleich im ViewModel.

Gateway CircuitBreaker (2026-03-09, gleiche Session)

Verifikation ergab: Der ursprüngliche ClassNotFoundException-Bug ist durch Spring Cloud 2025.0.1 vollständig behoben.

Befund

  • Dependency: spring-cloud-starter-circuitbreaker-reactor-resilience4j — korrekte Reactive-Variante für WebFlux/Gateway.
  • GatewayConfig.kt: CircuitBreaker korrekt als Route-Filter konfiguriert:
    circuitBreaker {
        it.name = "pingServiceCB"
        it.fallbackUri = URI.create("forward:/fallback/ping")
    }
    
  • FallbackController.kt: Fallback-Endpunkt /fallback/ping implementiert.
  • GatewayMetricsConfig.kt: CB-Events werden als Prometheus-Counter (gateway_circuit_breaker_events_total) erfasst.
  • Resilience4j 2.3.0: Kompatibel mit Spring Cloud 2025.0.1 / Spring Boot 3.5.x — kein Versionskonflikt.

Fazit

Kein Code-Change erforderlich. Der Fix war implizit durch das Spring Cloud Downgrade auf 2025.0.1 bereits enthalten.

Docker-Stabilität End-to-End (2026-03-09, gleiche Session)

Vollständige Analyse aller Docker Compose Dateien (dc-infra, dc-backend, dc-ops, dc-gui) sowie .env und gemounteter Konfigs.

Befund & Fixes

Fix 1: .envKC_COMMAND Regression 🔴

  • Problem: .env hatte KC_COMMAND=start-dev --import-realm — exakt der Bug vom 2026-03-06 Log. Das pre-built Keycloak-Image startet im falschen Dev-Modus, OIDC-Flow schlägt fehl.
  • Fix: KC_COMMAND=start --optimized --import-realm

Fix 2: base-application.yaml — Valkey Env-Var-Namen 🟠

  • Problem: base-application.yaml (gemountet in allen Backend-Services) nutzte ${SPRING_DATA_REDIS_HOST} / ${SPRING_DATA_REDIS_PORT} / ${SPRING_DATA_REDIS_PASSWORD} als Env-Var-Namen. dc-backend.yaml setzt aber nur SPRING_DATA_VALKEY_* → Redis-Host fiel auf localhost-Default zurück statt auf den valkey-Container.
  • Fix: Env-Var-Namen auf ${SPRING_DATA_VALKEY_HOST}, ${SPRING_DATA_VALKEY_PORT}, ${SPRING_DATA_VALKEY_PASSWORD} umgestellt. Spring Boot Property-Pfad spring.data.redis.* bleibt korrekt.

Fix 3: gateway/application.yaml — Ungültiger Property-Namespace 🟠

  • Problem: Gateway nutzte spring.data.valkey.* — kein valider Spring Boot Auto-Konfigurations-Namespace. Spring Boot kennt nur spring.data.redis.* für die Redis/Lettuce-Autoconfiguration.
  • Fix: spring.data.valkeyspring.data.redis (Env-Vars SPRING_DATA_VALKEY_* bleiben).

Verifiziert: Korrekte Konfigurationen

  • Startup-Reihenfolge: Postgres → Keycloak → Consul → Valkey → Gateway/Ping (via depends_on + Healthchecks)
  • Netzwerk: Alle Services im meldestelle-network
  • Zipkin: service_started (stateless, kein Healthcheck nötig)
  • Consul Healthcheck: curl auf /v1/status/leader
  • Keycloak Healthcheck: curl auf localhost:9000/health/ready

🐛 Bugfix: Keycloak Healthcheck schlägt fehl obwohl Keycloak läuft (2026-03-09)

Symptom

docker compose --profile infra up -d — Keycloak startet korrekt (Port 8080 + 9000 gebunden, Realm importiert), wird von Docker aber nie als healthy markiert. Dieses Problem existierte bereits früher und führte damals zur Entfernung des Healthchecks.

Root Cause 1: /health/ready vs. /health/live

Keycloak 26.x führt beim Start den JGroups-Cluster-Join-Prozess aus. Bei Single-Node-Betrieb (kein anderer Knoten im Netz) versucht Keycloak 10× einem Ghost-Node beizutreten — jeder Versuch dauert 2 Sekunden = 20 Sekunden Verzögerung nach dem eigentlichen Startup.

  • /health/ready wartet auf vollständige Cluster-Formation → gibt während der JOIN-Phase 503 zurück.
  • /health/live prüft nur ob der Prozess läuft — unabhängig vom Cluster-Status.

Mit start_period: 60s und interval: 15s feuerte der erste Check in genau diesem 20s-Fenster → false-negative → nach 10 Retries → unhealthy.

Root Cause 2: KC_HOSTNAME_STRICT_HTTPS — deprecated v1-Option

WARNING: Hostname v1 options [hostname-strict-https] are still in use erscheint in jedem Startup-Log. In Keycloak 26.x wurde das Hostname-System auf v2 umgestellt. KC_HOSTNAME_STRICT_HTTPS ist ein v1-Parameter der ignoriert wird — HTTP-Zugriff wird ausschließlich über KC_HTTP_ENABLED gesteuert.

Fixes in dc-infra.yaml

Parameter Vorher Nachher Begründung
test …/health/ready …/health/live Kein false-negative während JGroups-JOIN
start_period "60s" "120s" Mehr Puffer für langsamen First-Start (Realm-Import)
interval "15s" "20s" Weniger Stress während Startup-Phase
timeout "5s" "10s" Mehr Zeit für Endpoint-Antwort
retries "10" (String) 10 (Integer) Korrekter YAML-Typ
KC_HOSTNAME_STRICT_HTTPS gesetzt entfernt Deprecated v1-Option, erzeugt Warning

Gelerntes

  • /health/live ist für Single-Node-Keycloak die richtige Wahlready ist für Cluster-Szenarien konzipiert wo mehrere Nodes koordiniert hochfahren müssen.
  • retries in Docker Compose Healthcheck sollte als Integer, nicht als String angegeben werden.

Ping Service — Tracer Bullet Analyse & Fix (2026-03-09, gleiche Session)

Vollständige Analyse des Ping Service Stacks (Backend → Gateway → Frontend). Ergebnis: Die gesamte fachliche Implementierung war bereits vorhanden. Ein einziger kritischer Bug gefunden und behoben.

Befund: Stack vollständig implementiert

  • Backend (backend/services/ping/ping-service): Domain, Application, Persistence, Controller, Security (at.mocode.infrastructure.security via @ComponentScan), DB-Migrations V1+V2 — vollständig.
  • Contracts (contracts/ping-api): PingResponse, EnhancedPingResponse, HealthResponse, PingEvent, PingApi — vollständig.
  • Gateway (backend/infrastructure/gateway): Routing /api/ping/**stripPrefix(1) → Ping Service mit CircuitBreaker Fallback — korrekt.
  • Frontend (frontend/features/ping-feature): PingApiKoinClient, PingViewModel, PingScreen, PingSyncService, Koin DI — vollständig.
  • Shell-Integration: MainApp.ktAppScreen.Ping eingebunden, „Ping-Service"-Button auf Home-Screen, Navigation korrekt.

Fix: PingApiKoinClient.kt — Query-Parameter-Mismatch 🔴

  • Problem: syncPings() sendete ?lastSyncTimestamp=... als Query-Parameter. Backend @RequestParam erwartet ?since=... — Delta-Sync lieferte immer alle Daten.
  • Fix: url.parameters.append("lastSyncTimestamp", ...)url.parameters.append("since", ...)

Gitea-Pipeline & Runner Analyse (2026-03-09, gleiche Session)

Befund: Aktiver CI/CD-Kern

  • .gitea/workflows/docker-publish.yaml ist der einzige aktiv ausgeführte Workflow — grün seit 06.03.2026.
  • Matrix: 4 Services (keycloak, api-gateway, ping-service, web-app), max-parallel: 1 (RAM-Schutz).
  • Native linux/arm64-Builds auf VM 102 (10.0.0.23), Push in interne Registry 10.0.0.22:3000.

Verifiziert (Screenshot 2026-03-09_12-47)

  • insecure-registries: ["10.0.0.22:3000"] permanent in /etc/docker/daemon.json gesetzt
  • act_runner: active (running) seit 06.03.2026, enabled; preset: enabled
  • systemctl is-enabled act_runnerenabled

Bereinigung .github/-Ordner

  • Problem: 6 GitHub Actions Workflows in .github/workflows/ — auf Gitea nie ausgeführt (toter Code).
  • Zusätzlich: ci-main.yml, deploy-proxmox.yml, ssot-guard.yml referenzierten falschen Pfad docker/docker-compose.yaml.
  • Fix: pr-guard.yml (prüft hartcodierte Versionen in build.gradle.kts) nach .gitea/workflows/pr-guard.yaml migriert.
  • Fix: Gesamten .github/-Ordner gelöscht.
  • youtrack-sync.yml war nicht aktiv und wurde mitgelöscht — kein Handlungsbedarf.

Aktive Workflows nach Bereinigung

Datei Trigger Zweck
.gitea/workflows/docker-publish.yaml Push main Build & Push aller Docker-Images
.gitea/workflows/pr-guard.yaml Pull Request Prüft hartcodierte Versionen in Gradle-Dateien

Bugfix: Keycloak Healthcheck — Finale Korrektur (Port 8080 → 9000)

Problem: Keycloak 26.5.5 startet sauber (~15s), wird aber dauerhaft nicht healthy.

Root Cause (final): Der vorherige Fix hatte Port 9000 → 8080 geändert — das war falsch.

Port Zweck Health-Endpoint
8080 Haupt-HTTP (Login, Admin-Console, APIs) kennt /health/* nicht → 404
9000 Management (Actuator, Health, Metrics) /health/live, /health/ready

curl http://localhost:8080/health/live404curl -f schlägt fehl → unhealthy.

Fix dc-infra.yaml:

test: ["CMD-SHELL", "curl -sf --max-time 5 http://localhost:9000/health/live || exit 1"]
interval: "15s"
timeout: "10s"
retries: 5
start_period: "90s"

Warum /health/live statt /health/ready? /health/ready wartet auf JGroups-Cluster-Formation (Single-Node: 10×2s JOIN-Timeout = 20s Verzögerung). /health/live prüft nur Prozess-Liveness — sofort UP sobald Quarkus läuft.

start_period: 90s — Puffer für First-Run mit DB-Schema-Init (Liquibase-Migrations ~1520s).

🔜 Nächste Schritte

  • TLS/HTTPS — Langfristig: KC_HOSTNAME_STRICT_HTTPS=true setzen, sobald TLS eingerichtet ist.
  • Entries Service — Beginn der Implementierung des ersten echten Fach-Services ("Nennungen").

Bugfix: Keycloak Healthcheck — Echter Root Cause (curl nicht vorhanden)

Zeitpunkt: 2026-03-09 ~15:15

Root Cause

Das Keycloak-Image quay.io/keycloak/keycloak:26.5.5 basiert auf ubi9-micro — einem minimalen Red Hat Image * ohne curl / wget / Package Manager*. Alle bisherigen Port-Fixes (8080 → 9000) waren korrekt, aber der curl-Befehl scheiterte im Container mit command not found. Keycloak selbst lief die ganze Zeit einwandfrei — nur der Healthcheck-Befehl fehlte das nötige Tool.

Fix

Datei Änderung
dc-infra.yaml Healthcheck: curl → Bash /dev/tcp-Trick

Neuer Healthcheck (kein externes Tool nötig):

test: ["CMD-SHELL", "exec 3<>/dev/tcp/localhost/9000 && printf 'GET /health/live HTTP/1.0\r\nHost: localhost\r\n\r\n' >&3 && cat <&3 | grep -q '"status":"UP"'"]

Warum /dev/tcp funktioniert

  • Bash hat /dev/tcp eingebaut — verfügbar ohne Installation
  • exec 3<>/dev/tcp/localhost/9000 öffnet TCP-Verbindung auf Port 9000
  • printf ... >&3 sendet einen minimalen HTTP/1.0 GET-Request
  • cat <&3 | grep -q '"status":"UP"' liest Antwort und prüft Health-Status

Gelernt

Vor jedem curl-basierten Healthcheck prüfen: Ist curl im Container-Image vorhanden? Minimale Images (ubi9-micro, distroless, alpine ohne curl-Paket) haben es nicht. Sicherer Standard: /dev/tcp (bash built-in) statt curl — funktioniert immer wenn bash vorhanden ist.


Bugfix: Keycloak Healthcheck — grep-Pattern Leerzeichen

Zeitpunkt: 2026-03-09 ~15:27

Root Cause

Nach dem /dev/tcp-Fix war der Healthcheck immer noch unhealthy (ExitCode=1). Der grep-Pattern lautete '"status":"UP"' (kein Leerzeichen nach :), die tatsächliche JSON-Antwort von Keycloak enthält jedoch "status": "UP" (mit Leerzeichen nach dem Doppelpunkt).

Beweis via docker exec:

HTTP/1.0 200 OK
{
    "status": "UP",
    "checks": []
}

grep nach "status":"UP" → kein Match → ExitCode 1 → unhealthy.

Fix

Datei Änderung
dc-infra.yaml grep-Pattern: "status":"UP""status": "UP"

Vorher:

test: ["CMD-SHELL", "... | grep -q '\"status\":\"UP\"'"]

Nachher:

test: ["CMD-SHELL", "... | grep -q '\"status\": \"UP\"'"]

Ergebnis

Nach docker compose up -d keycloak: Status healthy, 2x ExitCode=0 bestätigt.

Gelernt

Den Healthcheck-Befehl immer manuell via docker exec testen und die exakte API-Antwort prüfen — JSON-Formatierung (Leerzeichen, Einrückung) kann je nach Keycloak-Version variieren. Sicherer wäre grep -q '"UP"' ohne den Key, um Formatierungsunterschiede zu vermeiden.


Optimierung: Healthcheck grep-Pattern vereinfacht

Zeitpunkt: 2026-03-09 ~15:30

Maßnahme

Das grep-Pattern wurde weiter vereinfacht auf grep -q '"UP"', um robust gegen JSON-Formatierungsänderungen (Leerzeichen, Einrückung) in zukünftigen Keycloak-Versionen zu sein.

Datei Vorher Nachher
dc-infra.yaml grep -q '"status": "UP"' grep -q '"UP"'

Ergebnis

Status healthy, ExitCode=0 bestätigt. Pattern ist nun versionsunabhängig.


Bugfix: Login fehlgeschlagen — directAccessGrantsEnabled für frontend-client

Zeitpunkt: 2026-03-09 ~15:41

Symptom

Web-App (localhost:4000) zeigte beim direkten Username/Passwort-Login:

Login fehlgeschlagen: HTTP 400 - {"error":"unauthorized_client","error_description":"Client not allowed for direct access grants"}

Root Cause

AuthApiClient.kt verwendet AppConstants.KEYCLOAK_CLIENT_ID = "frontend-client" mit grant_type=password (Resource Owner Password Credentials / ROPC-Flow). Der frontend-client hatte in meldestelle-realm.json jedoch directAccessGrantsEnabled: false — bewusst deaktiviert als reiner PKCE-Client, dabei aber vergessen, dass die Web-App ROPC nutzt.

Fix

Datei Änderung
meldestelle-realm.json frontend-client: directAccessGrantsEnabled: falsetrue

Live-Update via Admin REST API (kein Neustart nötig):

# Token holen
TOKEN=$(curl -s -X POST "http://localhost:8180/realms/master/protocol/openid-connect/token" \
  -d "username=kc-admin&password=kc-password&grant_type=password&client_id=admin-cli" ...)

# Client-UUID ermitteln & PUT mit directAccessGrantsEnabled:true
curl -X PUT "http://localhost:8180/admin/realms/meldestelle/clients/$UUID" \
  -H "Authorization: Bearer $TOKEN" -d "$UPDATED_CLIENT_JSON"
# → HTTP 204

Verifikation

curl -X POST ".../realms/meldestelle/protocol/openid-connect/token" \
  -d "username=admin&password=Admin%231234&grant_type=password&client_id=frontend-client"
# → "token_type":"Bearer"  ✅

Gelernt

--import-realm in Keycloak importiert Realms nur beim ersten Start (wenn Realm noch nicht existiert). Änderungen an der realm.json nach dem ersten Import müssen entweder via Admin REST API oder durch Löschen des Volumes (Datenverlust!) neu eingespielt werden. Für Dev-Umgebungen: KC_COMMAND=start --optimized --import-realm mit separatem Reset-Script oder Admin-API-Skript für Konfigurationsänderungen vorbereiten.