didi-lot2-backend/backend/services/gateway-auth-layer/didiKeycloak/MIGRATION.md
2026-07-10 03:39:53 -07:00

9.1 KiB

Keycloak — migrat pe SSO cluster (2026-04-30)

TL;DR: containerul local keycloak (Keycloak 22) nu mai rulează. DIDI folosește acum SSO cluster la https://<sso-extern> (Keycloak 26 HA, 3 replicas pe Dev Docker Swarm). Realm didi-clients migrat cu toate datele (users, clients, groups). Theme custom didi-clients-theme deployed pe SSO via bind mount pe nodurile Swarm.


Ce era aici (înainte de 2026-04-30)

Container keycloak (Keycloak 22, image quay.io/keycloak/keycloak:22.0) definit în backend/production/docker-compose.yml. Single-instance pe didi12 (10.11.10.12:28000). DB pe Patroni cluster (keycloak_db). Hostname fix KC_HOSTNAME_URL=https://didi365.eu/auth.

Folosit doar de DIDI. Theme custom didi-clients-theme (purple gradient).

De ce migrare

  1. Single-tenant lock-in: Keycloak local servea doar didi365.eu. Pentru alte produse (lege365, rafai, etc.) ar fi trebuit instanțe separate sau hostname dinamic complex.
  2. Single-point-of-failure: 1 container, 1 host. Down când didi12 down.
  3. DB password issue: 2026-04-29 cineva a rotat parola keycloak user în Patroni → connection pool fail → service degraded.
  4. SSO cluster live: 2026-04-29 Lucian a deploy-uit Keycloak 26 HA pe Dev Swarm, cu hostname public <sso-extern>.

Ce e acum

SSO Cluster

