Overview
Goal: Employees sign in with Keycloak (your IdP). Your product still issues Cognito JWTs so API Gateway, ALB, and pods validate one issuer.
Browser / SPA Amazon Cognito Keycloak on EKS RDS
│ │ │ │
│ /oauth2/authorize │ │ │
├───────────────────────►│ │ │
│ │ redirect (OIDC) │ │
│ ├────────────────────────►│ ALB → pods │
│ │ ├─────────────────────►│ PostgreSQL
│ │ auth code back │ realms, users, │ (private)
│ │◄────────────────────────┤ sessions │
│ Cognito JWT │ │ │
│◄───────────────────────┤ │ │
▼ │ │ │
App pods (same/other EKS) ◄── Bearer Cognito access token (not Keycloak token)
| Piece | Role |
|---|---|
| Keycloak (EKS) | Corporate IdP UI + OIDC endpoints behind ALB + HTTPS |
| Amazon RDS (PostgreSQL) | Durable store for Keycloak data (users, realms, clients) - only Keycloak talks to RDS |
| Cognito User Pool | OAuth/OIDC front door for your app; federates to Keycloak |
| App client | OAuth client registered in Cognito (callback URLs, secrets) |
| Hosted UI | Cognito login page with “Sign in with Keycloak” button |
| EKS app workload | Validates Cognito JWT (JWKS). Does not query RDS for login |
Pods: Human login goes Browser → Cognito → Keycloak. Pods trust Cognito JWKS only. For machine identity (no human), use a separate Cognito app client (client credentials) or IRSA for AWS APIs - Keycloak federation is for people, not kube ServiceAccounts.
Keycloak words (30-second map)
Think of Keycloak like a mini “company login server.”
| Term | Plain meaning | Tiny example |
|---|---|---|
| Realm | Isolated tenant (users, clients, themes). | corp = one company; demo = lab sandbox |
| Client | An application allowed to use OAuth/OIDC. | cognito-federation = Cognito is the OAuth client |
| Client authentication | Client has a secret (confidential client). | Cognito needs client_secret at token endpoint |
| Redirect URI | Where Keycloak sends the browser after login. | https://YOUR_DOMAIN.auth.REGION.amazoncognito.com/oauth2/idpresponse |
| Standard flow | Authorization code flow (browser login). | Always on for Cognito federation |
| Scopes | What user info Keycloak may release. | openid email profile |
| User | Account that can sign in. | alice / password in dev only |
| Issuer | Base URL of the realm for OIDC discovery. | https://keycloak.example.com/realms/corp |
| OpenID Endpoint Configuration | .well-known/openid-configuration JSON. | Cognito reads this when you set Issuer URL |
Cognito words that pair with the above:
| Cognito term | Pairs with Keycloak |
|---|---|
| User pool | Your app’s user directory + token issuer |
| Identity provider (OIDC) | Registers Keycloak issuer + client id/secret |
| Attribute mapping | Copy email, sub, etc. from Keycloak token into Cognito profile |
| Hosted UI domain | https://<prefix>.auth.<region>.amazoncognito.com |
| Federated username | Often <ProviderName>_<sub> (e.g. Keycloak_a1b2c3…) |
Part 1 - Keycloak on EKS + Amazon RDS
Order: RDS PostgreSQL (private) → store DB password in Secrets Manager → deploy Keycloak on same VPC as EKS → ALB Ingress + ACM → public hostname → Cognito OIDC issuer URL.
Cognito must reach Keycloak over HTTPS on the internet (or at least a URL AWS can call). App pods in EKS never connect to RDS for auth; only Keycloak pods use JDBC to RDS.
Internet ──► ALB (ACM cert) ──► Keycloak Deployment (namespace keycloak)
│
└── JDBC :5432 ──► RDS PostgreSQL (private subnets)
| RDS choice | Notes |
|---|---|
| Engine | PostgreSQL 15+ (Keycloak supports PostgreSQL, MariaDB, MySQL) |
| Access | Not publicly accessible; same VPC as EKS worker nodes |
| HA | Multi-AZ for production; single-AZ OK for lab |
| Name / user | DB keycloak, user keycloak (match Helm / KC_DB_* below) |
Step 1 - Create RDS PostgreSQL (console)
- RDS → Create database → Standard create → PostgreSQL.
- Templates: Dev/Test for lab, Production for real use.
- Settings: DB identifier
keycloak-db, master usernamekeycloakadmin(or use defaultpostgresand create app user later). - Credentials: Secrets Manager or self-managed strong password.
- Instance:
db.t4g.smallor larger if many users. - Storage: gp3, enable storage autoscaling if you expect growth.
- Connectivity
- VPC = EKS cluster VPC
- No public access
- DB subnet group = private subnets
- Create or pick security group
rds-keycloak-sg
- Additional configuration
- Initial database name:
keycloak
- Initial database name:
- Create database. Note Endpoint hostname.
Security group rules
| Direction | Rule |
|---|---|
RDS rds-keycloak-sg inbound | TCP 5432 from EKS node security group (or cluster security group / pod ENI SG if you use custom networking) |
| RDS outbound | Default (or restrict to Keycloak SG only) |
AWS CLI - RDS PostgreSQL instance (minimal lab)
export AWS_REGION=us-east-1
export VPC_ID=vpc-xxxxxxxx
export EKS_NODE_SG=sg-xxxxxxxx # SG attached to worker nodes
RDS_SG=$(aws ec2 create-security-group \
--group-name rds-keycloak-sg \
--description "Keycloak RDS" \
--vpc-id "$VPC_ID" \
--query GroupId --output text)
aws ec2 authorize-security-group-ingress \
--group-id "$RDS_SG" \
--protocol tcp --port 5432 --source-group "$EKS_NODE_SG"
aws rds create-db-instance \
--db-instance-identifier keycloak-db \
--engine postgres \
--engine-version 16.4 \
--db-instance-class db.t4g.small \
--allocated-storage 20 \
--master-username keycloak \
--master-user-password 'Generate-Strong-Password-Here' \
--db-name keycloak \
--vpc-security-group-ids "$RDS_SG" \
--no-publicly-accessible \
--backup-retention-period 7
aws rds describe-db-instances \
--db-instance-identifier keycloak-db \
--query 'DBInstances[0].Endpoint.Address' --output text
SQL - optional separate app user (if master user is postgres)
Connect from a bastion or kubectl run psql pod in the VPC:
CREATE USER keycloak WITH PASSWORD 'app-user-password';
CREATE DATABASE keycloak OWNER keycloak;
GRANT ALL PRIVILEGES ON DATABASE keycloak TO keycloak;
Keycloak creates its own tables on first start (start / start-optimized).
Step 2 - Store DB password for Kubernetes
Console: Secrets Manager → Store a new secret → Credentials for Amazon RDS database → select keycloak-db → secret name keycloak/rds.
In cluster: sync with External Secrets Operator or create a Secret manually before Helm install.
kubectl - DB secret (manual)
kubectl create namespace keycloak
kubectl create secret generic keycloak-db \
--namespace keycloak \
--from-literal=password='Generate-Strong-Password-Here'
Step 3 - Deploy Keycloak on EKS (Helm + ALB)
Prerequisites: AWS Load Balancer Controller on the cluster, public subnets tagged for ALB, ACM certificate for auth.example.com.
kubectl create namespace keycloak(if not done).- Install chart with embedded PostgreSQL disabled and externalDatabase pointing at RDS.
- Set public hostname so OIDC issuer is stable (Cognito caches discovery from this URL).
- Wait for Ingress → ALB → target healthy →
https://auth.example.comloads admin console.
Helm - Bitnami Keycloak + external RDS + ALB Ingress
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
export RDS_HOST=keycloak-db.xxxxxxxxxxxx.us-east-1.rds.amazonaws.com
export ACM_CERT_ARN=arn:aws:acm:us-east-1:ACCOUNT:certificate/xxxxxxxx
helm upgrade --install keycloak bitnami/keycloak \
--namespace keycloak \
--set auth.adminUser=admin \
--set auth.adminPassword='ChangeMe-Admin-123' \
--set production=true \
--set proxy=edge \
--set postgresql.enabled=false \
--set externalDatabase.host="$RDS_HOST" \
--set externalDatabase.port=5432 \
--set externalDatabase.user=keycloak \
--set externalDatabase.database=keycloak \
--set externalDatabase.existingSecret=keycloak-db \
--set externalDatabase.existingSecretPasswordKey=password \
--set ingress.enabled=true \
--set ingress.ingressClassName=alb \
--set ingress.hostname=auth.example.com \
--set ingress.annotations."alb\.ingress\.kubernetes\.io/scheme"=internet-facing \
--set ingress.annotations."alb\.ingress\.kubernetes\.io/target-type"=ip \
--set ingress.annotations."alb\.ingress\.kubernetes\.io/certificate-arn"="$ACM_CERT_ARN" \
--set ingress.annotations."alb\.ingress\.kubernetes\.io/listen-ports"='[{"HTTPS":443}]'
Route 53 (or your DNS): CNAME auth.example.com → ALB DNS name from kubectl get ingress -n keycloak.
Manifest - official Keycloak image + env (RDS + hostname)
apiVersion: apps/v1
kind: Deployment
metadata:
name: keycloak
namespace: keycloak
spec:
replicas: 2
selector:
matchLabels:
app: keycloak
template:
metadata:
labels:
app: keycloak
spec:
containers:
- name: keycloak
image: quay.io/keycloak/keycloak:26.0
args: ["start"]
env:
- name: KC_DB
value: postgres
- name: KC_DB_URL_HOST
value: keycloak-db.xxxxx.us-east-1.rds.amazonaws.com
- name: KC_DB_URL_DATABASE
value: keycloak
- name: KC_DB_USERNAME
value: keycloak
- name: KC_DB_PASSWORD
valueFrom:
secretKeyRef:
name: keycloak-db
key: password
- name: KC_HOSTNAME
value: https://auth.example.com
- name: KC_PROXY_HEADERS
value: xforwarded
- name: KC_HTTP_ENABLED
value: "true"
- name: KC_BOOTSTRAP_ADMIN_USERNAME
value: admin
- name: KC_BOOTSTRAP_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: keycloak-admin
key: password
ports:
- containerPort: 8080
Pair with Service + ALB Ingress (same annotations as Helm example). Run at least two replicas in production; sessions and realm data live in RDS so pods are disposable.
Step 4 - Keycloak admin UI (realm bootstrap)
- Open
https://auth.example.com/admin. - Login with bootstrap admin user.
- Realm dropdown → Create realm → name
corp→ Create (stay out ofmasterfor app config). - Realm settings → OpenID Endpoint Configuration → confirm issuer:
https://auth.example.com/realms/corp
Use that exact value later in Cognito Issuer URL (Part 4).
Local Docker only - UI practice without EKS (not for Cognito federation)
docker run -d --name keycloak -p 8080:8080 \
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
-e KC_BOOTSTRAP_ADMIN_PASSWORD='ChangeMe-Admin-123' \
quay.io/keycloak/keycloak:26.0 start-dev
start-dev = in-memory DB, HTTP. Use only to learn Keycloak screens. Cognito integration needs EKS (or EC2) + HTTPS issuer, not laptop localhost.
Part 2 - Keycloak setup for Cognito
Do these inside realm corp, before touching AWS.
Step A - Create OIDC client for Cognito
- Clients → Create client.
- General settings
- Client type: OpenID Connect
- Client ID:
cognito-federation
- Capability config
- Client authentication: On (confidential client)
- Authorization: Off (unless you use Keycloak authorization services)
- Authentication flow: enable Standard flow only
- Login settings (you will fill redirect after Cognito domain exists)
- Valid redirect URIs: placeholder
https://example.com/oauth2/idpresponsefor now - Web origins:
+(allow redirect origin) or your Cognito domain origin
- Valid redirect URIs: placeholder
- Save → open client → Credentials tab → copy Client secret (needed in Cognito).
Step B - Test user
- Users → Create new user
- Username:
alice - Email:
alice@example.com(optional but helps mapping) - Email verified: On
- Username:
- Credentials tab → Set password → temporary Off → password e.g.
Alice-Dev-123.
Step C - Issuer URL (write this down)
Realm settings → OpenID Endpoint Configuration → note issuer (no trailing slash):
https://auth.example.com/realms/corp
Must match what browsers and Cognito fetch from /.well-known/openid-configuration on your ALB hostname (Part 1).
Keycloak Admin CLI (kcadm) - create client + user
# Inside Keycloak container or with kcadm on PATH
/opt/keycloak/bin/kcadm.sh config credentials \
--server https://auth.example.com \
--realm master \
--user admin \
--password 'ChangeMe-Admin-123'
/opt/keycloak/bin/kcadm.sh create realms -s realm=corp -s enabled=true
/opt/keycloak/bin/kcadm.sh create clients -r corp \
-s clientId=cognito-federation \
-s protocol=openid-connect \
-s publicClient=false \
-s standardFlowEnabled=true \
-s 'redirectUris=["https://YOUR_PREFIX.auth.us-east-1.amazoncognito.com/oauth2/idpresponse"]' \
-s 'webOrigins=["+"]'
# Client secret (replace INTERNAL_ID from clients list)
/opt/keycloak/bin/kcadm.sh get clients -r corp -q clientId=cognito-federation --fields id,secret
Part 3 - Cognito User Pool + Hosted UI
Region example: us-east-1. Replace ACCOUNT, REGION, and names.
Step 1 - User pool
- Amazon Cognito → User pools → Create user pool.
- Sign-in experience
- Cognito user pool sign-in: Email or Username (pick one; federation still works).
- Federated sign-in: leave defaults.
- Security - MFA off for lab.
- Sign-up - disable self-registration if this pool is employees-only.
- Required attributes - enable email if you map email from Keycloak.
- Name pool e.g.
corp-federated→ create.
Step 2 - Domain (Hosted UI)
- Pool → App integration → Domain.
- Cognito domain → prefix e.g.
corp-kc-demo(must be unique). - Note full domain:
https://corp-kc-demo.auth.us-east-1.amazoncognito.com
Step 3 - App client (your SPA or test app)
- App integration → App clients → Create app client.
- Name:
web-spa. - Confidential client if you have a backend that holds a secret; Public client for pure SPA + PKCE.
- Allowed callback URLs:
https://jwt.io(quick test) orhttps://app.example.com/callback. - Allowed sign-out URLs: same origin as app.
- OAuth 2.0 grant types: Authorization code grant.
- OpenID Connect scopes:
openid,email,profile(and custom resource scopes later if needed). - Save Client ID (and secret if confidential).
Step 4 - Hosted UI identity provider list
- App integration → App client → Edit hosted UI.
- Identity providers: enable Cognito user pool (optional) and your Keycloak provider (next section).
- OAuth 2.0 grant types and scopes aligned with step 3.
AWS CLI - user pool, domain, app client
export AWS_REGION=us-east-1
POOL_ID=$(aws cognito-idp create-user-pool \
--pool-name corp-federated \
--policies 'PasswordPolicy={MinimumLength=8,RequireUppercase=false,RequireLowercase=false,RequireNumbers=false,RequireSymbols=false}' \
--auto-verified-attributes email \
--query 'UserPool.Id' --output text)
aws cognito-idp create-user-pool-domain \
--domain corp-kc-demo-$(aws sts get-caller-identity --query Account --output text | tail -c 7) \
--user-pool-id "$POOL_ID"
CLIENT_ID=$(aws cognito-idp create-user-pool-client \
--user-pool-id "$POOL_ID" \
--client-name web-spa \
--generate-secret \
--allowed-o-auth-flows code \
--allowed-o-auth-scopes openid email profile \
--allowed-o-auth-flows-user-pool-client \
--callback-urls 'https://jwt.io' \
--logout-urls 'https://jwt.io' \
--supported-identity-providers COGNITO \
--query 'UserPoolClient.ClientId' --output text)
echo "POOL_ID=$POOL_ID CLIENT_ID=$CLIENT_ID"
Part 4 - Wire Keycloak as Cognito OIDC IdP
Console
- User pool → Sign-in experience → Federated identity provider sign-in → Add identity provider → OpenID Connect (OIDC).
- Provider name:
Keycloak(appears on Hosted UI; used asidentity_provider=Keycloak). - Client ID:
cognito-federation(from Keycloak). - Client secret: from Keycloak client Credentials.
- Authorized scopes:
openid email profile. - Issuer URL:
https://auth.example.com/realms/corp(Keycloak on EKS, Part 1).- Cognito fetches
/.well-known/openid-configurationfrom this issuer.
- Cognito fetches
- Attributes request method:
GET(Keycloak default). If token/userinfo fails, tryPOST. - Attribute mapping (minimum):
| User pool attribute | OpenID Connect attribute |
|---|---|
| username | sub |
| name | name |
- Save.
Fix Keycloak redirect URI (critical)
Back in Keycloak client Valid redirect URIs, set exactly:
https://<your-cognito-domain-prefix>.auth.<region>.amazoncognito.com/oauth2/idpresponse
Example:
https://corp-kc-demo.auth.us-east-1.amazoncognito.com/oauth2/idpresponse
No trailing slash. Multiple pools → one URI per pool domain.
AWS CLI - create OIDC IdP + enable on app client
export POOL_ID=us-east-1_XXXXXXXXX
export CLIENT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxx
export KC_ISSUER='https://keycloak.example.com/realms/corp'
export KC_CLIENT_ID=cognito-federation
export KC_CLIENT_SECRET='paste-from-keycloak'
aws cognito-idp create-identity-provider \
--user-pool-id "$POOL_ID" \
--provider-name Keycloak \
--provider-type OIDC \
--provider-details \
client_id="$KC_CLIENT_ID",client_secret="$KC_CLIENT_SECRET",oidc_issuer="$KC_ISSUER",authorize_scopes="openid email profile",attributes_request_method=GET \
--attribute-mapping email=email,username=sub,name=name
aws cognito-idp update-user-pool-client \
--user-pool-id "$POOL_ID" \
--client-id "$CLIENT_ID" \
--supported-identity-providers COGNITO Keycloak \
--allowed-o-auth-flows code \
--allowed-o-auth-scopes openid email profile \
--allowed-o-auth-flows-user-pool-client \
--callback-urls 'https://jwt.io' \
--logout-urls 'https://jwt.io'
Cognito requirement: External OIDC IdPs must support client_secret_post at the token endpoint. Keycloak confidential clients do. See Cognito OIDC IdP.
Part 5 - End-to-end test (browser)
Hosted UI URL template
https://<domain>.auth.<region>.amazoncognito.com/oauth2/authorize
?client_id=<CLIENT_ID>
&response_type=code
&scope=openid+email+profile
&redirect_uri=<URL_ENCODED_CALLBACK>
&identity_provider=Keycloak
identity_provider=Keycloakskips Cognito local password and jumps to Keycloak.- Omit it to show Cognito + federated buttons.
What should happen
- Browser opens Cognito Hosted UI → Keycloak (or direct redirect).
- Keycloak login →
alice/ password. - Browser lands on
redirect_uriwith?code=.... - App exchanges code at Cognito token endpoint (server-side or PKCE from SPA).
- Response includes ID token, access token, refresh token issued by Cognito (
iss= Cognito pool).
First login side effect
Cognito creates a shadow user in the pool (federated profile). Later tokens use Cognito signing keys even though password lives in Keycloak.
curl - token exchange (confidential client + secret)
export DOMAIN='corp-kc-demo.auth.us-east-1.amazoncognito.com'
export CLIENT_ID='...'
export CLIENT_SECRET='...'
export REDIRECT_URI='https://jwt.io'
export AUTH_CODE='paste-code-from-redirect'
curl -sS -X POST "https://${DOMAIN}/oauth2/token" \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d "grant_type=authorization_code" \
-d "client_id=${CLIENT_ID}" \
-d "client_secret=${CLIENT_SECRET}" \
-d "code=${AUTH_CODE}" \
-d "redirect_uri=${REDIRECT_URI}" | jq .
Part 6 - How EKS pods use this
Keycloak usually runs in namespace keycloak on the same cluster as your apps, but a different Deployment. App pods talk to Cognito JWKS; only Keycloak pods open JDBC to RDS.
Separate three cases. Mixing them causes “why is my pod calling Keycloak?” confusion.
Case A - User-facing API in a pod (most common)
User browser ──► Cognito (+ Keycloak login once) ──► access_token (Cognito)
│
└──► Ingress/ALB ──► Pod validates JWT with Cognito JWKS
| Do | Don’t |
|---|---|
Validate Authorization: Bearer <access_token> | Send Keycloak token to the pod |
Check iss, exp, aud/client_id, scopes | Mount Keycloak admin password in the pod |
Cache JWKS from https://cognito-idp.<region>.amazonaws.com/<poolId>/.well-known/jwks.json | Assume sub format matches Keycloak’s raw sub without checking Cognito mapping |
Username in Cognito is often Keycloak_<keycloak-sub>. Authorize on cognito:groups, custom claims, or app roles - not on Keycloak group names unless you map them (advanced: SAML or custom Lambda triggers).
Pod Deployment - pass pool metadata as env (app validates JWT)
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
template:
spec:
containers:
- name: api
image: your-api:latest
env:
- name: COGNITO_REGION
value: us-east-1
- name: COGNITO_USER_POOL_ID
value: us-east-1_XXXXXXXXX
- name: COGNITO_CLIENT_ID
value: xxxxxxxxxxxxxxxxxxxxxxxxxx
ports:
- containerPort: 8080
Your framework middleware downloads JWKS once, validates every request. Reference pattern: EKS microservice access tokens (Cognito).
Case B - Pod calls AWS APIs (S3, DynamoDB, etc.)
Use IRSA (IAM Roles for Service Accounts). Identity chain is Kubernetes ServiceAccount → STS → IAM, not Cognito and not Keycloak.
Pod ── projected SA token (aud=sts.amazonaws.com) ──► AssumeRoleWithWebIdentity ──► AWS API
Humans may still use Cognito+Keycloak for the web app; batch workers use IRSA. No federation change required.
Case C - Machine-to-machine HTTP (no human in browser)
Keycloak federation does not help nightly jobs. Options:
| Pattern | When |
|---|---|
| Cognito app client + client credentials (if enabled for your pool/client) | Service calls your API with Cognito-issued M2M token |
| Keycloak service account (client credentials to Keycloak) | Only if the resource owner is Keycloak, not Cognito |
| IRSA | AWS resource access from the cluster |
Do not embed alice password in a pod to “simulate” federation.
Optional - ALB authenticate with Cognito (ingress annotation idea)
ALB can authenticate users against Cognito before traffic hits pods. Users still federate to Keycloak at Cognito Hosted UI; pods receive requests only after ALB session is established (pattern varies by controller version). Prefer in-app JWT validation when you need fine-grained scopes inside the service.
Troubleshooting (fast)
| Symptom | Likely fix |
|---|---|
redirect_uri_mismatch at Keycloak | Cognito /oauth2/idpresponse URI exact match in Keycloak client |
| Cognito “Invalid issuer” | Issuer URL must match discovery document; HTTPS in prod; no trailing / |
| Login works, email empty | Keycloak user has email; scope includes email; attribute mapping email=email |
| Token rejected in pod | Validate Cognito JWT, not Keycloak; check iss and token_use (access vs id) |
| Cognito cannot reach localhost Keycloak | Keycloak on EKS + public ALB + ACM; issuer = https://auth.example.com/realms/corp |
Keycloak pod Connection refused to RDS | RDS in same VPC; SG allows 5432 from node/pod SG; correct endpoint hostname |
| Keycloak starts but empty / migration errors | DB name keycloak, user has privileges; password matches Secret; use start not start-dev with RDS |
| Wrong issuer in tokens | KC_HOSTNAME / Ingress hostname must match DNS; check OpenID Endpoint Configuration in realm |
Production checklist (short)
- RDS: Multi-AZ, automated backups, encryption at rest, minor version auto-upgrade, enough storage for session/realm growth.
- EKS: Keycloak ≥ 2 replicas, PDB, HPA optional; ALB TLS 1.2+; restrict admin console (IP allow list or VPN-only internal ALB + separate public hostname for
/realms/*only if you split paths). - Network: RDS never public; rotate DB password via Secrets Manager + rolling restart.
- Rotate Keycloak Cognito federation client secret; update Cognito IdP.
- Restrict callback/logout URLs to real app origins (not
jwt.io). - MFA in Keycloak (or corporate IdP upstream).
- App pods: Cognito JWT validation only; IRSA for AWS APIs; no RDS credentials in application Deployments.