23 KiB
| 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-gatewayundweb-apphatten"*"inwebOrigins(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://callbackals 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.xunterstützt dies nativ. frontend-clientvs.web-app: Klare Trennung:web-appfür Browser-basierte Web-App,frontend-clientfü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: UseradminCredential"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ältmicrometer-tracing-bridge-brave,zipkin-reporter-brave,zipkin-sender-okhttp3— von allen Services eingebunden.monitoring-defaults.properties: setztmanagement.zipkin.tracing.endpoint=http://localhost:9411/api/v2/spansals Default, überschreibbar viaMANAGEMENT_ZIPKIN_TRACING_ENDPOINT.dc-backend.yaml: setztMANAGEMENT_ZIPKIN_TRACING_ENDPOINT=http://zipkin:9411/api/v2/spansfür alle Services inkl. Gateway.
Fix: Gateway application.yaml
management.tracing.propagation.type:w3c→b3(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 viamonitoring-clientvorhanden).
✅ 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:OidcCallbackResultsealed class + expectlaunchOidcFlow(),consumePendingOidcCallback(),getOidcRedirectUri().frontend/core/auth/src/jvmMain/.../OidcCallback.jvm.kt: EingebetteterHttpServer(Port 18080) +Desktop.browse()+CompletableDeferred(Timeout 5 min).frontend/core/auth/src/jsMain/.../OidcCallback.js.kt:window.location.hrefRedirect + URL-Parameter-Parsing beim App-Start + History-Bereinigung viareplaceState.
Geänderte Dateien
frontend/core/domain/.../AppConstants.kt:KEYCLOAK_CLIENT_ID→frontend-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-authunterstützt Authorization Code + PKCE nicht nativ für KMP. - Pure Kotlin SHA-256: Kein
expect/actualnö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
sessionStoragegespeichert (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/pingimplementiert.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: .env — KC_COMMAND Regression 🔴
- Problem:
.envhatteKC_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.yamlsetzt aber nurSPRING_DATA_VALKEY_*→ Redis-Host fiel auflocalhost-Default zurück statt auf denvalkey-Container. - Fix: Env-Var-Namen auf
${SPRING_DATA_VALKEY_HOST},${SPRING_DATA_VALKEY_PORT},${SPRING_DATA_VALKEY_PASSWORD}umgestellt. Spring Boot Property-Pfadspring.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 nurspring.data.redis.*für die Redis/Lettuce-Autoconfiguration. - Fix:
spring.data.valkey→spring.data.redis(Env-VarsSPRING_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/readywartet auf vollständige Cluster-Formation → gibt während der JOIN-Phase503zurück./health/liveprü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/liveist für Single-Node-Keycloak die richtige Wahl —readyist für Cluster-Szenarien konzipiert wo mehrere Nodes koordiniert hochfahren müssen.retriesin 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.securityvia@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.kt—AppScreen.Pingeingebunden, „Ping-Service"-Button auf Home-Screen, Navigation korrekt.
Fix: PingApiKoinClient.kt — Query-Parameter-Mismatch 🔴
- Problem:
syncPings()sendete?lastSyncTimestamp=...als Query-Parameter. Backend@RequestParamerwartet?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.yamlist 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 Registry10.0.0.22:3000.
Verifiziert (Screenshot 2026-03-09_12-47)
insecure-registries: ["10.0.0.22:3000"]permanent in/etc/docker/daemon.jsongesetzt ✅act_runner:active (running)seit 06.03.2026,enabled; preset: enabled✅systemctl is-enabled act_runner→enabled✅
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.ymlreferenzierten falschen Pfaddocker/docker-compose.yaml. - Fix:
pr-guard.yml(prüft hartcodierte Versionen inbuild.gradle.kts) nach.gitea/workflows/pr-guard.yamlmigriert. - Fix: Gesamten
.github/-Ordner gelöscht. youtrack-sync.ymlwar 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/live → 404 → curl -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 ~15–20s).
🔜 Nächste Schritte
- TLS/HTTPS — Langfristig:
KC_HOSTNAME_STRICT_HTTPS=truesetzen, 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/tcpeingebaut — verfügbar ohne Installation exec 3<>/dev/tcp/localhost/9000öffnet TCP-Verbindung auf Port 9000printf ... >&3sendet einen minimalen HTTP/1.0 GET-Requestcat <&3 | grep -q '"status":"UP"'liest Antwort und prüft Health-Status
Gelernt
Vor jedem
curl-basierten Healthcheck prüfen: Istcurlim Container-Image vorhanden? Minimale Images (ubi9-micro,distroless,alpineohne curl-Paket) haben es nicht. Sicherer Standard:/dev/tcp(bash built-in) stattcurl— 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 exectesten und die exakte API-Antwort prüfen — JSON-Formatierung (Leerzeichen, Einrückung) kann je nach Keycloak-Version variieren. Sicherer wäregrep -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: false → true |
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-realmin 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-realmmit separatem Reset-Script oder Admin-API-Skript für Konfigurationsänderungen vorbereiten.