Keycloak auf Azure — Prod-Deployment-Design¶
Design-Dokument für den produktiven Keycloak unter auth.targetshot.app
und auth-admin.targetshot.app. Staging bleibt unverändert auf dem
bestehenden on-prem Docker-Setup hinter HAProxy.
Zielbild¶
- Kein OS-Patching: Managed Compute via Azure App Service Linux Container, nicht Azure VM.
- Admin-Only-Intranet-Schutz: Pfad-basierte App-Service Access
Restrictions ersetzen die haproxy-Regeln für
/admin/*und/realms/master/*. Ein Keycloak-Worker deckt beide Hostnames ab — kein Cluster-Setup nötig. - Managed Postgres: Azure Database for PostgreSQL Flexible Server in einem delegated Subnet, Private Endpoint, kein Public Access.
- Kleine Baseline, vertikal skalierbar: B2 App-Service-Plan + B1ms Postgres. Hochstufen im Portal ohne Redesign.
Komponenten¶
| Ressource | SKU / Config | Zweck |
|---|---|---|
| Resource Group | targetshot-kc in Germany West Central |
Kapselung |
| App Service Plan | Linux B2 (2 vCPU, 3.5 GB) |
Compute für Keycloak |
| Web App | keycloak-prod, Docker Container aus ACR |
Keycloak-Instanz |
| Azure Container Registry | Basic (targetshotregistry oder ähnlich) |
Eigenes Keycloak-Image mit Themes |
| Postgres Flex | Standard_B1ms, 32 GB SSD, Burstable |
KC-Datenbank |
| VNet | targetshot-kc-vnet, 10.20.0.0/16 |
Private Netzwerk |
Subnet app |
10.20.1.0/24, delegated to Microsoft.Web/serverFarms |
App Service Regional VNet Integration |
Subnet db |
10.20.2.0/24, delegated to Microsoft.DBforPostgreSQL/flexibleServers |
PG Flex Private Endpoint |
| Private DNS Zone | privatelink.postgres.database.azure.com |
Namensauflösung für PG intern |
Domains & DNS¶
Zwei Hostnames zeigen auf dieselbe App Service, Trennung passiert erst bei den Access Restrictions.
Cloudflare-Records¶
| Typ | Name | Inhalt | Proxy |
|---|---|---|---|
| CNAME | auth |
keycloak-prod.azurewebsites.net |
DNS only |
| CNAME | auth-admin |
keycloak-prod.azurewebsites.net |
DNS only |
| TXT | asuid.auth |
Verification-ID aus Azure | — |
| TXT | asuid.auth-admin |
gleiche Verification-ID | — |
DNS-only, nicht proxied. Wenn Cloudflare proxy't, sieht App Service die CF-Edge-IP und unsere Pfad-IP-Regeln fallen zusammen. Später kann Azure Front Door die DDoS-/WAF-Rolle übernehmen.
Azure-Domain-Binding¶
- App Service → Custom domains → Add custom domain →
auth.targetshot.app. - Verification-ID an Cloudflare als
asuid.authTXT-Record. - Validate → Azure bindet die Domain an den Web App.
- „Create App Service Managed Certificate" klicken → Azure macht HTTP-01-Challenge über den CNAME, Cert lebt so lange die Domain gebunden ist und wird automatisch erneuert.
- Dieselbe Prozedur für
auth-admin.targetshot.app.
Access Restrictions (admin-only-intranet)¶
Regeln am App Service, ausgewertet in Priority-Reihenfolge (niedriger = zuerst). Die Allow-Regeln sind also erste Wahl, dann greifen die Denies als Fallback.
| Priority | Path filter | Action | Source |
|---|---|---|---|
| 50 | /admin/* |
Allow | Business-IP (93.241.78.8/32) + weitere Allowlist-IPs |
| 51 | /realms/master/* |
Allow | dieselbe Allowlist |
| 100 | /admin/* |
Deny | 0.0.0.0/0 |
| 101 | /realms/master/* |
Deny | 0.0.0.0/0 |
| 1000 | — | Allow (default) | 0.0.0.0/0 |
Ergebnis: /realms/<normal>/protocol/openid-connect/... ist für jeden
erreichbar, /admin/* und /realms/master/* nur aus der Allowlist.
Keycloak setzt via KC_HOSTNAME_ADMIN die Admin-Konsole auf
auth-admin.targetshot.app, aber die Security liegt auf Pfad-Ebene —
dadurch ist egal ob jemand die Admin-Hostname-Variante rät.
Keycloak-Konfiguration¶
Env-Vars in der Web App (oder via az webapp config appsettings):
KC_DB=postgres
KC_DB_URL=jdbc:postgresql://targetshot-kc.privatelink.postgres.database.azure.com:5432/keycloak?sslmode=require
KC_DB_USERNAME=keycloak
KC_DB_PASSWORD=<Key Vault Reference>
KC_HOSTNAME=auth.targetshot.app
KC_HOSTNAME_ADMIN=auth-admin.targetshot.app
KC_HOSTNAME_STRICT=true
KC_PROXY_HEADERS=xforwarded
KC_HOSTNAME_BACKCHANNEL_DYNAMIC=true
KC_HTTP_ENABLED=true
KC_HEALTH_ENABLED=true
KC_METRICS_ENABLED=true
KC_THEME_CACHE_ENABLED=true
WEBSITES_PORT=8080
KC_HTTP_ENABLED=true weil App Service TLS selbst terminiert und den
Container per HTTP anspricht. KC_PROXY_HEADERS=xforwarded sorgt
dafür, dass Keycloak die echte Client-IP aus dem Forwarded-Header
liest.
Passwort gehört in Azure Key Vault; die Web App nutzt Managed Identity
+ eine Key-Vault-Reference (@Microsoft.KeyVault(...)).
Image-Pipeline¶
Themes + ggf. Providers werden ins Image gebaut, nicht gemountet. Theme-Änderungen sind selten, Bind-Mounts auf App Service wären Azure Files = SMB = Mehraufwand ohne Gegenwert.
infrastructure/keycloak/Dockerfileerbt vonquay.io/keycloak/keycloak:<version>, kopiertinfrastructure/keycloak/themes/nach/opt/keycloak/themes/und ruftkc.sh buildfür das Production-Image auf.- GitHub Action baut das Image bei Push auf
infrastructure/keycloak/**, pusht nach ACR alsghcr.io/targetshot-app/keycloak:<sha>. az webapp config container setsetzt den Tag auf der Web App um; App Service zieht das neue Image und startet den Container neu.
Staging kann dieselbe Image-Pipeline mit anderen Tags nutzen, läuft aber weiterhin auf dem on-prem Host — dort bleibt Docker-Compose mit Bind-Mount für themes (einfacher Wechsel ohne Rebuild in Staging).
Datenbank-Initialisierung¶
- PG Flex mit Admin-User
pgadminanlegen. - Einmalig per
psql(vom App-Subnet aus, temporärer Jump-Container)CREATE DATABASE keycloak; CREATE USER keycloak WITH PASSWORD ...; GRANT ALL ON DATABASE keycloak TO keycloak;ausführen. - Passwort in Key Vault als Secret ablegen.
- Web-App zieht Credentials via Managed Identity + Key Vault Reference.
- Keycloak startet, führt Liquibase-Schema-Migrationen beim ersten Boot aus.
Realm-Import¶
Bestehende Realms aus dem aktuellen Staging-Keycloak exportieren
(kc.sh export --dir /tmp/export --realm <realm>), ins neue Image
kopieren oder beim ersten Start per KC_IMPORT_REALM einspielen.
Realm-Export ist idempotent; User-Migrations separat planen (Postgres
Dump + Restore ist einfacher als Realm-Level-Export für User).
Scaling-Pfad¶
| Achse | Schritt | Operativ |
|---|---|---|
| Compute vertikal | B2 → S1 → P0v3 → P1v3 | „Scale up" im Portal, ohne Downtime auf gleichem Plan |
| DB vertikal | B1ms → B2s → D2ds_v4 | Reboot-Fenster |
| Compute horizontal | ab S1 möglich | Keycloak-Cluster via Infinispan JDBC-Ping konfigurieren |
| Multi-Region | Azure Front Door + Second App | Failover-Policy, eigenes Doc |
Kosten (Germany West Central, ca., pay-as-you-go)¶
| Posten | Monatlich |
|---|---|
App Service Plan Linux B2 |
~€25 |
Postgres Flex B1ms + 32 GB |
~€18 |
| Azure Container Registry Basic | ~€4 |
| Egress (<100 GB/mo) | ~€0 |
| VNet / Subnets | €0 |
| Managed TLS Cert | €0 |
| Summe | ~€47 / Monat |
Was dieses Dokument noch nicht enthält¶
- Bicep- oder Terraform-IaC-Dateien — nächster Schritt.
- GitHub-Actions-Workflow für Image-Build und App-Service-Deployment.
- Realm-Migrations-Runbook (Export/Import, User-Dump).
- Rollback-Plan, falls der Cutover scheitert (DNS-TTL, Fallback auf bestehendes on-prem Staging-Keycloak).
Diese Themen kommen in eigene Runbooks, sobald die IaC-Basis steht.