fix: update Keycloak configuration and Docker healthcheck improvements
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Successful in 6m48s
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Successful in 6m40s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 1m44s
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Successful in 1m31s

- Enabled `directAccessGrants` for `frontend-client` in `meldestelle-realm.json` to support ROPC login flow.
- Strengthened admin credentials in realm configuration to meet password policy requirements.
- Upgraded Keycloak to `26.5.5` with updated Docker healthcheck logic:
  - Replaced `curl` with bash `/dev/tcp` for compatibility with `ubi9-micro` image.
  - Switched health endpoint from `/ready` to `/live` for single-node use.
  - Adjusted healthcheck timings (`start_period`, `timeout`, `interval`) for smoother startup.
- Removed deprecated v1 hostname parameter `KC_HOSTNAME_STRICT_HTTPS`.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
2026-03-09 15:49:58 +01:00
parent 4ae11e6668
commit d6a484c347
8 changed files with 289 additions and 18 deletions
@@ -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 ~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):**
```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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB