Keycloak can run as a shared OIDC IdP in AWS (Fargate + Postgres) for non-production use: development stacks, demos, and ephemeral preview hosts. Local Docker Compose Keycloak remains unchanged for laptop development (docs/development/keycloak-oidc.md, ADR-0019).
The long-lived portal path that uses the production IdP is separate (module.app); do not point production traffic at this Keycloak.
- Hostname:
https://auth.<DOMAIN> - Public ALB + Fargate (
tofu/modules/sebt_keycloak) + dedicated Postgres - Realm/theme baked into
docker/keycloak - Consumers override API OIDC env toward Keycloak and omit production IdP client secrets when those would conflict
- Admin path protection: ALB listener rules keep
/realms/*(OIDC) public but deny/adminand/admin/*with HTTP 403 unless the request includes the bypass header from Secrets Manager (…-keycloak-admin-bypass) or comes from an optional allowlisted CIDR (keycloak_admin_ingress_cidrs/TF_VAR_keycloak_admin_ingress_cidrs). Preview deploy scripts send the bypass header automatically.
- Apply bootstrap so the Keycloak ECR repo exists (environment bootstrap tofu).
- Build and push the image:
ECR_KEYCLOAK_REPOSITORY_URL=<ecr-repo-url> ./scripts/preview/build-keycloak.sh- Apply environment tofu (
enable_preview_keycloak=true, default) so ECS/ALB/DNS/Postgres are created. - Confirm discovery:
https://auth.<DOMAIN>/realms/sebt/.well-known/openid-configuration
- If this Keycloak instance already had a
sebtrealm beforesebt-preview-deploywas added to the realm JSON, seed the deploy client once (import does not overwrite existing realms). Pick one of:
A. Bootstrap script (preferred when bootstrap admin works):
./scripts/preview/bootstrap-keycloak-deploy-client.shUses the bootstrap admin secret in Secrets Manager (…-keycloak-admin). If admin login returns 401, admin credentials have drifted from the live Keycloak DB — use B or C instead.
B. Manual Admin Console (when bootstrap admin is broken but you can still reach /admin with a working user):
The public ALB returns 403 for /admin* unless your IP is in keycloak_admin_ingress_cidrs or you send the bypass header. For break-glass console access, either temporarily add your VPN/office CIDR via TF_VAR_keycloak_admin_ingress_cidrs and re-apply, or call the Admin API with the bypass header from Secrets Manager (…-keycloak-admin-bypass → headerValue). Prefer SSM exec on the Keycloak task when you only need to inspect logs.
- Open
https://auth.<DOMAIN>/admin(after allowlisting) and sign in. - Realm sebt → Clients → Create client:
- Client ID:
sebt-preview-deploy - Client authentication: On
- Authentication flow: disable Standard flow; enable Service accounts roles only
- Client ID:
- Credentials tab: set the client secret to
sebt-preview-deploy-secret(or your override). - Service accounts roles →
realm-management: assignmanage-clients,view-clients,query-clients. - Confirm:
# should print an access_token JSON field (HTTP 200)
curl -sS -X POST "https://auth.<DOMAIN>/realms/sebt/protocol/openid-connect/token" \
-d grant_type=client_credentials \
-d client_id=sebt-preview-deploy \
-d client_secret=sebt-preview-deploy-secretC. Recreate the Keycloak database (last resort): destroy/recreate the Keycloak RDS (or otherwise empty the DB), rebuild/push the image with the updated realm JSON, and force a new ECS deployment so --import-realm imports a fresh sebt realm including sebt-preview-deploy.
Until one of A–C succeeds, Preview (CO) deploys will fail when registering redirect URIs and login will keep returning Invalid parameter: redirect_uri.
Preview deploy scripts point OIDC at this Keycloak automatically. OTP bypass remains enabled on previews as a fallback.
Keycloak 26 only allows path-trailing wildcards in Valid Redirect URIs (https://host.example/*), not hostname wildcards (https://*.example/*). Because each preview uses a distinct pr-N.<DOMAIN> host, deploy-co.sh registers that host on the shared sebt-portal and sebt-portal-stepup clients via the Keycloak Admin API after Route53 aliases are created (so the preview URL still resolves if Keycloak is temporarily unreachable), and destroy-co.sh removes it. Helpers live in scripts/preview/keycloak.sh. Registration remains required for a successful deploy; without it, OIDC login will fail.
Preview scripts authenticate with the dedicated sebt-preview-deploy client (client_credentials), not the bootstrap admin user. Defaults match the baked realm secret; override with PREVIEW_KEYCLOAK_DEPLOY_CLIENT_ID / PREVIEW_KEYCLOAK_DEPLOY_CLIENT_SECRET, or PREVIEW_KEYCLOAK_DEPLOY_SECRET_ID pointing at Secrets Manager JSON { "clientId", "clientSecret" }. If those overrides are wrong or the live client was never seeded, token errors name the client and point back to the bootstrap / manual steps above.
From docker/keycloak/sebt-realm.preview.json (password password):
| Username | Notes |
|---|---|
verified |
Emits Socure + myColorado-shaped IAL claims (1.5) on login and step-up |
co-loaded |
Co-loaded persona; same IAL 1.5 claims on login and step-up |
ial1-only |
IAL1-only (no verification-level claims) |
Realm clients (also the preview deploy script defaults):
- Login:
sebt-portal/sebt-portal-dev-secret - Step-up:
sebt-portal-stepup/sebt-portal-stepup-dev-secret - Preview Admin API:
sebt-preview-deploy/sebt-preview-deploy-secret(service account withmanage-clients)
Override with PREVIEW_OIDC_* / PREVIEW_KEYCLOAK_DEPLOY_* env vars if you rotate secrets in the realm.
Baked realm redirect URIs cover the long-lived host and localhost only. Ephemeral preview hosts are added at deploy time (see above).
- Do not use this IdP for production.
- Bootstrap admin credentials remain in Secrets Manager (
…-keycloak-admin) for break-glass / one-time bootstrap only. /admin*is blocked at the ALB by default. Preview scripts read…-keycloak-admin-bypass(override withPREVIEW_KEYCLOAK_ADMIN_BYPASS_SECRET_IDorPREVIEW_KEYCLOAK_ADMIN_BYPASS_HEADER). Optional CIDR allowlist:keycloak_admin_ingress_cidrs.- After realm or theme changes: rebuild/push the image, then force a new ECS deployment for the Keycloak service. Note that
--import-realmdoes not overwrite an existing realm in the Postgres database; usebootstrap-keycloak-deploy-client.shor the Admin API when you need live client changes beyond what preview deploy already manages. - When the environment domain changes, update the long-lived entries in
sebt-realm.preview.jsonand rebuild the image.