End-to-End Setup Guide¶
This guide walks you through deploying a production-ready Keycloak setup from scratch, including database configuration, high availability, TLS, and monitoring.
For a simpler quick start, see the Quick Start Guide.
This is the longer infrastructure-heavy path for platform teams setting up shared Keycloak infrastructure and the first tenant onboarding flow. If you plan to manage raw CR manifests directly instead of using the charts, see Helm vs Direct CR Deployments.
Overview¶
This guide covers:
- Infrastructure Setup - Kubernetes cluster, ingress, cert-manager, CloudNativePG
- Operator + Keycloak Installation - Deploy using Helm with database and monitoring
- Multi-Tenant Setup - Platform team configures namespaces and authorization
- Realm Creation - Application teams create and manage realms via Helm
- Client Configuration - OAuth2/OIDC client setup with credential management
- Verification & Testing - End-to-end OAuth2 flow validation
- Production Checklist - Security, monitoring, backup verification
Estimated Time: 30-45 minutes
Prerequisites¶
Required¶
| Component | Version | Purpose | Installation |
|---|---|---|---|
| Kubernetes | 1.26+ | Container orchestration | kubernetes.io |
| kubectl | 1.26+ | Kubernetes CLI | Install Guide |
| Helm | 3.8+ | Package manager (OCI support required) | helm.sh |
Recommended for Production¶
| Component | Purpose | Installation |
|---|---|---|
| CloudNativePG | PostgreSQL operator | CNPG Docs |
| Ingress Controller | External access (nginx, traefik) | Ingress NGINX |
| cert-manager | Automatic TLS certificates | cert-manager Docs |
| Prometheus | Metrics collection | Prometheus Operator |
Cluster Requirements¶
- Nodes: 3+ nodes for high availability
- CPU: 4+ cores per node recommended
- Memory: 8+ GB per node recommended
- Storage: StorageClass available for database persistence
- RBAC: Cluster admin permissions required for installation
Part 1: Infrastructure Setup¶
1.1 Install CloudNativePG Operator¶
helm repo add cnpg https://cloudnative-pg.io/charts
helm repo update
helm install cnpg cnpg/cloudnative-pg \
--namespace cnpg-system \
--create-namespace \
--wait
# Verify installation
kubectl get pods -n cnpg-system
1.2 Install Ingress Controller (nginx)¶
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx \
--create-namespace \
--set controller.metrics.enabled=true \
--wait
# Get external IP (may take a few minutes)
kubectl get svc -n ingress-nginx ingress-nginx-controller -w
1.3 Install cert-manager¶
helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--set crds.enabled=true \
--wait
# Verify installation
kubectl get pods -n cert-manager
1.4 Configure DNS¶
Point your domain to the ingress controller's external IP:
INGRESS_IP=$(kubectl get svc -n ingress-nginx ingress-nginx-controller \
-o jsonpath='{.status.loadBalancer.ingress[0].ip}')
echo "Configure DNS A record:"
echo " keycloak.example.com → $INGRESS_IP"
1.5 Create ClusterIssuer for TLS¶
# Create Let's Encrypt ClusterIssuer
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@example.com # Update this
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- http01:
ingress:
class: nginx
EOF
# Verify issuer is ready
kubectl get clusterissuer letsencrypt-prod
Part 2: Operator + Keycloak Installation¶
2.1 Check Available StorageClasses¶
kubectl get storageclass
# Note your storageClass name for the next step
# Common values: standard, gp2, gp3, premium-rwo
2.2 Install Keycloak Operator with Keycloak Instance¶
Deploy the operator with a production-ready Keycloak instance and CloudNativePG database:
helm install keycloak-operator oci://ghcr.io/vriesdemichael/charts/keycloak-operator \
--namespace keycloak-system \
--set keycloak.managed=true \
--set keycloak.replicas=3 \
--set keycloak.version="26.0.0" \
--set keycloak.database.cnpg.enabled=true \
--set keycloak.database.cnpg.clusterName=keycloak-postgres \
--set keycloak.database.cnpg.instances=3 \
--set keycloak.database.cnpg.storage.size=50Gi \
--set keycloak.database.cnpg.storage.storageClass=standard \
--set keycloak.ingress.enabled=true \
--set keycloak.ingress.className=nginx \
--set keycloak.ingress.host=keycloak.example.com \
--set keycloak.ingress.path=/ \
--set keycloak.ingress.tlsEnabled=true \
--set keycloak.ingress.tlsSecretName=keycloak-tls \
--set keycloak.ingress.annotations."cert-manager\.io/cluster-issuer"=letsencrypt-prod \
--set monitoring.enabled=true \
--set operator.replicaCount=2
Note: Update
keycloak.example.comto your actual domain andstorageClassto match your cluster.
2.3 Verify Installation¶
# Wait for operator pods
kubectl wait --for=condition=ready pod \
-l app.kubernetes.io/name=keycloak-operator \
-n keycloak-system \
--timeout=120s
# Check Keycloak instance status
kubectl get keycloak -n keycloak-system
# Check PostgreSQL cluster
kubectl get cluster -n keycloak-system
# Check all pods
kubectl get pods -n keycloak-system
Expected output: - Operator: 2 pods running - Keycloak: 3 pods running - PostgreSQL: 3 pods (1 primary, 2 replicas)
2.4 Retrieve Admin Credentials¶
# Get admin password
kubectl get secret keycloak-admin-password \
-n keycloak-system \
-o jsonpath='{.data.password}' | base64 -d && echo
Note: Admin access is typically not needed - manage everything through Helm charts and CRDs.
Part 3: Multi-Tenant Setup (Platform Team)¶
3.1 Understanding the Authorization Model¶
The operator uses namespace-based authorization:
- Realm Creation: Controlled by Kubernetes RBAC (who can install the keycloak-realm chart)
- Client Creation: Controlled by realm's
clientAuthorizationGrants(which namespaces can install keycloak-client chart) - No Tokens/Secrets: Authorization is purely declarative
- GitOps-Friendly: All authorization changes via Helm values
3.2 Create Application Team Namespace¶
kubectl create namespace team-alpha
kubectl label namespace team-alpha team=alpha environment=production
3.3 Create Realm for Application Team¶
Use the keycloak-realm Helm chart to create a realm:
helm install team-alpha-realm oci://ghcr.io/vriesdemichael/charts/keycloak-realm \
--namespace team-alpha \
--set realmName=team-alpha \
--set displayName="Team Alpha Identity" \
--set operatorRef.namespace=keycloak-system \
--set clientAuthorizationGrants[0]=team-alpha \
--set security.registrationAllowed=false \
--set security.resetPasswordAllowed=true \
--set security.rememberMe=true \
--set security.verifyEmail=true
3.4 Verify Realm Creation¶
# Wait for realm to be ready
kubectl wait --for=condition=Ready keycloakrealm/team-alpha-realm \
-n team-alpha \
--timeout=120s
# Check realm status
kubectl get keycloakrealm -n team-alpha
# View OIDC endpoints
kubectl get keycloakrealm team-alpha-realm -n team-alpha \
-o jsonpath='{.status.endpoints}' | jq .
3.5 Grant Additional Namespaces (Optional)¶
To allow another namespace to create clients in this realm:
helm upgrade team-alpha-realm oci://ghcr.io/vriesdemichael/charts/keycloak-realm \
--namespace team-alpha \
--reuse-values \
--set clientAuthorizationGrants[0]=team-alpha \
--set clientAuthorizationGrants[1]=team-alpha-staging
Part 4: Realm Creation (Application Team)¶
Application teams create their own realms using the Helm chart.
4.1 Create Production Realm¶
helm install my-app-realm oci://ghcr.io/vriesdemichael/charts/keycloak-realm \
--namespace team-alpha \
--set realmName=my-app-prod \
--set displayName="My Application (Production)" \
--set operatorRef.namespace=keycloak-system \
--set clientAuthorizationGrants[0]=team-alpha \
--set security.registrationAllowed=false \
--set security.resetPasswordAllowed=true \
--set security.verifyEmail=true \
--set tokenSettings.accessTokenLifespan=300 \
--set tokenSettings.ssoSessionIdleTimeout=1800 \
--set tokenSettings.ssoSessionMaxLifespan=36000
4.2 Create Staging Realm¶
helm install my-app-staging-realm oci://ghcr.io/vriesdemichael/charts/keycloak-realm \
--namespace team-alpha \
--set realmName=my-app-staging \
--set displayName="My Application (Staging)" \
--set operatorRef.namespace=keycloak-system \
--set clientAuthorizationGrants[0]=team-alpha \
--set security.registrationAllowed=true
4.3 Verify Realms¶
Part 5: Client Configuration¶
5.1 Create Web Application Client¶
helm install my-webapp-client oci://ghcr.io/vriesdemichael/charts/keycloak-client \
--namespace team-alpha \
--set clientId=my-webapp \
--set description="My Web Application" \
--set realmRef.name=my-app-realm \
--set realmRef.namespace=team-alpha \
--set publicClient=false \
--set standardFlowEnabled=true \
--set directAccessGrantsEnabled=false \
--set redirectUris[0]="https://myapp.example.com/callback" \
--set redirectUris[1]="https://myapp.example.com/silent-refresh" \
--set webOrigins[0]="https://myapp.example.com"
5.2 Create API Client (Service Account)¶
helm install my-api-client oci://ghcr.io/vriesdemichael/charts/keycloak-client \
--namespace team-alpha \
--set clientId=my-api \
--set description="My API Service" \
--set realmRef.name=my-app-realm \
--set realmRef.namespace=team-alpha \
--set publicClient=false \
--set standardFlowEnabled=false \
--set serviceAccountsEnabled=true
5.3 Verify Client Creation¶
# Wait for clients to be ready
kubectl wait --for=condition=Ready keycloakclient/my-webapp-client \
-n team-alpha \
--timeout=120s
# List all clients
kubectl get keycloakclient -n team-alpha
5.4 Managed Client Credentials Secret¶
The operator automatically creates a same-namespace Secret for confidential clients. If you do not set secretName, the default secret name is <release-fullname>-credentials, so the my-webapp-client release above produces my-webapp-client-keycloak-client-credentials.
That Secret contains the fields most applications need directly:
client-idclient-secretissuerkeycloak-urlrealmtoken-endpointuserinfo-endpointjwks-endpoint
5.5 Wire the Secret Into an Application Deployment¶
Keep the application in the same namespace and read the generated Secret through secretKeyRef or envFrom.
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-webapp
namespace: team-alpha
spec:
replicas: 2
selector:
matchLabels:
app: my-webapp
template:
metadata:
labels:
app: my-webapp
spec:
containers:
- name: webapp
image: ghcr.io/example/my-webapp:latest
env:
- name: OIDC_CLIENT_ID
valueFrom:
secretKeyRef:
name: my-webapp-client-keycloak-client-credentials
key: client-id
- name: OIDC_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: my-webapp-client-keycloak-client-credentials
key: client-secret
- name: OIDC_ISSUER
valueFrom:
secretKeyRef:
name: my-webapp-client-keycloak-client-credentials
key: issuer
- name: OIDC_JWKS_ENDPOINT
valueFrom:
secretKeyRef:
name: my-webapp-client-keycloak-client-credentials
key: jwks-endpoint
If your application can consume all keys as environment variables, envFrom also works:
Part 6: Verification & Testing¶
6.1 Verify All Resources¶
# Operator
kubectl get pods -n keycloak-system -l app.kubernetes.io/name=keycloak-operator
# Keycloak instance
kubectl get keycloak -n keycloak-system
# Database
kubectl get cluster -n keycloak-system
# Realms
kubectl get keycloakrealm -A
# Clients
kubectl get keycloakclient -A
All resources should show PHASE=Ready.
6.2 Test OIDC Discovery¶
ISSUER_URL=$(kubectl get secret my-webapp-client-keycloak-client-credentials -n team-alpha \
-o jsonpath='{.data.issuer}' | base64 -d)
curl -s "$ISSUER_URL/.well-known/openid-configuration" | jq .
6.3 Test Client Credentials Flow¶
CLIENT_ID=$(kubectl get secret my-api-client-keycloak-client-credentials -n team-alpha \
-o jsonpath='{.data.client-id}' | base64 -d)
CLIENT_SECRET=$(kubectl get secret my-api-client-keycloak-client-credentials -n team-alpha \
-o jsonpath='{.data.client-secret}' | base64 -d)
TOKEN_URL=$(kubectl get secret my-api-client-keycloak-client-credentials -n team-alpha \
-o jsonpath='{.data.token-endpoint}' | base64 -d)
curl -s -X POST "$TOKEN_URL" \
-d "grant_type=client_credentials" \
-d "client_id=$CLIENT_ID" \
-d "client_secret=$CLIENT_SECRET" | jq .
6.4 Test Authorization Code Flow¶
CLIENT_ID=$(kubectl get secret my-webapp-client-keycloak-client-credentials -n team-alpha \
-o jsonpath='{.data.client-id}' | base64 -d)
ISSUER_URL=$(kubectl get secret my-webapp-client-keycloak-client-credentials -n team-alpha \
-o jsonpath='{.data.issuer}' | base64 -d)
AUTH_URL="${ISSUER_URL}/protocol/openid-connect/auth"
echo "Open in browser:"
echo "${AUTH_URL}?client_id=${CLIENT_ID}&redirect_uri=https://myapp.example.com/callback&response_type=code&scope=openid%20profile%20email"
Part 7: Production Checklist¶
Security¶
- TLS enabled on ingress
- cert-manager issuing valid certificates
- Network policies configured (optional)
- RBAC configured for application teams
- Secrets stored securely (consider External Secrets Operator)
High Availability¶
- Operator: 2+ replicas
- Keycloak: 3+ replicas
- PostgreSQL: 3+ instances (CloudNativePG)
- Pod anti-affinity configured
- PodDisruptionBudgets configured
Backup & Recovery¶
- CloudNativePG backups configured (S3/GCS)
- Backup retention policy set
- Restore procedure tested
- Helm values stored in Git
Monitoring¶
- ServiceMonitor created for Prometheus
- Grafana dashboards imported
- Alerts configured for critical issues
- Log aggregation configured
GitOps¶
- All Helm values stored in Git
- ArgoCD/Flux applications configured
- PR workflow for changes
- Drift detection enabled
GitOps with ArgoCD¶
Repository Structure¶
gitops-repo/
├── infrastructure/
│ ├── cnpg/
│ │ └── application.yaml # wave: 0
│ ├── cert-manager/
│ │ └── application.yaml # wave: 0
│ └── ingress-nginx/
│ └── application.yaml # wave: 0
├── keycloak/
│ ├── operator/
│ │ └── application.yaml # wave: 1
│ └── realms/
│ ├── team-alpha/
│ │ ├── realm.yaml # wave: 2
│ │ └── clients.yaml # wave: 3
│ └── team-beta/
│ ├── realm.yaml # wave: 2
│ └── clients.yaml # wave: 3
ArgoCD Application for Operator¶
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: keycloak-operator
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "1"
spec:
project: default
source:
repoURL: ghcr.io/vriesdemichael/charts
chart: keycloak-operator
targetRevision: 0.3.x
helm:
valuesObject:
keycloak:
enabled: true
replicas: 3
database:
cnpg:
enabled: true
instances: 3
monitoring:
enabled: true
operator:
replicaCount: 2
destination:
server: https://kubernetes.default.svc
namespace: keycloak-system
syncPolicy:
automated:
prune: true
selfHeal: true
ArgoCD Application for Realm¶
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: team-alpha-realm
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "2"
spec:
project: default
source:
repoURL: ghcr.io/vriesdemichael/charts
chart: keycloak-realm
targetRevision: 0.3.x
helm:
valuesObject:
realmName: team-alpha
displayName: "Team Alpha Identity"
operatorRef:
namespace: keycloak-system
clientAuthorizationGrants:
- team-alpha
security:
resetPasswordAllowed: true
verifyEmail: true
destination:
server: https://kubernetes.default.svc
namespace: team-alpha
syncPolicy:
automated:
prune: true
selfHeal: true
Troubleshooting¶
Operator Not Starting¶
kubectl logs -n keycloak-system -l app.kubernetes.io/name=keycloak-operator
kubectl describe pod -n keycloak-system -l app.kubernetes.io/name=keycloak-operator
Keycloak Stuck in Pending¶
kubectl describe keycloak keycloak -n keycloak-system
kubectl get events -n keycloak-system --sort-by='.lastTimestamp'
kubectl get cluster -n keycloak-system # Check database
Realm Creation Fails¶
kubectl describe keycloakrealm <realm-name> -n <namespace>
kubectl logs -n keycloak-system -l app.kubernetes.io/name=keycloak-operator | grep <realm-name>
Client Authorization Error¶
# Check realm's authorization grants
kubectl get keycloakrealm <realm-name> -n <namespace> \
-o jsonpath='{.spec.clientAuthorizationGrants}'
# Ensure client namespace is in the list
helm upgrade <realm-release> oci://ghcr.io/vriesdemichael/charts/keycloak-realm \
--namespace <namespace> \
--reuse-values \
--set clientAuthorizationGrants[0]=existing-ns \
--set clientAuthorizationGrants[1]=new-ns
Database Connection Issues¶
kubectl get cluster -n keycloak-system
kubectl logs -n keycloak-system -l cnpg.io/cluster=keycloak-postgres
Next Steps¶
After completing this guide:
- Configure Identity Providers - Add Google, GitHub, Azure AD SSO (Guide)
- Set Up Monitoring - Import Grafana dashboards (Observability)
- Configure Backups - Set up CloudNativePG backups to S3 (Backup Guide)
- Add More Teams - Repeat Part 3-5 for additional teams
- Review Security - Implement network policies, audit logging
Further Reading: