Docs/Deployment/Keycloak Provisioning

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), plus agentic-ai-mobile, agentic-ai-app, agentic-ai-ide reserved for external clients that are not part of this repository
Social IdPs in the template
github, google, microsoft — committed disabled with REPLACE_WITH_*_CLIENT_ID placeholders; the reconcile step enables them when *_CLIENT_ID/*_CLIENT_SECRET are set
Compose
quay.io/keycloak/keycloak:26.0.7, command start --import-realm (KEYCLOAK_START_COMMAND=start-dev only for local use); realm JSON and theme mounted read-only; Full tier stores data in keycloak-db, Lite in the keycloak-data volume
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

ScriptWhat 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.comGrants the platform-admin realm role (platform-operator console access).
deploy/keycloak/repair-federated-profiles.shRepairs federated users whose email/firstName/lastName were blanked.
deploy/keycloak/restore-default-role.pyRe-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

Documented gotchas

SymptomCause and fix
Every /api/tenant/** call 500s; no tenant_id claimKeycloak 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 resetRun the reconcile; the template cannot carry the real core client secret or the realm-admin role mapping.
Login works, every API call 401Issuer mismatch — see the callout above.
"Invalid redirect URI" on loginRealm 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 JWKSPoint them at the internal URL (http://keycloak:8080/...), as Compose does; an edge proxy in front of auth. may block Python user agents.
Realm SMTPSet 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