diff --git a/.env b/.env index 1dca8f5a..1dc6d11f 100644 --- a/.env +++ b/.env @@ -49,7 +49,7 @@ SPRING_DATA_VALKEY_PORT=6379 SPRING_DATA_VALKEY_PASSWORD=valkey-password # --- KEYCLOAK --- -KEYCLOAK_IMAGE_TAG=26.4 +KEYCLOAK_IMAGE_TAG=26.5.5 KC_HEAP_MIN=512M KC_HEAP_MAX=1024M # Lokale Entwicklung: start-dev (kein Pre-Build nötig, kein --optimized) diff --git a/backend/infrastructure/gateway/Dockerfile b/backend/infrastructure/gateway/Dockerfile index 5da87e17..b0b6b096 100644 --- a/backend/infrastructure/gateway/Dockerfile +++ b/backend/infrastructure/gateway/Dockerfile @@ -5,8 +5,8 @@ # =================================================================== # === CENTRALIZED BUILD ARGUMENTS === -ARG GRADLE_VERSION -ARG JAVA_VERSION +ARG GRADLE_VERSION=9.3.1 +ARG JAVA_VERSION=25 ARG BUILD_DATE ARG VERSION diff --git a/backend/services/ping/Dockerfile b/backend/services/ping/Dockerfile index e07fed32..ca1542b4 100644 --- a/backend/services/ping/Dockerfile +++ b/backend/services/ping/Dockerfile @@ -5,8 +5,8 @@ # =================================================================== # === CENTRALIZED BUILD ARGUMENTS === -ARG GRADLE_VERSION -ARG JAVA_VERSION +ARG GRADLE_VERSION=9.3.1 +ARG JAVA_VERSION=25 ARG BUILD_DATE ARG VERSION diff --git a/config/docker/keycloak/Dockerfile b/config/docker/keycloak/Dockerfile index 9d0f9c44..6e08ca61 100644 --- a/config/docker/keycloak/Dockerfile +++ b/config/docker/keycloak/Dockerfile @@ -1,16 +1,18 @@ # =================================================================== # Production-Ready Keycloak Dockerfile # =================================================================== -# Based on: quay.io/keycloak/keycloak:26.4 +# Based on: quay.io/keycloak/keycloak:26.5.5 # Features: # - Pre-built optimized image (faster startup) # - Security hardening # - Health monitoring # =================================================================== -ARG KEYCLOAK_IMAGE_TAG +ARG KEYCLOAK_IMAGE_TAG=26.5.5 FROM quay.io/keycloak/keycloak:${KEYCLOAK_IMAGE_TAG} +ARG KEYCLOAK_IMAGE_TAG=26.5.5 + LABEL maintainer="Meldestelle Development Team" LABEL description="Production-ready Keycloak for Meldestelle authentication" LABEL version="${KEYCLOAK_IMAGE_TAG}" diff --git a/config/docker/keycloak/meldestelle-realm.json b/config/docker/keycloak/meldestelle-realm.json index 323dd71b..ee821925 100644 --- a/config/docker/keycloak/meldestelle-realm.json +++ b/config/docker/keycloak/meldestelle-realm.json @@ -239,7 +239,7 @@ "protocol": "openid-connect", "standardFlowEnabled": true, "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, + "directAccessGrantsEnabled": true, "serviceAccountsEnabled": false, "authorizationServicesEnabled": false, "attributes": { @@ -283,7 +283,7 @@ "credentials": [ { "type": "password", - "value": "password", + "value": "Admin#1234", "temporary": false } ], @@ -395,4 +395,4 @@ "clientSessionMaxLifespan": "0", "clientOfflineSessionIdleTimeout": "0" } -} \ No newline at end of file +} diff --git a/dc-infra.yaml b/dc-infra.yaml index 4f9f3c01..80485b89 100644 --- a/dc-infra.yaml +++ b/dc-infra.yaml @@ -76,8 +76,8 @@ services: context: . dockerfile: config/docker/keycloak/Dockerfile args: - KEYCLOAK_IMAGE_TAG: "${KEYCLOAK_IMAGE_TAG:-26.4}" - image: "${DOCKER_REGISTRY:-git.mo-code.at/mocode-software}/keycloak:${KEYCLOAK_IMAGE_TAG:-26.4}" + KEYCLOAK_IMAGE_TAG: "${KEYCLOAK_IMAGE_TAG:-26.5.5}" + image: "${DOCKER_REGISTRY:-git.mo-code.at/mocode-software}/keycloak:${KEYCLOAK_IMAGE_TAG:-26.5.5}" container_name: "${PROJECT_NAME:-meldestelle}-keycloak" restart: unless-stopped profiles: [ "infra", "all" ] @@ -93,8 +93,8 @@ services: KC_HOSTNAME: "${KC_HOSTNAME:-localhost}" # WICHTIG: false erlaubt Zugriff über beliebige Hostnamen (nötig für Server-Betrieb ohne TLS) KC_HOSTNAME_STRICT: "${KC_HOSTNAME_STRICT:-false}" - # WICHTIG: false erlaubt HTTP (kein HTTPS-Zwang) – für Entwicklung und HTTP-only Server - KC_HOSTNAME_STRICT_HTTPS: "${KC_HOSTNAME_STRICT_HTTPS:-false}" + # KC_HOSTNAME_STRICT_HTTPS wurde entfernt — deprecated v1-Option in Keycloak 26.x (hostname v2). + # HTTP-Zugriff wird ausschließlich über KC_HTTP_ENABLED gesteuert. KC_HTTP_ENABLED: "true" # Admin-Interface explizit auf allen Interfaces binden (0.0.0.0) KC_HTTP_MANAGEMENT_PORT: "9000" @@ -109,11 +109,15 @@ services: postgres: condition: "service_healthy" healthcheck: - test: [ "CMD-SHELL", "curl -sf http://localhost:9000/health/ready || exit 1" ] + # Keycloak basiert auf ubi9-micro — curl/wget sind NICHT im Image enthalten! + # Lösung: Bash /dev/tcp — kein externes Tool nötig, funktioniert auf jedem bash-Image. + # Management-Port 9000: Health-Endpoints (/health/live, /health/ready) laufen hier. + # /health/live: prüft nur Prozess-Liveness — kein Warten auf JGroups-Cluster-Formation. + 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 '\"UP\"'" ] interval: "15s" - timeout: "5s" - retries: "10" - start_period: "60s" + timeout: "10s" + retries: 5 + start_period: "90s" volumes: - "./config/docker/keycloak:/opt/keycloak/data/import:Z" # start --optimized nutzt das pre-built Image (kc.sh build im Dockerfile) diff --git a/docs/99_Journal/2026-03-09_Session_Log_Keycloak_Haertung.md b/docs/99_Journal/2026-03-09_Session_Log_Keycloak_Haertung.md index 44546ae0..36487079 100644 --- a/docs/99_Journal/2026-03-09_Session_Log_Keycloak_Haertung.md +++ b/docs/99_Journal/2026-03-09_Session_Log_Keycloak_Haertung.md @@ -41,6 +41,31 @@ Sicherheitseinstellungen auf einen produktionsreifen Stand gebracht. - **`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 @@ -171,6 +196,51 @@ gemounteter Konfigs. - **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 Wahl** — `ready` 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 @@ -226,7 +296,202 @@ fachliche Implementierung war bereits vorhanden. Ein einziger kritischer Bug gef | `.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`:** + +```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=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):** + +```yaml +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:** + +```yaml +test: ["CMD-SHELL", "... | grep -q '\"status\":\"UP\"'"] +``` + +**Nachher:** + +```yaml +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: false` → `true` | + +Live-Update via Admin REST API (kein Neustart nötig): + +```bash +# 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 + +```bash +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. diff --git a/docs/ScreenShots/docker-web-app_fehlschlag_2026-03-09_15-39.png b/docs/ScreenShots/docker-web-app_fehlschlag_2026-03-09_15-39.png new file mode 100644 index 00000000..13ecaf39 Binary files /dev/null and b/docs/ScreenShots/docker-web-app_fehlschlag_2026-03-09_15-39.png differ