Componentă Detaliu
Hostname public <sso-extern> (DNS public, cert Let's Encrypt valid)
Hostname intern <sso-extern-admin> (admin URL via KC_HOSTNAME_ADMIN)
IP public 82.79.147.181 (port-forward la Traefik intern)
Edge router Traefik central (10.11.10.171:443)
Keycloak version 26.0 (quay.io/keycloak/keycloak:26.0)
HA 3 replicas, max 1 per node
Cluster Dev Docker Swarm (10.11.50.151-154)
DB Patroni cluster (10.11.50.166:5000/keycloak_db)
Cache ispn (Infinispan, dns.query=tasks.keycloak)
Stack name keycloak-cluster (docker service ls)

Hostname configuration

KC_HOSTNAME: https://<sso-extern>          # public URL (used in tokens, redirects)
KC_HOSTNAME_ADMIN: https://<sso-extern-admin> # admin endpoints (internal-only via 307 redirect)
KC_HOSTNAME_STRICT_BACKCHANNEL: false
KC_PROXY_HEADERS: xforwarded

Issuer in tokens: https://<sso-extern>/realms/didi-clients. Endpoints (no /auth/ prefix in K26):

  • /realms/didi-clients/.well-known/openid-configuration
  • /realms/didi-clients/protocol/openid-connect/auth
  • /realms/didi-clients/protocol/openid-connect/token
  • /realms/didi-clients/protocol/openid-connect/certs (JWKS)

Realm-uri pe SSO

  • master — admin Keycloak (NU folosi pentru apps)
  • didi-clients — DIDI customer-facing app (migrat 1:1 din local)
  • didi-admins — DIDI admin panel (creat de Lucian, neutilizat încă)

Theme deployment (didi-clients-theme)

Themes sunt mounted ca bind mount pe fiecare nod Swarm:

mount:
  type: bind
  source: /var/keycloak-themes/didi-clients-theme
  target: /opt/keycloak/themes/didi-clients-theme
  readonly: true

Adăugat via docker service update --mount-add (nu via stack file). Pentru ca toate 3 replicas să găsească tema, fișierele trebuie pe toate 4 nodurile Swarm (10.11.50.151-154).

Procedură deploy theme update

  1. Pack theme local pe didi12:

    cd backend/services/gateway-auth-layer/didiKeycloak/themes
    tar -czf /home/admin365/didi-clients-theme.tar.gz didi-clients-theme/
    
  2. Pe dev-docker-mgr (Swarm manager — 10.11.50.151):

    scp admin365@10.11.10.12:/home/admin365/didi-clients-theme.tar.gz /tmp/
    sudo tar -xzf /tmp/didi-clients-theme.tar.gz -C /var/keycloak-themes/
    for n in 152 153 154; do
      scp /tmp/didi-clients-theme.tar.gz admin365@10.11.50.$n:/tmp/
      ssh -t admin365@10.11.50.$n 'sudo tar -xzf /tmp/didi-clients-theme.tar.gz -C /var/keycloak-themes/'
    done
    sudo docker service update --force keycloak-cluster_keycloak
    
  3. Verify:

    curl -ksm 5 https://<sso-extern>/resources/<version>/login/didi-clients-theme/css/login.css | head
    

Theme structure (PatternFly v4 specific)

Keycloak 26 default theme (keycloak) folosește PatternFly v4 markup. Custom theme cu parent=keycloak moștenește template-urile, dar PF4 are reguli CSS specifice care necesită overrides în login.css:

  • Password input wrap — în <div class="pf-c-input-group"> cu eye-icon button. Necesită CSS specific pentru .pf-c-input-group .pf-c-form-control
  • Pseudo-element ::after pe button — PF4 button-uri au <button>::after { border: ...; position: absolute } care creează "chenarul". Trebuie killed cu display:none !important și content:none !important

Vezi themes/didi-clients-theme/login/resources/css/login.css secțiunile PatternFly v4 input-group fix și Eye-icon button pentru detalii.

Cluster Kong JWT consumer

Consumer didi-keycloak-users în cluster Kong (10.11.10.176:8001) are 4 issuers acceptate pentru tranziție smooth:

https://didi365.eu/auth/realms/didi-clients          # legacy local Keycloak
https://<host-local>/auth/realms/didi-clients # legacy intern alias
https://<sso-extern-admin>/realms/didi-clients         # SSO intern (transition)
https://<sso-extern>/realms/didi-clients            # SSO public canonical (CURRENT)

Toate 4 au același RSA public key (Lucian a exportat realm-ul cu key preserved la migrare). Tokens emise acum de SSO au iss=https://<sso-extern>/realms/didi-clients — cluster Kong validează corect.

Vezi didiKong/declarative/kong-cluster.yml pentru config consumer.

SPA configuration (didi-frontend)

web/src/services/keycloak.service.ts:

const keycloakUrl =
  (import.meta.env.VITE_KEYCLOAK_URL as string | undefined) ||
  'https://<sso-extern>';

const keycloak = new Keycloak({
  url: keycloakUrl,
  realm: 'didi-clients',
  clientId: 'didi-web-app',
});

SPA construiește toate URL-urile (auth, token, logout) relativ la https://<sso-extern>. Browser-ul user-ului se redirectează direct la SSO (NU prin proxy local). Cookie-urile Keycloak sunt setate pentru <sso-extern> domain.

Ce a rămas local

În folder-ul ăsta (didiKeycloak/):

  • themes/ — sursa originală a temelor (didi-clients-theme, didi-ai-theme, didi-backend-theme). Folosită ca master pentru deployment pe SSO cluster.
  • realm-import/didi-clients-realm.json — backup realm config (legacy, nu mai e mounted)
  • Dockerfile — pentru imagine custom Keycloak 22 (legacy, nemai folosit)

Volume cleanup

Volume-ul didi-production-keycloak-data (DB H2 local + cache) e păstrat 1 săptămână pentru rollback safety.

# Remove după 2026-05-07:
docker volume rm didi-production-keycloak-data

Rollback (în caz de probleme)

Containerul local Keycloak nu mai există. Pentru rollback:

  1. Reset DB password (era broken pentru user keycloak):

    ALTER USER keycloak WITH PASSWORD 'keycloak123';
    
  2. Restore docker-compose:

    git revert <commit-care-sterge-keycloak-din-compose>
    cd backend/production && docker compose up -d keycloak
    
  3. Update SPA să re-folosească local:

    • În frontend/web/src/services/keycloak.service.ts, schimbă URL la ${origin}/auth
    • Rebuild + redeploy didi-frontend
  4. Restore /auth proxy în frontend/nginx-default.conf (la cluster Kong sau local Keycloak)

Linkuri rapide

  • SSO public: <https:///admin/master/console/> (admin: admin365 / parolă din credentials)
  • SSO intern: https://<sso-extern-admin>/admin/ (via /etc/hosts → 10.11.10.171, doar intern)
  • Cluster setup repo: landingzone/keycloak-sso/ (git.finesynergy.eu/lucian/landingzone)
  • Stack file: pe dev-docker-mgr (Lucian) sau în repo

Status

  • Migrare aplicată: 2026-04-30
  • Container local oprit + șters
  • Service definition removed din docker-compose
  • Theme deployed pe SSO 3 replicas
  • End-to-end auth flow verificat (login, dashboard, JWT validare prin cluster Kong)
  • Volume didi-production-keycloak-data păstrat până 2026-05-07

Lecții importante

  1. KC_HOSTNAME schimbă tot răspunsul Keycloak — toate URL-urile generate, issuer-ul în tokens, cookie domain. Pentru aplicații web publice trebuie hostname public DNS-resolvable (NU .local).

  2. Keycloak 26 a renunțat la /auth prefix — endpoints sunt /realms/.... Theme cu parent=keycloak moștenește templates PF4. Custom CSS trebuie să acopere PF4 markup specific (input-group, button::after pseudo-elements).

  3. KC_HOSTNAME_ADMIN — separă admin de URL-ul public (security best practice). Admin via <sso-extern-admin> (intern), client-facing via <sso-extern>.

  4. Bind mount pe Swarm cere theme files pe toate nodurile (constraint max 1 per node cu 3 replicas → cel puțin 3 din 4 noduri rulează task). Fișiere pe 4 noduri = sigur.

  5. Realm export/import păstrează RSA signing keys — JWT-urile vechi rămân valide după migrare. Cluster Kong consumer poate avea multiple issuers cu același public_key.

  6. PatternFly v4 button ::after — chenarul "fantomă" pe button-uri vine din pseudo-element absolut poziționat. Trebuie content: none !important ca să-l killezi.