Skip to content

Quick Start Guide

Get started with the Keycloak Operator in 10 minutes using the 3-helm-chart approach!

Prerequisites

Before you begin, ensure you have:

  • ✅ Kubernetes cluster (v1.26+)
  • kubectl configured to access your cluster
  • Helm 3 installed
  • ✅ Cluster admin permissions (for CRD installation)

The 3-Helm-Chart Approach

This operator uses a modular helm chart structure:

  1. Database (cloudnative-pg/cloudnative-pg) - PostgreSQL cluster
  2. Operator + Keycloak (keycloak-operator/keycloak-operator) - Operator and instance
  3. Application Resources (keycloak-realm, keycloak-client) - Your realms and clients

This separation enables: - ✅ Shared Database - Multiple Keycloak instances can use one PostgreSQL cluster - ✅ GitOps Friendly - Each chart in separate Helm release/ArgoCD application - ✅ Namespace Isolation - Realms and clients in their own namespaces - ✅ Modular Upgrades - Update components independently

Step 1: Install PostgreSQL Database

Install CloudNativePG operator and create a PostgreSQL cluster:

# Install CloudNativePG operator
helm repo add cnpg https://cloudnative-pg.github.io/charts
helm repo update
helm install cnpg cnpg/cloudnative-pg \
  --namespace cnpg-system \
  --create-namespace \
  --wait

The Keycloak operator chart can create the PostgreSQL cluster automatically using keycloak.database.cnpg.enabled=true.

Step 2: Install Operator + Keycloak Instance

Install the operator with Keycloak instance enabled:

# Add the keycloak-operator Helm repository
helm repo add keycloak-operator https://vriesdemichael.github.io/keycloak-operator
helm repo update

# Install operator + Keycloak with CloudNativePG database
helm install keycloak-operator keycloak-operator/keycloak-operator \
  --namespace keycloak-system \
  --create-namespace \
  --set keycloak.enabled=true \
  --set keycloak.database.cnpg.enabled=true \
  --set keycloak.database.cnpg.clusterName=keycloak-postgres \
  --set keycloak.replicas=3 \
  --wait

What this installs: - ✅ Keycloak operator (2 replicas for HA) - ✅ Keycloak instance (3 replicas) - ✅ PostgreSQL cluster (via CloudNativePG) - ✅ Admission webhooks with cert-manager certificates - ✅ Service accounts and RBAC

Verify everything is running:

# Check operator
kubectl get pods -n keycloak-system -l app.kubernetes.io/name=keycloak-operator

# Check Keycloak instance
kubectl get keycloak -n keycloak-system

# Check PostgreSQL cluster
kubectl get cluster -n keycloak-system

# Expected output:
# NAME       PHASE   AGE
# keycloak   Ready   2m

Using External Database:

If you have an existing PostgreSQL database:

helm install keycloak-operator keycloak-operator/keycloak-operator \
  --namespace keycloak-system \
  --create-namespace \
  --set keycloak.enabled=true \
  --set keycloak.database.host=postgresql.database.svc \
  --set keycloak.database.port=5432 \
  --set keycloak.database.database=keycloak \
  --set keycloak.database.username=keycloak \
  --set keycloak.database.passwordSecret.name=db-password \
  --set keycloak.database.passwordSecret.key=password

Step 3: Create Application Realm

Create a realm for your application using the realm Helm chart:

# Create namespace for your app
kubectl create namespace my-app

# Install realm chart
helm install my-app-realm keycloak-operator/keycloak-realm \
  --namespace my-app \
  --set realmName=my-app \
  --set displayName="My Application" \
  --set instanceRef.name=keycloak \
  --set instanceRef.namespace=keycloak-system \
  --set 'clientAuthorizationGrants={my-app,my-app-staging}'

Understanding clientAuthorizationGrants: - Lists which namespaces can create clients in this realm - In this example: my-app and my-app-staging can create clients - This is the authorization model - no tokens required! - Fully declarative and GitOps-friendly

Wait for the realm to become ready:

kubectl wait --for=condition=Ready keycloakrealm/my-app-realm \
  -n my-app \
  --timeout=2m

# Check status
kubectl get keycloakrealm -n my-app
# Expected output:
# NAME            PHASE   AGE
# my-app-realm    Ready   45s

Step 4: Create OAuth2/OIDC Client

Create an OAuth2/OIDC client for your application:

helm install my-app-client keycloak-operator/keycloak-client \
  --namespace my-app \
  --set clientId=my-app \
  --set name="My Application" \
  --set realmRef.name=my-app-realm \
  --set realmRef.namespace=my-app \
  --set publicClient=false \
  --set standardFlowEnabled=true \
  --set directAccessGrantsEnabled=true \
  --set 'redirectUris={https://my-app.example.com/callback,http://localhost:3000/callback}' \
  --set 'webOrigins={https://my-app.example.com,http://localhost:3000}'

Wait for the client to become ready:

kubectl wait --for=condition=Ready keycloakclient/my-app-client \
  -n my-app \
  --timeout=2m

# Check status
kubectl get keycloakclient -n my-app
# Expected output:
# NAME             PHASE   AGE
# my-app-client    Ready   30s

Step 5: Retrieve Client Credentials

The operator automatically creates a Kubernetes secret with OAuth2 credentials:

