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

203 lines
9.1 KiB
Markdown

# 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
```yaml
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:
```yaml
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:
```bash
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):
```bash
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:
```bash
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`:
```typescript
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.
```bash
# 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`):
```sql
ALTER USER keycloak WITH PASSWORD 'keycloak123';
```
2. **Restore docker-compose**:
```bash
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://<sso-extern>/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.