Project not found.
SOP: Keycloak Client & Realm Management
SOP: Keycloak Client & Realm Management
Purpose
This SOP describes how to create and manage Keycloak realms, clients, roles, and users on the pal-e platform. It covers both the Terraform path (primary — realm/client/role IaC via pal-e-services) and the admin console path (secondary — user management and emergency operations). The outcome is a working OIDC client with documented config, realm roles mapped to the ID token, and secrets wired into the consuming app's k8s deployment.
Background
The Keycloak server is deployed by
pal-e-platform/terraform/modules/keycloak/main.tf (namespace, secret, PVC, deployment, service, theme configmap). Image: quay.io/keycloak/keycloak:26.0.7, H2 dev-file DB, Tailscale funnel TLS termination.Realm and client configuration is managed via Terraform IaC in
pal-e-services/terraform/keycloak.tf using the mrparkers/keycloak provider (v5.0). Configuration is declared in k3s.tfvars. Four realms and five clients are currently managed this way.History: The original SOP (April 2026) was admin-console-only because no Terraform provider existed in the tree at that time. The IaC adoption has since landed in pal-e-services. The admin console remains the path for user management (creating users, assigning roles, resetting passwords) since users are not Terraform-managed.
Current State
| Realm | Purpose | Roles |
|---|---|---|
| <code>platform</code> | Pal-E Platform (Forgejo, Grafana, Harbor, admin dashboard) | (none) |
| <code>westside-basketball</code> | Westside Kings & Queens Basketball | admin, coach, player |
| <code>pal-e-docs</code> | Pal-E Docs | admin, user |
| <code>pal-enterprises</code> | Pal Enterprises | owner, client |
Prerequisites
- Access to
~/pal-e-services/terraform/on archbox (for Terraform path). - Tailscale-connected device with browser access to
https://keycloak.tail5b443a.ts.net/admin(for admin console path). - Keycloak admin credentials (stored in
~/secrets/pal-e-services/keycloak-admin.env; also in k8skeycloak-adminsecret inkeycloaknamespace). - Decision made up-front: public client (PKCE, no secret, browser-only SPA) or confidential client (server-side, secret stays on server). Match the consuming app's architecture.
- The exact production hostname for the consuming app (needed for redirect URIs and web origins).
Path A: Terraform (Primary — Realms, Clients, Roles)
Step 1: Add Realm (if new)
Edit
~/pal-e-services/terraform/k3s.tfvars. Add an entry to the keycloak_realms block:
Each project gets its own realm — do not reuse realms across projects.
Step 2: Add Client
Add an entry to the
keycloak_clients block in the same file:
Step 3: Apply
Step 4: Verify
Step 5: Retrieve Client Secret (confidential clients)
After
tofu apply, retrieve the client secret from the Keycloak admin console: Clients → {client} → Credentials tab → copy Client Secret. The Terraform provider uses ignore_changes = [client_secret], so the secret is generated by Keycloak and fetched manually.Path B: Admin Console (User Management)
Users are not Terraform-managed. Create and manage users via the admin console:
- Open
https://keycloak.tail5b443a.ts.net/admin. Log in with admin credentials. - Switch realm dropdown to the target realm — never operate against
master. - Users → Add user. Set username, email, first name, last name. Enable the user.
- Credentials tab → Set password (set Temporary to OFF for test users).
- Role mapping tab → Assign role → select realm roles.
Secrets Wiring
Four env vars needed in the consuming app's k8s secret:
| Key | Value | Source |
|---|---|---|
| <code>KEYCLOAK_URL</code> | <code>https://keycloak.tail5b443a.ts.net</code> | Static, platform-wide |
| <code>KEYCLOAK_REALM</code> | Realm name | Matches realm key in tfvars |
| <code>KEYCLOAK_CLIENT_ID</code> | Client ID | Matches client_id in tfvars |
| <code>KEYCLOAK_CLIENT_SECRET</code> | Generated by Keycloak | Admin console Credentials tab |
Update the k8s secret via
kubectl create secret generic ... --dry-run=client -o yaml | kubectl apply -f -. Then add secretKeyRef entries to pal-e-deployments/overlays/{app}/prod/deployment-patch.yaml.For apps using SOPS: store in
overlays/{app}/prod/{app}-secrets.enc.yaml via sops. Never commit unencrypted secrets.Network Policy
Keycloak has a default-deny ingress NetworkPolicy in
pal-e-platform/terraform/network-policies.tf. New apps must be added to the allowlist before they can reach Keycloak. Add the app's namespace to the existing policy and run tofu apply.OIDC Client Config Summary
| Field | Value | Notes |
|---|---|---|
| Protocol | OpenID Connect | — |
| Client ID | <code>{app-slug}</code> | Must match consuming app env var exactly |
| Client authentication | <code>ON</code> (confidential) or <code>OFF</code> (public+PKCE) | Drives whether a secret is generated |
| Standard flow | Enabled | Authorization Code flow |
| Implicit flow | Disabled | Deprecated, never used |
| Direct access grants | Disabled | ROPC is forbidden (deprecated in OAuth 2.1) |
| Service accounts | Disabled | Unless the app needs service-account flows |
| PKCE | <code>S256</code> | Mandatory for all clients |
| Redirect URIs | Narrowest path possible | Exact callback path for server-side; <code>/*</code> for SPA |
| Post-logout redirect URIs | App's logout landing | Required for RP-initiated logout |
| Web origins | App's bare origin | Drives CORS. Avoid <code>+</code> |
| Front channel logout | ON | Recommended for SLO |
| Access token lifespan | Realm default (5 min) | Override only with documented reason |
State Parameter / CSRF (Consuming App)
This SOP creates the Keycloak side. The consuming app must implement the OIDC
state parameter correctly — it is mandatory:- Generate a cryptographically random
statevalue (≥128 bits) per authorization request. - Store it tied to the user's session before redirecting to Keycloak.
- On callback, compare
statefrom the query string to the stored value using constant-time comparison. Mismatch → reject with HTTP 400. - For PKCE clients, the same lifecycle applies to
code_verifier.
Most OIDC libraries (OmniAuth, keycloak-js, PKCE helpers) handle this automatically.
Rules
- Always confirm the realm dropdown before any operation — never operate against
master. - One realm per project. Do not reuse realms across projects.
- One client per app. Do not reuse clients across apps.
- Always set PKCE Code Challenge Method to
S256— for both public and confidential clients. - Always disable Direct Access Grants (ROPC) and Implicit Flow.
- Always use the narrowest redirect URI path consistent with the app architecture.
- Always populate post-logout redirect URIs and web origins.
- Confidential client secrets live ONLY in Keycloak DB and k8s secrets (or SOPS-encrypted overlay). No other copy.
- The consuming app MUST implement OIDC
statevalidation on the callback. - Realms, clients, and roles go through Terraform (pal-e-services). Users go through admin console.
- Never modify existing clients as a side effect of adding a new one.
- Never change realm-level settings (theme, password policy, token lifespan) as a side effect of client work.
Related
convention-sveltekit-spa— pattern for public+PKCE clients (browser-only SPA flows).review-1096-2026-04-25— original scope investigation (admin-console era, now superseded by Terraform adoption).pal-e-services/terraform/keycloak.tf— Terraform resources for realms and clients.pal-e-services/terraform/k3s.tfvars— declarative realm/client/role config.pal-e-platform/terraform/modules/keycloak/main.tf— Keycloak server deployment.pal-e-platform/terraform/network-policies.tf— Keycloak ingress allowlist.