# View all credentials
kubectl get secret my-app-client-credentials -n my-app -o yaml

# Extract specific values
CLIENT_ID=$(kubectl get secret my-app-client-credentials -n my-app \
  -o jsonpath='{.data.client_id}' | base64 -d)

CLIENT_SECRET=$(kubectl get secret my-app-client-credentials -n my-app \
  -o jsonpath='{.data.client_secret}' | base64 -d)

ISSUER_URL=$(kubectl get secret my-app-client-credentials -n my-app \
  -o jsonpath='{.data.issuer_url}' | base64 -d)

echo "Client ID: $CLIENT_ID"
echo "Client Secret: $CLIENT_SECRET"
echo "Issuer URL: $ISSUER_URL"

Create an environment file for your application:

kubectl get secret my-app-client-credentials -n my-app -o json | \
  jq -r '.data | to_entries[] | "\(.key | ascii_upcase)=\(.value | @base64d)"' > .env

cat .env

Step 6: Integrate with Your Application

Example: Node.js

const { Issuer } = require('openid-client');

const issuer = await Issuer.discover(process.env.ISSUER_URL);
const client = new issuer.Client({
  client_id: process.env.CLIENT_ID,
  client_secret: process.env.CLIENT_SECRET,
  redirect_uris: ['http://localhost:3000/callback'],
  response_types: ['code'],
});

Example: Python

from authlib.integrations.flask_client import OAuth

oauth = OAuth(app)
oauth.register(
    name='keycloak',
    client_id=os.getenv('CLIENT_ID'),
    client_secret=os.getenv('CLIENT_SECRET'),
    server_metadata_url=os.getenv('ISSUER_URL') + '/.well-known/openid-configuration',
    client_kwargs={'scope': 'openid profile email'}
)

Example: Spring Boot

spring:
  security:
    oauth2:
      client:
        registration:
          keycloak:
            client-id: ${CLIENT_ID}
            client-secret: ${CLIENT_SECRET}
            scope: openid,profile,email
        provider:
          keycloak:
            issuer-uri: ${ISSUER_URL}

Verify Installation

Check that all components are healthy:

# Operator
kubectl get pods -n keycloak-system -l app.kubernetes.io/name=keycloak-operator

# Keycloak instance
kubectl get keycloak -n keycloak-system

# PostgreSQL
kubectl get cluster -n keycloak-system

# Realm
kubectl get keycloakrealm -n my-app

# Client
kubectl get keycloakclient -n my-app

All resources should show PHASE=Ready.

Clean Up

# Delete application resources
helm uninstall my-app-client -n my-app
helm uninstall my-app-realm -n my-app
kubectl delete namespace my-app

# Delete operator and Keycloak
helm uninstall keycloak-operator -n keycloak-system

# Delete database (optional - will delete all data!)
helm uninstall cnpg -n cnpg-system

Advanced: Using with ArgoCD

Structure your GitOps repository:

apps/
├── database/
│   └── cloudnative-pg.yaml        # wave: 0
├── keycloak-operator/
│   └── operator-with-instance.yaml # wave: 1
└── my-app/
    ├── realm.yaml                  # wave: 2
    └── client.yaml                 # wave: 3

Example ArgoCD Application:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: keycloak-operator
  annotations:
    argocd.argoproj.io/sync-wave: "1"
spec:
  project: default
  source:
    repoURL: https://vriesdemichael.github.io/keycloak-operator
    chart: keycloak-operator
    targetRevision: 0.2.x
    helm:
      values: |
        keycloak:
          enabled: true
          database:
            cnpg:
              enabled: true
  destination:
    server: https://kubernetes.default.svc
    namespace: keycloak-system
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

Next Steps

Configuration: - SMTP Configuration - Email notifications - Identity Providers - Google, GitHub, Azure AD SSO - High Availability - Production HA setup

Understanding the System: - Architecture - How the operator works - Security Model - Authorization and RBAC - Drift Detection - Orphan detection

CRD References: - KeycloakRealm - All realm options - KeycloakClient - All client options

Troubleshooting

Operator not starting

kubectl logs -n keycloak-system -l app.kubernetes.io/name=keycloak-operator
kubectl describe clusterrolebinding keycloak-operator
kubectl get certificate -n keycloak-system

Keycloak stuck in Pending

kubectl describe keycloak keycloak -n keycloak-system
kubectl get cluster -n keycloak-system  # Check PostgreSQL
kubectl get events -n keycloak-system --sort-by='.lastTimestamp'

Realm creation fails

kubectl describe keycloakrealm my-app-realm -n my-app
kubectl get keycloakrealm my-app-realm -n my-app -o jsonpath='{.status.conditions}' | jq
kubectl logs -n keycloak-system -l app.kubernetes.io/name=keycloak-operator | grep my-app-realm

Client authorization error

Symptom: Client shows "namespace not authorized"

# Check realm's authorization grants
kubectl get keycloakrealm my-app-realm -n my-app \
  -o jsonpath='{.spec.clientAuthorizationGrants}' | jq

# Add your namespace to the grant list
helm upgrade my-app-realm keycloak-operator/keycloak-realm \
  --namespace my-app \
  --reuse-values \
  --set 'clientAuthorizationGrants={my-app,my-new-namespace}'

Support