Keycloak Provisioning
How the agentic-ai realm, its clients, the branded theme and the social
identity providers get into Keycloak — and, more importantly, what an import
cannot do. The one idea to hold: the realm JSON is a seed; the live realm is
the source of truth, because --import-realm never re-imports an
existing realm.
Standing rule: issuer consistency
Keycloak pins its hostname (KC_HOSTNAME = PUBLIC_AUTH_URL), so tokens
carry <PUBLIC_AUTH_URL>/realms/agentic-ai. Core must accept exactly that
issuer; list any other (for example the old one during a domain change) in
KEYCLOAK_ADDITIONAL_ISSUERS. A mismatch 401s every authenticated call while
browser logins still work. See ADR-0005.
- Realm template
deploy/keycloak/agentic-ai-realm.json;${env.X:default}placeholders are resolved from the keycloak container's environment at first import- Clients in the template
agentic-ai-core(confidential),agentic-ai-web,agentic-ai-platform-admin(public, PKCE), plusagentic-ai-mobile,agentic-ai-app,agentic-ai-idereserved for external clients that are not part of this repository- Social IdPs in the template
github,google,microsoft— committed disabled withREPLACE_WITH_*_CLIENT_IDplaceholders; the reconcile step enables them when*_CLIENT_ID/*_CLIENT_SECRETare set- Compose
quay.io/keycloak/keycloak:26.0.7, commandstart --import-realm(KEYCLOAK_START_COMMAND=start-devonly for local use); realm JSON and theme mounted read-only; Full tier stores data inkeycloak-db, Lite in thekeycloak-datavolume- Helm
deploy/helm/charts/keycloak: realm imported on first start, then a post-install/post-upgrade Job reconciles it; or bring any OIDC issuer
Provisioning model
flowchart LR J[realm JSON in deploy/keycloak] -->|mounted| K[keycloak container] T[theme: deploy/keycloak/themes/agentic-ai] -->|mounted| K K -->|--import-realm, FIRST start only| L[(live realm in Keycloak DB)] R[reconcile-keycloak.sh / Helm reconcile Job] -->|kcadm, idempotent| L H[admin console / scripts] --> L
--import-realm is a no-op once the realm exists
Editing the JSON and restarting has no effect on a live realm. Client redirect URIs and web
origins are written from PUBLIC_APP_URL/PUBLIC_ADMIN_URL at the
first import only; after a domain change, update the agentic-ai-web and
agentic-ai-platform-admin clients in the admin console (in domain mode it is reachable
only on 127.0.0.1:8180: ssh -L 8180:127.0.0.1:8180 server).
Scripts in the repository
| Script | What it does |
|---|---|
deploy/keycloak/reconcile-keycloak.sh (+ reconcile-realm.kcadm.sh) | Idempotent post-import fixes, run automatically by scripts/vegaduta up: (1) unmanagedAttributePolicy=ADMIN_EDIT; (2) sync the agentic-ai-core client secret from KEYCLOAK_CLIENT_SECRET; (3) realm-admin on the core service account; (4) SMTP from KEYCLOAK_SMTP_*; (5) Google/GitHub/Microsoft login from *_CLIENT_ID/_SECRET; (6) first-broker-login review profile and VERIFY_PROFILE off. The kcadm steps run inside the keycloak container. |
scripts/vegaduta make-admin you@example.com | Grants the platform-admin realm role (platform-operator console access). |
deploy/keycloak/repair-federated-profiles.sh | Repairs federated users whose email/firstName/lastName were blanked. |
deploy/keycloak/restore-default-role.py | Re-attaches the realm default-role composite to users that lost it (symptom: "Offline tokens are not allowed for this user"). Additive; supports --dry-run. |
scripts/keycloak/* | Local helpers: ensure-platform-admin-client.ps1 (creates a missing agentic-ai-platform-admin client), check-tenant-claim.ps1, delete-orphaned-user.ps1, grant-offline-access.mjs. |
Realm, clients, theme
- Redirect URIs per client.
agentic-ai-webandagentic-ai-platform-adminget your console origins at first import.agentic-ai-coreis confidential with the standard flow disabled —api.is a resource server and never receives a browser redirect. - Theme.
deploy/keycloak/themes/agentic-ai/holdslogin,accountandemail. Without the mount Keycloak silently falls back to the default theme. - Hostname.
KC_HOSTNAMEis the public auth URL; the domain overlay keeps the admin console onlocalhost(KC_HOSTNAME_ADMIN).
Documented gotchas
| Symptom | Cause and fix |
|---|---|
Every /api/tenant/** call 500s; no tenant_id claim | Keycloak 24+ drops undeclared attributes. Re-run the reconcile (scripts/vegaduta up). It must be ADMIN_EDIT, never ENABLED — ENABLED lets a user edit their own tenant_id and move into another tenant. |
| Signup 502 or 500 after a fresh import / DB reset | Run the reconcile; the template cannot carry the real core client secret or the realm-admin role mapping. |
| Login works, every API call 401 | Issuer mismatch — see the callout above. |
| "Invalid redirect URI" on login | Realm clients were imported with other URLs; fix them in the admin console. |
| "Offline tokens are not allowed" | Default-role composite missing; repair with restore-default-role.py or grant-offline-access.mjs. |
| Python services cannot fetch JWKS | Point them at the internal URL (http://keycloak:8080/...), as Compose does; an edge proxy in front of auth. may block Python user agents. |
| Realm SMTP | Set by the reconcile from KEYCLOAK_SMTP_*; the template carries no SMTP password. Verify with a real signup email after every change. |
Related: Identity & Access · Deployment Guide · Backup & Restore
Evidence & references
deploy/keycloak/(agentic-ai-realm.json,reconcile-keycloak.sh,reconcile-realm.kcadm.sh,repair-federated-profiles.sh,restore-default-role.py,themes/),scripts/keycloak/.deploy/compose/docker-compose.yml,lite.yml,docker-compose.domain.yml— keycloak service;deploy/helm/charts/keycloak/values.yaml.deploy/README.md— "Issuer consistency" and troubleshooting.