About
The Keycloak Operator is a Kubernetes operator developed by Hostzero that manages Keycloak instances through the Keycloak Admin API. The overall goal is to provide a cloud-native management interface for Keycloak instances.
Features
- Declarative Configuration: Manage Keycloak resources as Kubernetes Custom Resources
- Automatic Synchronization: Changes to CRs are automatically applied to Keycloak
- Secret Management: Client secrets are synced to Kubernetes Secrets;
configSecretRefkeeps IdP, LDAP, and other config values out of the CR (Secret references) - Status Tracking: Resource status reflects the current state in Keycloak
- Finalizers: Proper cleanup when resources are deleted
Goals
- Manage Keycloak instances solely through Kubernetes resources
- Provide a GitOps-friendly way to manage Keycloak configuration
- Enable infrastructure-as-code for identity management
- Support multiple Keycloak instances from a single operator
Non-Goals
- Manage the deployment of Keycloak instances (use Keycloak Operator or Helm for that)
- Support other IdM solutions than Keycloak
Supported Resources
The operator manages Keycloak through a set of Custom Resource Definitions covering instances, realms, clients, users, groups, roles, identity providers, federation components, authentication flows, organizations, and more.
A minimal example looks like this:
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRealm
metadata:
name: my-realm
spec:
instanceRef:
name: my-keycloak
definition:
realm: my-realm
enabled: true
displayName: My Realm
See Custom Resource Definitions for the full list of supported resources and their schemas.
Enterprise Support
This operator is developed and maintained by Hostzero GmbH, a provider of sovereign IT infrastructure and security solutions based in Germany.
For organizations with critical infrastructure needs (KRITIS), we offer:
| Service | Description |
|---|---|
| Enterprise Support | SLA-backed support with guaranteed response times |
| Security Consulting | Hardening, compliance audits, and KRITIS certification support |
| On-Premises Deployment | Air-gapped and sovereign cloud deployments |
| Incident Response | 24/7 emergency support for production environments |
| Training | Workshops and certification programs |
→ Contact Hostzero for enterprise solutions
License
This project is licensed under the MIT License. See the LICENSE file for details.
Installation
There are several ways to install the Keycloak Operator:
Helm Chart (Recommended)
The preferred way to install the Keycloak Operator is using the provided Helm chart from our OCI registry.
helm install keycloak-operator oci://ghcr.io/hostzero-gmbh/charts/keycloak-operator \
--namespace keycloak-operator \
--create-namespace
To install a specific version:
helm install keycloak-operator oci://ghcr.io/hostzero-gmbh/charts/keycloak-operator \
--version 0.1.0 \
--namespace keycloak-operator \
--create-namespace
For detailed Helm configuration options, see the Helm Chart documentation.
Kustomize
You can also deploy using kustomize:
# Install CRDs
kubectl apply -k config/crd
# Deploy the operator
kubectl apply -k config/default
From Source
For development or customization:
# Clone the repository
git clone https://github.com/Hostzero-GmbH/keycloak-operator.git
cd keycloak-operator
# Install CRDs
make install
# Run the operator locally
make run
Next Steps
After installation, proceed to the Quick Start guide to create your first Keycloak resources.
Quick Start
This guide will walk you through setting up the Keycloak Operator and creating your first managed resources.
Prerequisites
- A running Kubernetes cluster
kubectlinstalled and configuredhelminstalled (optional, for Helm installation)- A Keycloak instance (or use the provided Kind setup)
Step 1: Install the Operator
Option A: Using Helm (Recommended)
helm install keycloak-operator oci://ghcr.io/hostzero-gmbh/charts/keycloak-operator \
--namespace keycloak-operator \
--create-namespace
Option B: Using Helm from Source
helm install keycloak-operator ./charts/keycloak-operator \
--namespace keycloak-operator \
--create-namespace
Option C: Using Kind (for development)
# This creates a Kind cluster with Keycloak and deploys the operator
make kind-all
Step 2: Create Admin Credentials Secret
Create a secret containing your Keycloak admin credentials:
kubectl create secret generic keycloak-admin-credentials \
--namespace keycloak-operator \
--from-literal=username=admin \
--from-literal=password=your-admin-password
Step 3: Create a KeycloakInstance
Create a KeycloakInstance resource to connect the operator to your Keycloak server:
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakInstance
metadata:
name: my-keycloak
namespace: keycloak-operator
spec:
baseUrl: https://keycloak.example.com
auth:
passwordGrant:
secretRef:
name: keycloak-admin-credentials
Apply it:
kubectl apply -f keycloak-instance.yaml
Verify the connection:
kubectl get keycloakinstances -n keycloak-operator
You should see:
NAME READY URL VERSION AGE
my-keycloak true https://keycloak.example.com 26.0.0 30s
Step 4: Create a Realm
Create a realm in your Keycloak instance:
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRealm
metadata:
name: my-realm
namespace: keycloak-operator
spec:
instanceRef:
name: my-keycloak
definition:
realm: my-realm
displayName: My Application Realm
enabled: true
registrationAllowed: false
loginWithEmailAllowed: true
Apply it:
kubectl apply -f keycloak-realm.yaml
Step 5: Create a Client
Create an OAuth2/OIDC client:
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakClient
metadata:
name: my-app
namespace: keycloak-operator
spec:
realmRef:
name: my-realm
definition:
clientId: my-app
name: My Application
enabled: true
publicClient: false
standardFlowEnabled: true
directAccessGrantsEnabled: false
redirectUris:
- "https://my-app.example.com/callback"
webOrigins:
- "https://my-app.example.com"
clientSecretRef:
name: my-app-credentials
Apply it:
kubectl apply -f keycloak-client.yaml
The operator will create a Kubernetes secret with the client credentials:
kubectl get secret my-app-credentials -n keycloak-operator -o yaml
Step 6: Verify Resources
Check the status of all your Keycloak resources:
kubectl get keycloakinstances,keycloakrealms,keycloakclients -n keycloak-operator
Next Steps
- Learn about Helm Chart configuration
- Explore all Custom Resource Definitions
- Set up a local development environment
Helm Chart Installation
The Keycloak Operator Helm chart provides a flexible way to deploy the operator with customizable settings.
Installation
From OCI Registry (Recommended)
helm install keycloak-operator oci://ghcr.io/hostzero-gmbh/charts/keycloak-operator \
--namespace keycloak-operator \
--create-namespace
To install a specific version:
helm install keycloak-operator oci://ghcr.io/hostzero-gmbh/charts/keycloak-operator \
--version 0.1.0 \
--namespace keycloak-operator \
--create-namespace
From Local Chart
helm install keycloak-operator ./charts/keycloak-operator \
--namespace keycloak-operator \
--create-namespace
With Custom Values
helm install keycloak-operator ./charts/keycloak-operator \
--namespace keycloak-operator \
--create-namespace \
--values my-values.yaml
Configuration
Common Parameters
| Parameter | Description | Default |
|---|---|---|
replicaCount | Number of operator replicas | 1 |
image.repository | Container image repository | ghcr.io/hostzero-gmbh/keycloak-operator |
image.tag | Container image tag | Chart appVersion |
image.pullPolicy | Image pull policy | IfNotPresent |
Resources
| Parameter | Description | Default |
|---|---|---|
resources.limits.cpu | CPU limit | 500m |
resources.limits.memory | Memory limit | 256Mi |
resources.requests.cpu | CPU request | 100m |
resources.requests.memory | Memory request | 128Mi |
Features
| Parameter | Description | Default |
|---|---|---|
leaderElection.enabled | Enable leader election | true |
metrics.enabled | Enable metrics endpoint | true |
metrics.serviceMonitor.enabled | Create Prometheus ServiceMonitor | false |
CRDs
| Parameter | Description | Default |
|---|---|---|
crds.install | Install CRDs with Helm | true |
crds.keep | Keep CRDs on uninstall | true |
Example Values Files
Development
# values-dev.yaml
replicaCount: 1
image:
pullPolicy: Never
tag: "dev"
resources:
limits:
cpu: 200m
memory: 128Mi
leaderElection:
enabled: false
logging:
level: debug
crds:
keep: false
Production
# values-prod.yaml
replicaCount: 2
resources:
limits:
cpu: 1000m
memory: 512Mi
requests:
cpu: 200m
memory: 256Mi
metrics:
serviceMonitor:
enabled: true
podDisruptionBudget:
enabled: true
minAvailable: 1
networkPolicy:
enabled: true
Upgrading
helm upgrade keycloak-operator ./charts/keycloak-operator \
--namespace keycloak-operator \
--values my-values.yaml
Uninstalling
helm uninstall keycloak-operator --namespace keycloak-operator
Note: CRDs are kept by default. To remove them:
kubectl delete crd keycloakinstances.keycloak.hostzero.com
kubectl delete crd keycloakrealms.keycloak.hostzero.com
kubectl delete crd keycloakclients.keycloak.hostzero.com
kubectl delete crd keycloakusers.keycloak.hostzero.com
kubectl delete crd keycloakclientscopes.keycloak.hostzero.com
kubectl delete crd keycloakgroups.keycloak.hostzero.com
kubectl delete crd keycloakidentityproviders.keycloak.hostzero.com
Kind Cluster Setup
This guide explains how to set up a local development environment using Kind (Kubernetes in Docker).
Prerequisites
- Docker
- Kind (
brew install kindorgo install sigs.k8s.io/kind@latest) - kubectl
- Helm
Quick Setup
make kind-all
This creates a Kind cluster and deploys everything:
- Kind cluster with 3 nodes
- Keycloak instance (admin/admin at localhost:8080)
- Operator deployment
- Test KeycloakInstance resource
Development Workflow
# 1. Initial setup (once)
make kind-all
# 2. Start port-forward in a separate terminal
make kind-port-forward
# 3. After code changes, rebuild and restart
make kind-redeploy
# 4. Run tests
make kind-test-run
# 5. Run specific test
make kind-test-run TEST_RUN=TestMyFeature
Commands
| Command | Description |
|---|---|
make kind-all | Full setup: cluster + Keycloak + operator |
make kind-redeploy | Rebuild and restart operator (fast iteration) |
make kind-test-run | Run e2e tests (use TEST_RUN=TestName to filter) |
make kind-logs | Tail operator logs |
make kind-port-forward | Port-forward Keycloak to localhost:8080 |
make kind-reset | Reset cluster to clean state |
make kind-delete | Delete the Kind cluster |
Troubleshooting
Check Operator Logs
make kind-logs
Check Keycloak Logs
kubectl logs -n keycloak -l app=keycloak -f
Verify CRDs
kubectl get crds | grep keycloak
Check Resource Status
kubectl get keycloakinstances,keycloakrealms,keycloakclients -A
Exporting Keycloak Resources
The Keycloak Operator includes an export command that extracts resources from an existing Keycloak instance and generates Kubernetes CRD manifests. This is useful for:
- Migration: Moving from manual Keycloak configuration to operator-managed resources
- Discovery: Generating manifests from an existing Keycloak setup
- Backup: Creating declarative representations of your Keycloak configuration
Quick Start
Export a realm to stdout:
docker run --rm ghcr.io/hostzero-gmbh/keycloak-operator export \
--url https://keycloak.example.com \
--username admin \
--password "$KEYCLOAK_PASSWORD" \
--realm my-realm
Running the Export Command
The export command is included in the operator container image. Run it via Docker:
Direct Connection Mode
Connect directly to a Keycloak instance using URL and credentials:
docker run --rm ghcr.io/hostzero-gmbh/keycloak-operator export \
--url https://keycloak.example.com \
--username admin \
--password "$KEYCLOAK_PASSWORD" \
--realm my-realm \
--target-namespace production \
--instance-ref keycloak-prod
From Existing KeycloakInstance CR
If you already have the operator deployed with a KeycloakInstance configured, you can reuse those connection details:
docker run --rm -v ~/.kube:/root/.kube ghcr.io/hostzero-gmbh/keycloak-operator export \
--from-instance my-keycloak \
--namespace keycloak-operator \
--realm my-realm
This reads the connection URL and credentials from the existing KeycloakInstance CR and its associated Secret.
For cluster-scoped instances:
docker run --rm -v ~/.kube:/root/.kube ghcr.io/hostzero-gmbh/keycloak-operator export \
--from-cluster-instance my-cluster-keycloak \
--realm my-realm
Output Options
Stdout (Default)
Output all manifests as multi-document YAML, suitable for piping:
docker run --rm ghcr.io/hostzero-gmbh/keycloak-operator export \
--url https://keycloak.example.com \
--username admin \
--password "$KEYCLOAK_PASSWORD" \
--realm my-realm \
> manifests.yaml
Or apply directly:
docker run --rm ghcr.io/hostzero-gmbh/keycloak-operator export \
--url https://keycloak.example.com \
--username admin \
--password "$KEYCLOAK_PASSWORD" \
--realm my-realm \
| kubectl apply -f -
Single File
Write all manifests to a single file:
docker run --rm -v $(pwd):/output ghcr.io/hostzero-gmbh/keycloak-operator export \
--url https://keycloak.example.com \
--username admin \
--password "$KEYCLOAK_PASSWORD" \
--realm my-realm \
--output /output/manifests.yaml
Directory Structure
Create an organized directory hierarchy:
docker run --rm -v $(pwd)/manifests:/output ghcr.io/hostzero-gmbh/keycloak-operator export \
--url https://keycloak.example.com \
--username admin \
--password "$KEYCLOAK_PASSWORD" \
--realm my-realm \
--output-dir /output
This creates:
manifests/
realm.yaml
clients/
my-app.yaml
another-client.yaml
users/
john-doe.yaml
groups/
admin-group.yaml
roles/
custom-role.yaml
...
Filtering Resources
Include Specific Types
Export only certain resource types:
docker run --rm ghcr.io/hostzero-gmbh/keycloak-operator export \
--url https://keycloak.example.com \
--username admin \
--password "$KEYCLOAK_PASSWORD" \
--realm my-realm \
--include clients,users,groups
Exclude Types
Skip certain resource types:
docker run --rm ghcr.io/hostzero-gmbh/keycloak-operator export \
--url https://keycloak.example.com \
--username admin \
--password "$KEYCLOAK_PASSWORD" \
--realm my-realm \
--exclude role-mappings,protocol-mappers
Resource Types
Available resource types for filtering:
| Type | Description |
|---|---|
realm | The realm itself |
clients | OAuth2/OIDC clients |
client-scopes | Client scopes |
users | User accounts |
groups | User groups |
roles | Realm and client roles |
role-mappings | Role assignments to users/groups |
identity-providers | External identity providers (SAML, OIDC, etc.). Org-linked IdPs emit spec.organizationRef instead of definition.organizationId. |
components | LDAP federation, key providers, etc. |
protocol-mappers | Token claim mappers |
organizations | Organizations (Keycloak 26+) |
Identity providers linked to an organization are exported with spec.organizationRef pointing at the generated KeycloakOrganization (named from the organization name). The Keycloak organizationId UUID is stripped from definition so the exported manifest applies without being rejected. If the organization cannot be resolved (for example it was deleted), the field is dropped and a warning is logged.
Skip Built-in Resources
By default, Keycloak’s built-in resources are skipped (--skip-defaults=true). These include:
- Default clients:
account,account-console,admin-cli,broker,realm-management,security-admin-console - Default client scopes:
address,email,offline_access,phone,profile,roles,web-origins, etc. - Default roles:
offline_access,uma_authorization,default-roles-{realm} - Service account users (prefixed with
service-account-)
To include built-in resources:
docker run --rm ghcr.io/hostzero-gmbh/keycloak-operator export \
--url https://keycloak.example.com \
--username admin \
--password "$KEYCLOAK_PASSWORD" \
--realm my-realm \
--skip-defaults=false
Manifest Generation Options
Target Namespace
Set the namespace for generated manifests:
docker run --rm ghcr.io/hostzero-gmbh/keycloak-operator export \
--url https://keycloak.example.com \
--username admin \
--password "$KEYCLOAK_PASSWORD" \
--realm my-realm \
--target-namespace production
Instance Reference
Set the KeycloakInstance reference for all generated resources:
docker run --rm ghcr.io/hostzero-gmbh/keycloak-operator export \
--url https://keycloak.example.com \
--username admin \
--password "$KEYCLOAK_PASSWORD" \
--realm my-realm \
--instance-ref keycloak-prod
Realm Reference
Override the realm reference name (defaults to the sanitized realm name):
docker run --rm ghcr.io/hostzero-gmbh/keycloak-operator export \
--url https://keycloak.example.com \
--username admin \
--password "$KEYCLOAK_PASSWORD" \
--realm my-realm \
--realm-ref production-realm
Security Considerations
Secrets Are Never Exported
The export command never exports secrets. This includes:
- Client secrets
- User passwords
- Identity provider client secrets
- LDAP bind credentials (
config.bindCredential)
You must create these secrets separately and set the matching reference on the CR (clientSecretRef, configSecretRef, userSecret). Export does not emit configSecretRef; wire it after creating the Secret. See Secret references.
Password Handling
Pass passwords via environment variable instead of command line:
export KEYCLOAK_PASSWORD="your-password"
docker run --rm -e KEYCLOAK_PASSWORD ghcr.io/hostzero-gmbh/keycloak-operator export \
--url https://keycloak.example.com \
--username admin \
--password "$KEYCLOAK_PASSWORD" \
--realm my-realm
Example Workflow: Migrating to the Operator
- Export existing configuration:
docker run --rm -v $(pwd)/manifests:/output ghcr.io/hostzero-gmbh/keycloak-operator export \
--url https://keycloak.example.com \
--username admin \
--password "$KEYCLOAK_PASSWORD" \
--realm production \
--target-namespace keycloak \
--instance-ref keycloak-prod \
--output-dir /output
- Review and customize manifests:
# Review generated files
ls -la manifests/
# Edit as needed (add client secrets, customize settings)
vim manifests/clients/my-app.yaml
- Create required secrets:
# Create client secrets referenced by the manifests
kubectl create secret generic my-app-credentials \
--namespace keycloak \
--from-literal=client-secret=your-client-secret
# LDAP bind credentials (then set spec.configSecretRef on the exported Component)
kubectl create secret generic ldap-credentials \
--namespace keycloak \
--from-literal=bindCredential=your-ldap-password
- Deploy the operator (if not already installed):
helm install keycloak-operator oci://ghcr.io/hostzero-gmbh/charts/keycloak-operator \
--namespace keycloak-operator \
--create-namespace
- Create the KeycloakInstance:
# Create credentials secret
kubectl create secret generic keycloak-admin \
--namespace keycloak \
--from-literal=username=admin \
--from-literal=password=your-admin-password
# Apply instance configuration
kubectl apply -f - <<EOF
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakInstance
metadata:
name: keycloak-prod
namespace: keycloak
spec:
baseUrl: https://keycloak.example.com
auth:
passwordGrant:
secretRef:
name: keycloak-admin
EOF
- Apply the exported manifests:
kubectl apply -f manifests/
Command Reference
Usage: keycloak-operator export [options]
Connection Options (choose one mode):
--url Keycloak server URL
--username Admin username
--password Admin password (or use KEYCLOAK_PASSWORD env var)
--from-instance Name of KeycloakInstance CR
--from-cluster-instance Name of ClusterKeycloakInstance CR
--namespace Namespace of KeycloakInstance
Export Options:
--realm Realm to export (required)
Output Options:
--output Output file path (default: stdout)
--output-dir Output directory (creates file structure)
Manifest Options:
--target-namespace Namespace for generated manifests (default: "default")
--instance-ref KeycloakInstance name to reference
--realm-ref KeycloakRealm name to reference
Filtering Options:
--include Resource types to include (comma-separated)
--exclude Resource types to exclude (comma-separated)
--skip-defaults Skip built-in Keycloak resources (default: true)
General Options:
--verbose Enable verbose output
Configuration
The Keycloak Operator can be configured through various mechanisms:
- Helm Values: For deployment-time configuration
- Environment Variables: For runtime configuration
- Command-Line Flags: For operator behavior
Operator Configuration
The operator accepts the following configuration options:
| Option | Description | Default |
|---|---|---|
--metrics-bind-address | Address for metrics endpoint | :8080 |
--health-probe-bind-address | Address for health probes | :8081 |
--leader-elect | Enable leader election | false |
--zap-log-level | Log level (debug, info, error) | info |
--zap-encoder | Log format (json, console) | json |
--zap-devel | Development logging (console, more verbose) | false |
Keycloak Connection
Each KeycloakInstance resource defines how to connect to a Keycloak server:
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakInstance
metadata:
name: my-keycloak
spec:
# Base URL of the Keycloak server
baseUrl: https://keycloak.example.com
# Realm to authenticate against (default: master)
realm: master
# Authentication: exactly one of auth.passwordGrant or auth.clientCredentials.
auth:
passwordGrant:
secretRef:
name: keycloak-credentials
namespace: keycloak-operator # Optional, defaults to resource namespace
usernameKey: username # Optional, defaults to "username"
passwordKey: password # Optional, defaults to "password"
See KeycloakInstance for the full auth reference,
including the clientCredentials (OAuth2 service-account) variant.
Resource References
Resources reference their parent using *Ref fields. All namespaced references
are same-namespace only - every *Ref field resolves in the referring
resource’s own namespace. For cross-namespace sharing, use the cluster-scoped
ClusterKeycloakRealm / ClusterKeycloakInstance (via clusterRealmRef /
clusterInstanceRef).
# Realm references an Instance (same namespace)
spec:
instanceRef:
name: my-keycloak
# Client references a Realm (same namespace)
spec:
realmRef:
name: my-realm
See Also
Environment Variables
The operator is configured primarily through command-line flags (set by the Helm chart). A few environment variables are injected or read at runtime.
Operator Settings
| Variable | Description | Default |
|---|---|---|
POD_NAMESPACE | Namespace where the operator is running | Injected by Kubernetes |
POD_NAME | Name of the operator pod | Injected by Kubernetes |
Logging
Logging is configured with zap flags, not environment variables. Via Helm:
logging:
level: info # debug, info, error
format: json # json, console
development: false
Or locally:
go run ./cmd --zap-log-level=debug --zap-encoder=console --zap-devel
OpenTelemetry
OTLP export is enabled when an endpoint is set. See Monitoring.
| Variable | Description |
|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT | Collector endpoint (enables traces and logs) |
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT | Override for traces only |
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT | Override for logs only |
OTEL_EXPORTER_OTLP_PROTOCOL | grpc (default via Helm) or http/protobuf |
OTEL_SERVICE_NAME | Defaults to keycloak-operator when using the Helm chart |
OTEL_TRACES_SAMPLER | SDK sampler, e.g. parentbased_traceidratio |
OTEL_RESOURCE_ATTRIBUTES | Extra resource attributes |
Any other standard OTEL_* variable is passed through to the SDK. Use Helm extraEnv for settings not covered by otel.*.
Helm Values Reference
Complete reference for all Helm chart values.
Global
# Number of replicas
replicaCount: 1
# Image configuration
image:
repository: ghcr.io/hostzero/keycloak-operator
pullPolicy: IfNotPresent
tag: "" # Defaults to Chart.appVersion
# Image pull secrets
imagePullSecrets: []
# Name overrides
nameOverride: ""
fullnameOverride: ""
Service Account
serviceAccount:
create: true
annotations: {}
name: ""
Pod Configuration
# Pod annotations
podAnnotations: {}
# Pod labels
podLabels: {}
# Pod security context
podSecurityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
# Container security context
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
Resources
resources:
limits:
cpu: 500m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
Scheduling
nodeSelector: {}
tolerations: []
affinity: {}
priorityClassName: ""
Leader Election
leaderElection:
enabled: true
Metrics
metrics:
enabled: true
port: 8080
serviceMonitor:
enabled: false
additionalLabels: {}
interval: 30s
scrapeTimeout: 10s
Health Probes
health:
port: 8081
Logging
Passed through to --zap-log-level, --zap-encoder, and --zap-devel.
logging:
level: info # debug, info, error
format: json # json, console
development: false
OpenTelemetry
otel:
enabled: false
endpoint: "" # e.g. http://otel-collector:4317
protocol: grpc # or http/protobuf
Additional OTEL_* env vars via extraEnv. See Monitoring.
Performance Tuning
performance:
# Sync period for re-checking successfully reconciled resources
# Higher values reduce Keycloak API load but increase drift detection time
syncPeriod: "5m" # e.g., "5m", "30m", "1h"
# Maximum concurrent requests to Keycloak (0 = no limit)
# Lower values reduce Keycloak load but slow reconciliation
maxConcurrentRequests: 10
For large deployments (100+ resources), consider:
performance:
syncPeriod: "30m"
maxConcurrentRequests: 5
RBAC
rbac:
create: true
CRDs
crds:
install: true
keep: true # Keep CRDs on uninstall
Extra Configuration
# Additional environment variables
extraEnv: []
# - name: MY_VAR
# value: my-value
# Additional volumes
extraVolumes: []
# Additional volume mounts
extraVolumeMounts: []
High Availability
# Termination grace period
terminationGracePeriodSeconds: 10
# Network policy
networkPolicy:
enabled: false
ingress: []
egress: []
# Pod disruption budget
podDisruptionBudget:
enabled: false
minAvailable: 1
maxUnavailable: ""
Custom Resource Definitions
The Keycloak Operator provides several Custom Resource Definitions (CRDs) to manage Keycloak resources declaratively.
Resource Hierarchy
KeycloakInstance / ClusterKeycloakInstance
└── KeycloakRealm / ClusterKeycloakRealm
├── KeycloakClient
│ ├── KeycloakUser (service account, via clientRef)
│ ├── KeycloakRole (client role)
│ └── KeycloakProtocolMapper
├── KeycloakUser (regular users, via realmRef)
│ └── KeycloakUserCredential
├── KeycloakGroup
├── KeycloakClientScope
│ └── KeycloakProtocolMapper
├── KeycloakRole (realm role)
├── KeycloakRoleMapping (maps roles to Users/Groups)
├── KeycloakComponent (LDAP, key providers, etc.)
├── KeycloakIdentityProvider
│ └── KeycloakIdentityProviderMapper
├── KeycloakAuthenticationFlow
├── KeycloakRequiredAction
└── KeycloakOrganization (requires Keycloak 26+)
Overview
Instance Resources
| CRD | Description | Scope |
|---|---|---|
| KeycloakInstance | Connection to a Keycloak server | Namespaced |
| ClusterKeycloakInstance | Cluster-scoped Keycloak connection | Cluster |
Realm Resources
| CRD | Description | Parent |
|---|---|---|
| KeycloakRealm | Realm configuration | KeycloakInstance |
| ClusterKeycloakRealm | Cluster-scoped realm | ClusterKeycloakInstance |
OAuth & Client Resources
| CRD | Description | Parent |
|---|---|---|
| KeycloakClient | OIDC or SAML client | KeycloakRealm |
| KeycloakClientScope | Client scope configuration | KeycloakRealm |
| KeycloakProtocolMapper | Token claim mappers | KeycloakClient or KeycloakClientScope |
Identity Resources
| CRD | Description | Parent |
|---|---|---|
| KeycloakUser | User management | KeycloakRealm or KeycloakClient¹ |
| KeycloakUserCredential | User password management | KeycloakUser |
| KeycloakGroup | Group management | KeycloakRealm |
Role & Access Control
| CRD | Description | Parent |
|---|---|---|
| KeycloakRole | Realm and client roles | KeycloakRealm or KeycloakClient |
| KeycloakRoleMapping | Role-to-subject mappings | KeycloakUser or KeycloakGroup |
Federation & Infrastructure
| CRD | Description | Parent |
|---|---|---|
| KeycloakComponent | LDAP federation, key providers | KeycloakRealm |
| KeycloakIdentityProvider | External identity providers | KeycloakRealm |
| KeycloakIdentityProviderMapper | Identity provider claim/role/attribute mappers | KeycloakIdentityProvider |
| KeycloakAuthenticationFlow | Custom authentication / registration flows | KeycloakRealm |
| KeycloakRequiredAction | Required action providers (e.g. update password, verify email) | KeycloakRealm |
| KeycloakOrganization | Organization management² | KeycloakRealm |
¹ KeycloakUser supports clientRef for managing service account users associated with a client
² KeycloakOrganization requires Keycloak 26.0.0 or later
Common Patterns
Spec Layout
Every CRD that mirrors a Keycloak representation is built from the same four layers:
- Placement refs — typed references that anchor the resource in the hierarchy:
realmRef,clusterRealmRef,clientRef,clientScopeRef,parentGroupRef,identityProviderRef, … - Identity — one typed, required, immutable field naming the object in Keycloak:
realmName,clientId,username,alias, orname. It is mirrored to status and shown as a printer column. - Kubernetes integration — typed fields for anything that touches cluster objects or other CRs: Secret references (
clientSecretRef,smtpSecretRef,configSecretRef),organizationRef,initialPassword,tokenExchange. The operator injects these into the payload or applies them through separate Keycloak API calls. See Secret references. - Payload —
spec.definition, the verbatim Keycloak API representation. The operator writes into it (identifier, secret merges) but never reads configuration out of it; everything else passes through unchanged. This is what lets you configure any Keycloak property, even ones the CRD does not model.
spec:
instanceRef: # 1. placement
name: my-keycloak
realmName: my-realm # 2. identity
smtpSecretRef: # 3. Kubernetes integration
name: smtp-credentials
definition: # 4. payload — full Keycloak API object
enabled: true
displayName: My Realm
Every datum has exactly one home. A value that belongs to a typed layer must not also be configured inside definition: an identifier in the definition is tolerated only when it matches the spec field, and a conflicting value sets Ready=False.
Fully typed CRDs without a definition (KeycloakInstance, KeycloakRoleMapping, KeycloakUserCredential) are operator constructs rather than representation mirrors, so they have no payload layer.
For contributors, the layer of a new field follows mechanically:
- References a Kubernetes object or another CR? → typed spec field (layer 3).
- Managed through a separate Keycloak API endpoint rather than the representation
PUT? → its own CRD (like KeycloakRoleMapping or KeycloakProtocolMapper) or a typed spec section — never keys insidedefinition. Such a key is rejected withReady=False, because Keycloak may accept the parentPUTwhile discarding the nested change. - Part of the Keycloak representation itself? → stays in
definition, untyped.
Placement References
A resource names exactly one parent, and if that parent implies the realm, the realm is not named again. Setting more than one, or none, is rejected at admission.
| CRD | Placement refs |
|---|---|
KeycloakRealm, ClusterKeycloakRealm | instanceRef / clusterInstanceRef |
KeycloakClient, KeycloakClientScope, KeycloakComponent, KeycloakOrganization, KeycloakIdentityProvider, KeycloakRequiredAction, KeycloakAuthenticationFlow | realmRef / clusterRealmRef |
KeycloakRole, KeycloakUser | realmRef / clusterRealmRef / clientRef |
KeycloakGroup | realmRef / clusterRealmRef / parentGroupRef |
KeycloakProtocolMapper | clientRef / clientScopeRef |
KeycloakRoleMapping | subject.userRef / subject.groupRef / subject.serviceAccountRef |
KeycloakIdentityProviderMapper | identityProviderRef |
KeycloakUserCredential | userRef |
Where a ref points at something below the realm, the realm is derived from it: a client role reads the realm of its clientRef, and a nested group inherits the realm carried by the root of its parentGroupRef chain. Restating it alongside would allow the two to disagree, which is why it is rejected rather than merely redundant.
Secret and ConfigMap references (clientSecretRef, smtpSecretRef, configSecretRef, …) are layer 3, not placement, and are unaffected by this rule.
Status Tracking
All resources expose status information:
status:
ready: true
message: "Resource synchronized successfully"
conditions:
- type: Ready
status: "True"
lastTransitionTime: "2024-01-01T00:00:00Z"
reason: Synchronized
message: "Resource is in sync with Keycloak"
Finalizers
Resources use finalizers to ensure proper cleanup when deleted:
metadata:
finalizers:
- keycloak.hostzero.com/finalizer
Preserving Resources on Deletion
By default, when you delete a Custom Resource, the operator also deletes the corresponding resource in Keycloak. If you want to keep the resource in Keycloak while removing the CR from Kubernetes, use the keycloak.hostzero.com/preserve-resource annotation:
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRealm
metadata:
name: my-realm
annotations:
keycloak.hostzero.com/preserve-resource: "true"
spec:
# ...
When this annotation is set to "true", deleting the CR will:
- Remove the CR from Kubernetes
- Keep the resource in Keycloak untouched
This is useful for scenarios like:
- Migrating management of a resource to a different system
- Temporarily removing operator control without losing data
- Testing or debugging without affecting production resources
Note: The annotation value must be exactly
"true"(as a string) to preserve the resource. Any other value (or absence of the annotation) will result in normal deletion behavior.
Supported Resources: This annotation works with all resource types except KeycloakInstance and ClusterKeycloakInstance (which don’t manage Keycloak resources directly).
API Version
All CRDs use the keycloak.hostzero.com/v1beta1 API version:
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRealm
Secret references
Sensitive values belong in Kubernetes Secrets, not in Custom Resource YAML. The operator has two kinds of secret fields.
configSecretRef
CRs whose Keycloak payload has a definition.config map can merge every key from a Secret into that map before the operator talks to Keycloak:
- KeycloakIdentityProvider
- KeycloakComponent
- KeycloakProtocolMapper
- KeycloakIdentityProviderMapper
- KeycloakRequiredAction
Secret keys must already be the Keycloak config names (bindCredential, clientSecret, not password). Extra keys in the Secret are also pushed to Keycloak. Secret values win over the same key in definition.config.
The Secret must live in the same namespace as the CR. When the Secret changes, the operator re-reconciles.
kubectl create secret generic ldap-credentials \
--from-literal=bindCredential=s3cret
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakComponent
metadata:
name: ldap-federation
spec:
realmRef:
name: my-realm
name: corporate-ldap
configSecretRef:
name: ldap-credentials
definition:
providerId: ldap
providerType: org.keycloak.storage.UserStorageProvider
config:
enabled: ["true"]
vendor: ["ad"]
bindDn: ["cn=admin,dc=example,dc=com"]
Component config is a list of strings per key. The operator wraps each Secret value as ["…"] (bindCredential: "s3cret" in the Secret becomes ["s3cret"] in Keycloak). Identity providers, protocol mappers, identity provider mappers, and required actions keep string values.
Other secret APIs
Do not use configSecretRef for these. They have their own typed fields:
| Resource | Field | Purpose |
|---|---|---|
| KeycloakClient | clientSecretRef | OAuth client id / secret |
| KeycloakRealm / ClusterKeycloakRealm | smtpSecretRef | SMTP username and password |
| KeycloakUserCredential | userSecret | User password |
| KeycloakInstance / ClusterKeycloakInstance | auth.*.secretRef | Operator credentials to Keycloak |
KeycloakInstance
A KeycloakInstance represents a connection to a Keycloak server. It is the root resource for managing Keycloak configuration in a namespace.
Specification
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakInstance
metadata:
name: my-keycloak
spec:
baseUrl: https://keycloak.example.com
# Optional: admin realm to authenticate against (default: master)
realm: master
# Required: exactly one of auth.passwordGrant or auth.clientCredentials
auth:
# Password grant via an admin user (e.g. master-realm admin)
passwordGrant:
# Optional inline username; when set, overrides secretRef.usernameKey
username: admin
secretRef:
name: keycloak-admin
# Optional: namespace of the secret (defaults to resource namespace)
namespace: keycloak-operator
# Optional: keys inside the secret (defaults shown)
usernameKey: username
passwordKey: password
# OR: client_credentials grant via a confidential client
clientCredentials:
# Optional inline client_id; when set, overrides secretRef.clientIdKey
clientId: keycloak-operator
secretRef:
name: keycloak-operator-client
namespace: keycloak-operator
clientIdKey: client-id
clientSecretKey: client-secret
# Optional: TLS verification for the Keycloak HTTPS endpoint
tls:
# Reference a PEM-encoded CA bundle from a Secret or ConfigMap
caCert:
# Use either secretRef OR configMapRef (mutually exclusive)
secretRef:
name: keycloak-ca
# Optional: defaults to the KeycloakInstance namespace
namespace: keycloak-operator
# Optional: key inside the secret (default: ca.crt)
key: ca.crt
# configMapRef:
# name: keycloak-ca
# key: ca.crt
# Disable TLS verification entirely. Do not use in production.
insecureSkipVerify: false
# Optional: token caching configuration
token:
secretName: keycloak-token-cache
tokenKey: token
expiresKey: expires
TLS
spec.tls is optional. When omitted, the operator uses the system CA pool to
verify the Keycloak server certificate.
tls.caCertreferences a PEM-encoded CA bundle from either aSecretor aConfigMap. Exactly one ofsecretRef/configMapRefmay be set; setting both is rejected by admission. The default key isca.crt.tls.insecureSkipVerify: truedisables certificate verification. When set,caCertis ignored.
Authentication
Exactly one of auth.passwordGrant or auth.clientCredentials must be set; the
admission webhook rejects specs that omit both or set both. auth.passwordGrant
issues a password-grant token (typical for the master-realm admin user);
auth.clientCredentials issues a client_credentials token against a
confidential client / service account.
Username and client_id are not secrets, so they can be either inlined on the
spec or read from a key of the referenced Secret. When the inline field is set,
the corresponding *Key field on secretRef is ignored. Passwords and client
secrets always come from the Secret.
Credentials Secret (password grant)
apiVersion: v1
kind: Secret
metadata:
name: keycloak-admin
type: Opaque
stringData:
username: admin
password: your-secure-password
Client credentials Secret
apiVersion: v1
kind: Secret
metadata:
name: keycloak-operator-client
type: Opaque
stringData:
client-id: keycloak-operator
client-secret: your-client-secret
A one-liner:
kubectl create secret generic keycloak-operator-client \
--from-literal=client-id=keycloak-operator \
--from-literal=client-secret=$(openssl rand -hex 32)
Status
status:
ready: true
version: "26.0.0"
status: "Ready"
message: "Connected to Keycloak"
conditions:
- type: Ready
status: "True"
reason: Ready
lastTransitionTime: "2024-01-01T12:00:00Z"
Examples
Basic instance with password grant
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakInstance
metadata:
name: production-keycloak
namespace: keycloak-operator
spec:
baseUrl: https://auth.example.com
auth:
passwordGrant:
secretRef:
name: keycloak-admin
Service-account client
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakInstance
metadata:
name: production-keycloak
spec:
baseUrl: https://auth.example.com
auth:
clientCredentials:
secretRef:
name: keycloak-operator-client
Migrating from the pre-auth shape
Earlier v1beta1 revisions used spec.credentials and spec.client at the
top level. The fields have been replaced by spec.auth. Existing manifests
must be migrated; the operator rejects the old shape.
Password grant
Before:
spec:
baseUrl: https://auth.example.com
credentials:
secretRef:
name: keycloak-admin
usernameKey: username
passwordKey: password
After:
spec:
baseUrl: https://auth.example.com
auth:
passwordGrant:
secretRef:
name: keycloak-admin
usernameKey: username
passwordKey: password
Client credentials
Before (client_id and client_secret were inlined in the spec):
spec:
baseUrl: https://auth.example.com
credentials:
secretRef:
name: dummy-unused-admin
client:
id: keycloak-operator
secret: my-client-secret
After (move the credentials into a Secret, drop the dummy credentials block):
spec:
baseUrl: https://auth.example.com
auth:
clientCredentials:
secretRef:
name: keycloak-operator-client
with a companion Secret:
kubectl create secret generic keycloak-operator-client \
--from-literal=client-id=keycloak-operator \
--from-literal=client-secret=my-client-secret
Short names
| Alias | Full name |
|---|---|
kci | keycloakinstances |
kubectl get kci
ClusterKeycloakInstance
ClusterKeycloakInstance is the cluster-scoped counterpart of
KeycloakInstance. Resources in any namespace can
reference it, making it useful for a shared Keycloak server on a multi-tenant
platform.
Example
Password grant
apiVersion: keycloak.hostzero.com/v1beta1
kind: ClusterKeycloakInstance
metadata:
name: central-keycloak
spec:
baseUrl: https://keycloak.example.com
auth:
passwordGrant:
secretRef:
name: keycloak-admin
namespace: keycloak-system
Service-account client
apiVersion: keycloak.hostzero.com/v1beta1
kind: ClusterKeycloakInstance
metadata:
name: central-keycloak
spec:
baseUrl: https://keycloak.example.com
auth:
clientCredentials:
secretRef:
name: keycloak-operator-client
namespace: keycloak-system
Authentication
Same rules as KeycloakInstance: exactly one of
auth.passwordGrant / auth.clientCredentials; passwords and client secrets
always live in a Secret; username / clientId may be inlined.
The only difference: secretRef.namespace is required because the resource
is cluster-scoped.
TLS
spec.tls mirrors the namespaced KeycloakInstance.spec.tls shape, with one
difference: every namespace field is required.
spec:
tls:
caCert:
configMapRef:
name: keycloak-ca
namespace: keycloak-system
# key defaults to "ca.crt"
# insecureSkipVerify: true # disables verification, ignores caCert
Exactly one of caCert.secretRef / caCert.configMapRef may be set; setting
both is rejected by admission.
Spec
| Field | Type | Description | Required |
|---|---|---|---|
baseUrl | string | URL of the Keycloak server | Yes |
auth.passwordGrant / auth.clientCredentials | object | Authentication method (exactly one) | Yes |
auth.passwordGrant.username | string | Inline admin username (overrides secretRef.usernameKey) | No |
auth.passwordGrant.secretRef.name | string | Name of the credentials Secret | Yes |
auth.passwordGrant.secretRef.namespace | string | Namespace of the credentials Secret | Yes |
auth.passwordGrant.secretRef.usernameKey | string | Secret key for the username | No (default username) |
auth.passwordGrant.secretRef.passwordKey | string | Secret key for the password | No (default password) |
auth.clientCredentials.clientId | string | Inline client id (overrides secretRef.clientIdKey) | No |
auth.clientCredentials.secretRef.name | string | Name of the client-credentials Secret | Yes |
auth.clientCredentials.secretRef.namespace | string | Namespace of the client-credentials Secret | Yes |
auth.clientCredentials.secretRef.clientIdKey | string | Secret key for the client id | No (default client-id) |
auth.clientCredentials.secretRef.clientSecretKey | string | Secret key for the client secret | No (default client-secret) |
realm | string | Admin realm name | No (default master) |
tls.caCert.secretRef / tls.caCert.configMapRef | object | PEM-encoded CA bundle source (exactly one) | No |
tls.insecureSkipVerify | bool | Disable TLS verification (overrides caCert) | No (default false) |
token.* | object | Token cache configuration | No |
Comparison with KeycloakInstance
| Aspect | KeycloakInstance | ClusterKeycloakInstance |
|---|---|---|
| Scope | Namespaced | Cluster |
| Secret namespace | Optional (defaults to same as resource) | Required |
| Accessible from | Same namespace only | Any namespace |
| Short name | kci | ckci |
Migrating from the pre-auth shape
The legacy spec.credentials / spec.client blocks have been replaced by
spec.auth. Migrate existing manifests as shown in the
KeycloakInstance migration guide;
the only difference is that secretRef.namespace must always be set for
cluster-scoped resources.
KeycloakRealm
Identifier field: Set the realm name in the
spec.realmNamefield. It is required and immutable once set. Arealmkey insidespec.definitionis tolerated only when it matchesspec.realmName; a conflicting value is rejected.
spec.realmNameis immutable once set. A realm rename in Keycloak is destructive and would orphan the realm, so the value is locked after the first time it is set (the one-time migration from unset to a value is permitted).
A KeycloakRealm represents a realm within a Keycloak instance.
Specification
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRealm
metadata:
name: my-realm
spec:
# One of instanceRef or clusterInstanceRef must be specified
# Option 1: Reference to a namespaced KeycloakInstance
instanceRef:
name: my-keycloak
# Option 2: Reference to a ClusterKeycloakInstance
# clusterInstanceRef:
# name: my-cluster-instance
# Required: Realm name in Keycloak (do NOT set realm in definition)
realmName: my-realm
# Required: Realm definition (Keycloak RealmRepresentation)
definition:
displayName: My Realm
enabled: true
# ... any other Keycloak realm properties
Status
status:
ready: true
status: "Ready"
message: "Realm synchronized successfully"
resourcePath: "/admin/realms/my-realm"
instance:
instanceRef: my-keycloak
conditions:
- type: Ready
status: "True"
reason: Synchronized
Example
Basic Realm
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRealm
metadata:
name: my-app-realm
spec:
instanceRef:
name: production-keycloak
realmName: my-app
definition:
displayName: My Application
enabled: true
With ClusterKeycloakInstance
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRealm
metadata:
name: my-app-realm
spec:
clusterInstanceRef:
name: central-keycloak
realmName: my-app
definition:
displayName: My Application
enabled: true
Full Configuration
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRealm
metadata:
name: production-realm
spec:
instanceRef:
name: production-keycloak
realmName: production
definition:
displayName: Production Realm
enabled: true
# Login settings
registrationAllowed: false
registrationEmailAsUsername: true
loginWithEmailAllowed: true
duplicateEmailsAllowed: false
resetPasswordAllowed: true
rememberMe: true
# Session settings
ssoSessionIdleTimeout: 1800
ssoSessionMaxLifespan: 36000
accessTokenLifespan: 300
# Security settings
bruteForceProtected: true
permanentLockout: false
maxFailureWaitSeconds: 900
minimumQuickLoginWaitSeconds: 60
waitIncrementSeconds: 60
quickLoginCheckMilliSeconds: 1000
maxDeltaTimeSeconds: 43200
failureFactor: 5
# Themes
loginTheme: keycloak
accountTheme: keycloak
adminTheme: keycloak
emailTheme: keycloak
# SMTP settings (non-sensitive parts in definition)
smtpServer:
host: smtp.example.com
port: "587"
fromDisplayName: My App
from: noreply@example.com
starttls: "true"
auth: "true"
# SMTP credentials from a Kubernetes Secret (recommended over plaintext in definition)
smtpSecretRef:
name: my-smtp-credentials
userKey: user # optional, defaults to "user"
passwordKey: password # optional, defaults to "password"
SMTP Credentials from Secret
To avoid storing SMTP credentials in plaintext in the CR, use smtpSecretRef to reference a Kubernetes Secret:
kubectl create secret generic smtp-credentials \
--from-literal=user=smtp-user@example.com \
--from-literal=password=my-smtp-password
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRealm
metadata:
name: my-realm
spec:
instanceRef:
name: my-keycloak
smtpSecretRef:
name: smtp-credentials
# userKey: user # default
# passwordKey: password # default
realmName: my-realm
definition:
enabled: true
smtpServer:
host: smtp.example.com
port: "587"
from: noreply@example.com
starttls: "true"
auth: "true"
The operator reads the user and password values from the referenced secret and injects them into smtpServer before sending the realm configuration to Keycloak. The secret must exist in the same namespace as the KeycloakRealm.
For ClusterKeycloakRealm, the secret namespace must be specified explicitly:
apiVersion: keycloak.hostzero.com/v1beta1
kind: ClusterKeycloakRealm
metadata:
name: my-realm
spec:
clusterInstanceRef:
name: central-keycloak
smtpSecretRef:
name: smtp-credentials
namespace: keycloak-system
realmName: my-realm
definition:
enabled: true
smtpServer:
host: smtp.example.com
port: "587"
from: noreply@example.com
starttls: "true"
auth: "true"
When the referenced secret changes, the operator automatically re-reconciles the realm to pick up the new credentials.
Definition Properties
The definition field accepts any property from the Keycloak RealmRepresentation.
Common properties:
| Property | Type | Description |
|---|---|---|
realm | string | Realm name (required) |
displayName | string | Display name for the realm |
enabled | boolean | Whether the realm is enabled |
registrationAllowed | boolean | Allow user registration |
loginWithEmailAllowed | boolean | Allow login with email |
ssoSessionIdleTimeout | integer | SSO session idle timeout (seconds) |
accessTokenLifespan | integer | Access token lifespan (seconds) |
Binding Custom Authentication Flows
A realm definition may bind built-in authentication points to custom flows via browserFlow, registrationFlow, directGrantFlow, resetCredentialsFlow, clientAuthenticationFlow, or dockerAuthenticationFlow. Keycloak rejects realm imports that reference a flow alias which does not yet exist (see keycloak/keycloak#23980), which would otherwise prevent declaratively creating the realm and the flow at the same time.
The operator works around this with deferred bindings:
- On the first
CreateRealmcall, any flow-binding fields whose target alias does not yet exist in Keycloak are stripped before the request is sent. The realm is created and markedReady; the operator records that bindings were deferred. - The realm controller watches
KeycloakAuthenticationFlowresources and requeues the realm immediately when a referenced flow becomes ready, instead of waiting for the next periodic resync. - On the next reconcile (either triggered by the watch or by the periodic resync) the operator updates the realm with the original bindings now that the referenced flows exist.
Practically this means you can apply a KeycloakRealm and its KeycloakAuthenticationFlow resources together — in any order — and convergence happens within seconds.
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRealm
metadata:
name: my-realm
spec:
instanceRef:
name: my-keycloak
realmName: my-realm
definition:
enabled: true
browserFlow: my-custom-browser # may be created later
registrationFlow: my-custom-registration # may be created later
---
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakAuthenticationFlow
metadata:
name: my-custom-browser
spec:
realmRef:
name: my-realm
alias: my-custom-browser
providerId: basic-flow
executions:
- authenticator: auth-cookie
requirement: ALTERNATIVE
See KeycloakAuthenticationFlow for the flow CRD reference.
Identity Providers and Mappers
Identity providers themselves can be embedded in definition.identityProviders for initial realm creation, but Keycloak’s PUT /admin/realms/{realm} endpoint does not consume the identityProviders or identityProviderMappers fields, so they are silently dropped on realm updates. To declaratively manage identity providers and their mappers on existing realms (including the master realm), use the dedicated CRDs:
- KeycloakIdentityProvider — manages the identity provider instance.
- KeycloakIdentityProviderMapper — manages claim, role, and attribute mappers attached to an identity provider.
Preserving Realm on Deletion
To keep the realm in Keycloak when deleting the CR:
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRealm
metadata:
name: my-realm
annotations:
keycloak.hostzero.com/preserve-resource: "true"
spec:
instanceRef:
name: my-keycloak
realmName: my-realm
definition:
enabled: true
See Common Patterns for more details.
Short Names
| Alias | Full Name |
|---|---|
kcrm | keycloakrealms |
kubectl get kcrm
ClusterKeycloakRealm
Identifier field: Set the realm name in the
spec.realmNamefield. It is required and immutable once set. Arealmkey insidespec.definitionis tolerated only when it matchesspec.realmName; a conflicting value is rejected.
spec.realmNameis immutable once set. A realm rename in Keycloak is destructive and would orphan the realm, so the value is locked after the first time it is set (the one-time migration from unset to a value is permitted).
The ClusterKeycloakRealm resource defines a Keycloak realm at the cluster level, making it accessible to resources in any namespace.
Overview
This is the cluster-scoped equivalent of KeycloakRealm. Use it when:
- You need a realm that can be referenced from multiple namespaces
- You’re using
ClusterKeycloakInstancefor your Keycloak server - You want centralized realm management with distributed client/user definitions
Examples
Basic Cluster Realm
apiVersion: keycloak.hostzero.com/v1beta1
kind: ClusterKeycloakRealm
metadata:
name: shared-realm
spec:
clusterInstanceRef:
name: central-keycloak
realmName: shared
definition:
enabled: true
displayName: Shared Platform Realm
With Namespaced Instance
apiVersion: keycloak.hostzero.com/v1beta1
kind: ClusterKeycloakRealm
metadata:
name: company-realm
spec:
instanceRef:
name: keycloak-instance
namespace: keycloak-system
realmName: company
definition:
enabled: true
loginWithEmailAllowed: true
registrationAllowed: false
Full Configuration
apiVersion: keycloak.hostzero.com/v1beta1
kind: ClusterKeycloakRealm
metadata:
name: production-realm
spec:
clusterInstanceRef:
name: production-keycloak
realmName: prod # Override the Keycloak realm name
definition:
enabled: true
displayName: Production
sslRequired: external
registrationAllowed: false
loginWithEmailAllowed: true
duplicateEmailsAllowed: false
resetPasswordAllowed: true
verifyEmail: true
bruteForceProtected: true
accessTokenLifespan: 300
ssoSessionIdleTimeout: 1800
ssoSessionMaxLifespan: 36000
loginTheme: keycloak
accountTheme: keycloak
adminTheme: keycloak
emailTheme: keycloak
Spec
| Field | Type | Description | Required |
|---|---|---|---|
clusterInstanceRef.name | string | Reference to ClusterKeycloakInstance | One of these |
instanceRef.name | string | Reference to namespaced KeycloakInstance | One of these |
instanceRef.namespace | string | Namespace of the KeycloakInstance | Required if instanceRef |
realmName | string | Realm name in Keycloak (must not conflict with a realm key in definition) | Yes |
definition | object | Keycloak RealmRepresentation | Yes |
Definition Fields
The definition field accepts any valid Keycloak RealmRepresentation. Common fields include:
| Field | Type | Description |
|---|---|---|
realm | string | Realm identifier (required) |
enabled | boolean | Whether realm is enabled |
displayName | string | Display name |
sslRequired | string | SSL requirement: all, external, none |
registrationAllowed | boolean | Allow user self-registration |
loginWithEmailAllowed | boolean | Allow login with email |
verifyEmail | boolean | Require email verification |
resetPasswordAllowed | boolean | Enable password reset |
bruteForceProtected | boolean | Enable brute force protection |
accessTokenLifespan | integer | Access token lifetime (seconds) |
ssoSessionIdleTimeout | integer | SSO session idle timeout (seconds) |
loginTheme | string | Login page theme |
Status
| Field | Type | Description |
|---|---|---|
ready | boolean | Whether the realm is synced |
status | string | Current status (Ready, InstanceNotReady, CreateFailed, etc.) |
message | string | Additional status information |
resourcePath | string | Keycloak API path for this realm |
realmName | string | Actual realm name in Keycloak |
instance | object | Resolved instance reference |
conditions | []Condition | Kubernetes conditions |
Behavior
Instance Resolution
The controller supports two instance reference types:
- clusterInstanceRef: References a
ClusterKeycloakInstanceby name - instanceRef: References a namespaced
KeycloakInstanceby name and namespace
One of these must be specified.
Realm Synchronization
On each reconciliation:
- Connect to Keycloak using the referenced instance
- Check if the realm exists
- Create or update the realm with the specified definition
- Update status with the resource path
Cleanup
When a ClusterKeycloakRealm is deleted:
- The finalizer removes the realm from Keycloak
- All resources in Keycloak (clients, users, etc.) within that realm are deleted
- The Kubernetes resource is then removed
Use Cases
Multi-Tenant Platform
# Central Keycloak instance
apiVersion: keycloak.hostzero.com/v1beta1
kind: ClusterKeycloakInstance
metadata:
name: platform-keycloak
spec:
baseUrl: https://auth.example.com
auth:
passwordGrant:
secretRef:
name: admin-creds
namespace: auth-system
---
# Realm for each tenant (cluster-scoped)
apiVersion: keycloak.hostzero.com/v1beta1
kind: ClusterKeycloakRealm
metadata:
name: tenant-acme
spec:
clusterInstanceRef:
name: platform-keycloak
realmName: acme
definition:
enabled: true
displayName: ACME Corporation
---
# Clients can be in any namespace
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakClient
metadata:
name: acme-web-app
namespace: acme-apps
spec:
clusterRealmRef:
name: tenant-acme
clientId: acme-web-app
definition:
protocol: openid-connect
publicClient: true
redirectUris:
- https://app.acme.example.com/*
Environment-Specific Realms
# Development realm
apiVersion: keycloak.hostzero.com/v1beta1
kind: ClusterKeycloakRealm
metadata:
name: app-dev
spec:
clusterInstanceRef:
name: keycloak-dev
realmName: app-dev
definition:
enabled: true
registrationAllowed: true # Allow in dev
sslRequired: none # Relaxed for dev
---
# Production realm
apiVersion: keycloak.hostzero.com/v1beta1
kind: ClusterKeycloakRealm
metadata:
name: app-prod
spec:
clusterInstanceRef:
name: keycloak-prod
realmName: app-prod
definition:
enabled: true
registrationAllowed: false
sslRequired: external
bruteForceProtected: true
verifyEmail: true
Referencing from Namespaced Resources
Resources in any namespace can reference a ClusterKeycloakRealm:
# KeycloakClient in namespace-a
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakClient
metadata:
name: my-client
namespace: namespace-a
spec:
clusterRealmRef:
name: shared-realm # References ClusterKeycloakRealm
clientId: my-client
definition:
# ...
---
# KeycloakUser in namespace-b
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakUser
metadata:
name: my-user
namespace: namespace-b
spec:
clusterRealmRef:
name: shared-realm # Same ClusterKeycloakRealm
username: myuser
definition:
# ...
Comparison with KeycloakRealm
| Aspect | KeycloakRealm | ClusterKeycloakRealm |
|---|---|---|
| Scope | Namespaced | Cluster |
| Instance ref | Same namespace or cross-namespace | Cluster or any namespaced |
| Accessible from | Same namespace | Any namespace |
| Short name | kcrm | ckcrm |
| Use case | Single namespace | Multi-namespace/platform |
Notes
- Only one
ClusterKeycloakRealmwith a given name can exist - Deleting the realm will delete all Keycloak resources within it
- The referenced instance must be ready before the realm can be created
- Changes to the
definitionare applied on each reconciliation
KeycloakClient
Identifier field: Set the client ID in the
spec.clientIdfield. It is required and immutable once set. AclientIdinsidespec.definitionis tolerated only when it matchesspec.clientId; a conflicting value is rejected.
A KeycloakClient represents a client within a Keycloak realm. Both OpenID Connect and SAML clients are supported; the protocol is selected with definition.protocol and defaults to openid-connect.
Specification
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakClient
metadata:
name: my-app
spec:
# One of realmRef or clusterRealmRef must be specified
# Option 1: Reference to a namespaced KeycloakRealm
realmRef:
name: my-realm
# Option 2: Reference to a ClusterKeycloakRealm
# clusterRealmRef:
# name: my-cluster-realm
# Required: Client ID in Keycloak (do NOT set clientId in definition)
clientId: my-app
# Optional: Client definition (Keycloak ClientRepresentation)
definition:
name: My Application
enabled: true
publicClient: false
# ... any other Keycloak client properties
# Optional: Configure client secret handling
clientSecretRef:
name: my-app-credentials
# clientIdKey: client-id # Default: client-id
# clientSecretKey: client-secret # Default: client-secret
# create: true # Default: true
Status
status:
ready: true
status: "Ready"
clientUUID: "12345678-1234-1234-1234-123456789abc"
resourcePath: "/admin/realms/my-realm/clients/12345678-..."
message: "Client synchronized successfully"
instance:
instanceRef: my-keycloak
realm:
realmRef: my-realm
conditions:
- type: Ready
status: "True"
reason: Synchronized
Client Secret Handling
The clientSecretRef field controls how client secrets are managed:
| Field | Type | Default | Description |
|---|---|---|---|
name | string | (required) | Name of the Kubernetes Secret |
clientIdKey | string | client-id | Key for the client ID in the secret |
clientSecretKey | string | client-secret | Key for the client secret in the secret |
create | boolean | true | Whether to create the secret if it doesn’t exist |
Behavior
- If the secret exists: The operator reads the client secret from the specified key and configures Keycloak to use it.
- If the secret doesn’t exist and
create: true: The operator lets Keycloak auto-generate a secret and creates the Kubernetes Secret. - If the secret doesn’t exist and
create: false: The operator reports an error (strict mode for GitOps workflows). - Public clients (
publicClient: true): WhenclientSecretRefis set, the Secret is still materialized but only contains theclient-idkey — public OAuth clients have noclient_secretto store. This lets consumer charts pull the client ID viaenvFromorsecretKeyRefregardless of whether the client is public or confidential. IfclientSecretRefis not set, no Secret is created.
Use Cases
Auto-generate secret (default):
clientSecretRef:
name: my-app-credentials
# create: true (default)
Use pre-existing secret (GitOps/Sealed Secrets):
clientSecretRef:
name: my-sealed-secret
create: false
Custom key names:
clientSecretRef:
name: my-credentials
clientIdKey: OIDC_CLIENT_ID
clientSecretKey: OIDC_CLIENT_SECRET
Examples
Public Client (SPA)
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakClient
metadata:
name: my-spa
spec:
realmRef:
name: my-realm
clientId: my-spa
definition:
name: My Single Page Application
enabled: true
publicClient: true
standardFlowEnabled: true
directAccessGrantsEnabled: false
rootUrl: https://my-app.example.com
redirectUris:
- https://my-app.example.com/*
webOrigins:
- https://my-app.example.com
# Optional: still materialise a Secret so consumers can mount the
# client-id via envFrom. The Secret will only contain the client-id
# key — public clients have no client_secret.
clientSecretRef:
name: my-spa-credentials
Confidential Client (Backend)
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakClient
metadata:
name: my-api
spec:
realmRef:
name: my-realm
clientId: my-api
definition:
name: My Backend API
enabled: true
publicClient: false
serviceAccountsEnabled: true
standardFlowEnabled: false
directAccessGrantsEnabled: false
clientSecretRef:
name: my-api-credentials
Service Account with Roles
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakClient
metadata:
name: my-service
spec:
realmRef:
name: my-realm
clientId: my-service
definition:
name: My Service Account
enabled: true
publicClient: false
serviceAccountsEnabled: true
standardFlowEnabled: false
directAccessGrantsEnabled: false
authorizationServicesEnabled: true
clientSecretRef:
name: my-service-credentials
SAML Client
Set protocol: saml in the definition. SAML-specific settings are ordinary
attributes entries, exactly as in the Keycloak ClientRepresentation:
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakClient
metadata:
name: my-saml-app
spec:
realmRef:
name: my-realm
clientId: my-saml-app
definition:
name: My SAML Application
enabled: true
protocol: saml
redirectUris:
- "https://app.example.com/saml/*"
attributes:
saml_name_id_format: username
saml.assertion.signature: "true"
saml.client.signature: "false"
Keycloak generates signing material of its own (saml.signing.certificate,
saml.signing.private.key) and fills in defaults for attributes the definition
omits. The operator leaves those alone: it only compares the properties the
definition declares, and Keycloak merges attributes on update rather than
replacing them.
Claim mappers for a SAML client are KeycloakProtocolMapper
resources with protocol: saml.
Using Pre-existing Secret (Sealed Secrets / External Secrets)
# First, create or have your secret management tool create the secret:
apiVersion: v1
kind: Secret
metadata:
name: my-sealed-secret
type: Opaque
data:
client-id: bXktYXBw # base64 encoded
client-secret: c2VjcmV0... # base64 encoded
---
# Then reference it with create: false
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakClient
metadata:
name: my-app
spec:
realmRef:
name: my-realm
clientId: my-app
definition:
enabled: true
publicClient: false
clientSecretRef:
name: my-sealed-secret
create: false # Error if secret doesn't exist
Generated Secret Format
When the operator creates or manages a secret for a confidential client, it has this structure:
apiVersion: v1
kind: Secret
metadata:
name: my-app-credentials
ownerReferences:
- apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakClient
name: my-app
type: Opaque
data:
client-id: bXktYXBw # base64 encoded
client-secret: c2VjcmV0... # base64 encoded
For a public client (publicClient: true) the Secret is materialized with only the client-id key, since there is no OAuth client_secret:
apiVersion: v1
kind: Secret
metadata:
name: my-spa-credentials
ownerReferences:
- apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakClient
name: my-spa
type: Opaque
data:
client-id: bXktc3Bh # base64 encoded
Authentication Flow Binding Overrides
Keycloak allows overriding the default authentication flows (browser, direct grant) per client via authenticationFlowBindingOverrides. Normally this requires the internal UUID of the flow, which is generated dynamically and differs across environments – making it incompatible with GitOps.
The operator supports alias-based references so you can use the human-readable flow alias instead:
| Alias Key | Resolves To | Description |
|---|---|---|
browserFlowAlias | browser | Browser authentication flow |
directGrantFlowAlias | direct_grant | Direct grant (Resource Owner Password) flow |
The operator resolves aliases to UUIDs at reconciliation time. If both an alias key and the corresponding UUID key are present, the alias takes precedence.
Example: Using flow aliases
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakClient
metadata:
name: my-app
spec:
realmRef:
name: my-realm
clientId: my-app
definition:
enabled: true
publicClient: true
standardFlowEnabled: true
authenticationFlowBindingOverrides:
browserFlowAlias: "my-custom-browser-flow"
directGrantFlowAlias: "my-custom-direct-grant"
Example: Using UUIDs (unchanged, still supported)
authenticationFlowBindingOverrides:
browser: "a3f5c2d1-1234-5678-90ab-abcdef123456"
direct_grant: "b4e6d3a2-2345-6789-01bc-bcdef2345678"
If the specified alias does not match any authentication flow in the realm, the operator reports a FlowAliasResolutionFailed status with a descriptive error message.
Definition Properties
definition is the Keycloak ClientRepresentation,
passed through verbatim. Any property Keycloak accepts can be set here, not just
the ones listed below. Three keys are handled specially: clientId belongs in
spec.clientId, defaultClientScopes and optionalClientScopes are applied
through Keycloak’s scope-assignment endpoints instead of the client update, and
protocolMappers is rejected in favour of
KeycloakProtocolMapper.
Commonly used properties:
| Property | Type | Description |
|---|---|---|
name | string | Display name |
enabled | boolean | Whether client is enabled |
protocol | string | openid-connect (default) or saml |
publicClient | boolean | Public or confidential client |
standardFlowEnabled | boolean | Enable Authorization Code flow |
directAccessGrantsEnabled | boolean | Enable Resource Owner Password flow |
serviceAccountsEnabled | boolean | Enable service account |
redirectUris | string[] | Valid redirect URIs |
webOrigins | string[] | Allowed CORS origins |
rootUrl | string | Root URL for relative URIs |
attributes | map[string]string | Protocol-specific settings, including all saml.* options |
Short Names
| Alias | Full Name |
|---|---|
kcc | keycloakclients |
kubectl get kcc
KeycloakClientScope
Identifier field: Set the client scope name in the
spec.namefield. It is required and immutable once set. Anameinsidespec.definitionis tolerated only when it matchesspec.name; a conflicting value is rejected.
A KeycloakClientScope represents a client scope within a Keycloak realm.
Specification
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakClientScope
metadata:
name: my-scope
spec:
# One of realmRef or clusterRealmRef must be specified
# Option 1: Reference to a namespaced KeycloakRealm
realmRef:
name: my-realm
# Option 2: Reference to a ClusterKeycloakRealm
clusterRealmRef:
name: my-cluster-realm
# Required: Client scope definition
name: my-scope
definition:
protocol: openid-connect
# ... any other properties
Status
status:
ready: true
status: "Ready"
message: "Client scope synchronized successfully"
resourcePath: "/admin/realms/my-realm/client-scopes/12345678-..."
instance:
instanceRef: my-keycloak
realm:
realmRef: my-realm
conditions:
- type: Ready
status: "True"
reason: Synchronized
Example
Basic Scope
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakClientScope
metadata:
name: profile-extended
spec:
realmRef:
name: my-realm
name: profile-extended
definition:
description: Extended profile information
protocol: openid-connect
Scope with Protocol Mappers
Protocol mappers are declared as separate KeycloakProtocolMapper
resources. Keycloak ignores protocolMappers sent on the client scope update
endpoint, so the operator rejects the key inside definition rather than
accepting edits it cannot apply.
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakClientScope
metadata:
name: department-scope
spec:
realmRef:
name: my-realm
name: department
definition:
description: Department information
protocol: openid-connect
---
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakProtocolMapper
metadata:
name: department
spec:
clientScopeRef:
name: department-scope
name: department
definition:
protocol: openid-connect
protocolMapper: oidc-usermodel-attribute-mapper
consentRequired: false
config:
claim.name: department
user.attribute: department
jsonType.label: String
id.token.claim: "true"
access.token.claim: "true"
userinfo.token.claim: "true"
Definition Properties
| Property | Type | Description |
|---|---|---|
name | string | Scope name (required) |
description | string | Description |
protocol | string | Protocol (openid-connect, saml) |
attributes | map | Additional attributes |
protocolMappers is rejected here; use KeycloakProtocolMapper.
Short Names
| Alias | Full Name |
|---|---|
kccs | keycloakclientscopes |
kubectl get kccs
KeycloakProtocolMapper
Identifier field: Set the mapper name in the
spec.namefield. It is required and immutable once set. Anameinsidespec.definitionis tolerated only when it matchesspec.name; a conflicting value is rejected.
A KeycloakProtocolMapper defines how user attributes, roles, and other data are mapped into tokens. Protocol mappers can be attached to either clients or client scopes.
Specification
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakProtocolMapper
metadata:
name: my-mapper
spec:
# One of clientRef or clientScopeRef must be specified
clientRef:
name: my-client
# Or for client scopes:
# clientScopeRef:
# name: my-scope
# Optional: Secret whose keys are merged into definition.config
# configSecretRef:
# name: mapper-secrets
# Required: Mapper definition
name: department
definition:
protocol: openid-connect
protocolMapper: oidc-usermodel-attribute-mapper
config:
user.attribute: department
claim.name: department
Status
status:
ready: true
status: "Ready"
mapperID: "12345678-1234-1234-1234-123456789abc"
mapperName: "department"
parentType: "client"
parentID: "87654321-..."
message: "Protocol mapper synchronized successfully"
resourcePath: "/admin/realms/my-realm/clients/87654321-.../protocol-mappers/models/12345678-..."
instance:
instanceRef: my-keycloak
realm:
realmRef: my-realm
conditions:
- type: Ready
status: "True"
reason: Synchronized
Examples
Client Protocol Mapper
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakProtocolMapper
metadata:
name: department-mapper
namespace: keycloak
spec:
clientRef:
name: my-client
name: department
definition:
protocol: openid-connect
protocolMapper: oidc-usermodel-attribute-mapper
config:
user.attribute: department
claim.name: department
jsonType.label: String
id.token.claim: "true"
access.token.claim: "true"
userinfo.token.claim: "true"
Client Scope Protocol Mapper
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakProtocolMapper
metadata:
name: groups-mapper
namespace: keycloak
spec:
clientScopeRef:
name: my-scope
name: groups
definition:
protocol: openid-connect
protocolMapper: oidc-group-membership-mapper
config:
full.path: "false"
id.token.claim: "true"
access.token.claim: "true"
claim.name: groups
userinfo.token.claim: "true"
Parent Reference
A KeycloakProtocolMapper belongs to either a client or client scope:
| Reference | Use Case |
|---|---|
clientRef | Mapper applies to a specific client only |
clientScopeRef | Mapper applies to all clients using the scope |
Note: Exactly one of these must be specified.
Definition Properties
The definition field accepts any valid Keycloak ProtocolMapperRepresentation:
| Field | Type | Description |
|---|---|---|
name | string | Mapper name (required) |
protocol | string | Protocol (usually “openid-connect” or “saml”) |
protocolMapper | string | Mapper type (see common types below) |
consentRequired | boolean | Whether user consent is required |
config | object | Mapper-specific configuration |
Common Protocol Mapper Types
OpenID Connect
| Mapper Type | Description |
|---|---|
oidc-usermodel-attribute-mapper | Maps user attribute to token claim |
oidc-usermodel-property-mapper | Maps user property to token claim |
oidc-group-membership-mapper | Includes group membership in token |
oidc-role-name-mapper | Maps role names |
oidc-hardcoded-claim-mapper | Adds hardcoded claim |
oidc-audience-mapper | Adds audience to token |
oidc-full-name-mapper | Maps full name |
SAML
| Mapper Type | Description |
|---|---|
saml-user-attribute-mapper | Maps user attribute |
saml-group-membership-mapper | Maps group membership |
saml-role-list-mapper | Maps roles |
Short Names
| Alias | Full Name |
|---|---|
kcpm | keycloakprotocolmappers |
kubectl get kcpm
Notes
- Mapper names must be unique within the client or client scope
- The
configvalues are all strings (including boolean values like “true”/“false”) - Sensitive config values can come from a Secret via
configSecretRef(Secret references) - Changes to mappers affect all tokens issued after the change
KeycloakUser
Identifier field: Set the username in the
spec.usernamefield. Required for regular users and immutable once set; omit it for service account users, which are identified byclientRef. Ausernameinsidespec.definitionis tolerated only when it matchesspec.username; a conflicting value is rejected.
A KeycloakUser represents a user within a Keycloak realm, or a service account user associated with a client.
Specification
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakUser
metadata:
name: john-doe
spec:
# One of realmRef, clusterRealmRef, or clientRef must be specified
# Option 1: Reference to a KeycloakRealm (for regular realm users)
realmRef:
name: my-realm
# Option 2: Reference to a ClusterKeycloakRealm (for cluster-scoped realms)
# clusterRealmRef:
# name: my-cluster-realm
# Option 3: Reference to a KeycloakClient (for service account users)
# clientRef:
# name: my-client
# User definition (Keycloak UserRepresentation)
# Note: For service account users (clientRef), definition is optional
username: johndoe
definition:
email: john.doe@example.com
firstName: John
lastName: Doe
enabled: true
# ... any other Keycloak user properties
# (must not contain realmRoles, clientRoles, or groups — see below)
# Optional: role and group assignments (see "Roles and Groups")
realmRoles:
- offline_access
clientRoles:
my-app:
- admin
groups:
- developers
# Optional: Initial password (only set on creation)
initialPassword:
value: "temporary-password"
temporary: true # User must change on first login
To manage credentials in a Kubernetes Secret (including password generation), use KeycloakUserCredential.
Status
status:
ready: true
status: "Ready"
userID: "12345678-1234-1234-1234-123456789abc"
message: "User synchronized successfully"
resourcePath: "/admin/realms/my-realm/users/12345678-..."
isServiceAccount: false
instance:
instanceRef: my-keycloak
realm:
realmRef: my-realm
conditions:
- type: Ready
status: "True"
reason: Synchronized
Example
Basic User
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakUser
metadata:
name: admin-user
spec:
realmRef:
name: my-realm
username: admin
definition:
email: admin@example.com
firstName: Admin
lastName: User
enabled: true
emailVerified: true
User with Managed Credentials
Create the user, then attach a KeycloakUserCredential that generates a password, stores it in a Secret, and syncs it into Keycloak:
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakUser
metadata:
name: john-doe
spec:
realmRef:
name: my-realm
username: johndoe
definition:
email: john.doe@example.com
firstName: John
lastName: Doe
enabled: true
emailVerified: true
---
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakUserCredential
metadata:
name: john-doe-credential
spec:
userRef:
name: john-doe
userSecret:
secretName: john-doe-password
create: true # Generate a password and create the secret
See KeycloakUserCredential for existing secrets, key names, and password policy.
User with Attributes
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakUser
metadata:
name: employee
spec:
realmRef:
name: my-realm
username: jsmith
definition:
email: jsmith@company.com
firstName: Jane
lastName: Smith
enabled: true
attributes:
department:
- Engineering
employee_id:
- "12345"
manager:
- "jdoe"
User with Roles and Groups
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakUser
metadata:
name: developer
spec:
realmRef:
name: my-realm
username: developer1
definition:
email: dev@example.com
enabled: true
realmRoles:
- offline_access
clientRoles:
my-app:
- viewer
groups:
- developers
- team-alpha
Service Account User
Manage the service account user associated with a client. This is useful for assigning roles or attributes to a client’s service account.
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakUser
metadata:
name: my-service-account
spec:
# Use clientRef instead of realmRef for service account users
clientRef:
name: my-service-client
# Definition is optional - the service account is automatically created by Keycloak
# when serviceAccountsEnabled: true on the client
definition:
# You can add/modify attributes on the service account
attributes:
department:
- Platform
Service Account with Roles
The typed role fields also work for service account users:
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakUser
metadata:
name: my-service-sa
spec:
clientRef:
name: my-service-client
realmRoles:
- admin
clientRoles:
realm-management:
- manage-users
Alternatively, assign individual roles with KeycloakRoleMapping using serviceAccountRef — without an intermediate KeycloakUser (see KeycloakRoleMapping). Do not combine both mechanisms for the same user.
Roles and Groups
spec.realmRoles, spec.clientRoles, and spec.groups are reconciled via Keycloak’s dedicated role-mapping and group-membership endpoints. They must not appear inside spec.definition (Keycloak ignores them in the user representation anyway); the operator rejects such definitions.
Each field is authoritative when set:
- Omitted (
nil): the operator does not touch that category of assignments. - Set (even to an empty list): the operator reconciles Keycloak to exactly that set, removing anything else. For
clientRoles, roles on clients absent from the map are also removed.
Groups are matched by top-level group name. Roles and groups must already exist in the realm (e.g. via KeycloakRole or KeycloakGroup); unknown names are skipped with a log message.
Because these fields are authoritative, do not combine them with KeycloakRoleMapping resources targeting the same user — the user’s reconciler would remove the mappings again.
Definition Properties
Common properties from Keycloak UserRepresentation:
| Property | Type | Description |
|---|---|---|
username | string | Username (required) |
email | string | Email address |
firstName | string | First name |
lastName | string | Last name |
enabled | boolean | Whether user is enabled |
emailVerified | boolean | Email verified flag |
attributes | map | Custom user attributes |
requiredActions | string[] | Required actions on login |
Role and group assignments (realmRoles, clientRoles, groups) are typed spec fields, not definition properties.
Short Names
| Alias | Full Name |
|---|---|
kcu | keycloakusers |
kubectl get kcu
Parent Reference
A KeycloakUser can belong to one of three parent types:
| Reference | Use Case | Parent Type |
|---|---|---|
realmRef | Regular realm users | KeycloakRealm |
clusterRealmRef | Users in cluster-scoped realms | ClusterKeycloakRealm |
clientRef | Service account users | KeycloakClient |
Note: Exactly one of realmRef, clusterRealmRef, or clientRef must be specified.
Service Account Users
When using clientRef, the operator manages the service account user that Keycloak automatically creates for clients with serviceAccountsEnabled: true. This allows you to:
- Add custom attributes to the service account
- Use
KeycloakRoleMappingto assign roles to the service account - Manage the service account declaratively alongside other resources
The definition field is optional for service account users since Keycloak creates the user automatically.
Notes
spec.initialPasswordis only set on user creation- To manage or rotate a password declaratively, use KeycloakUserCredential
- For service account users, the username is automatically set by Keycloak (format:
service-account-<client-id>)
KeycloakUserCredential
The KeycloakUserCredential resource manages user credentials (passwords) in Keycloak via Kubernetes Secrets.
Overview
This CRD provides a way to:
- Store user passwords in Kubernetes Secrets
- Automatically create secrets with generated passwords
- Sync passwords to Keycloak users
- Manage password policies
Example
Using an existing Secret
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakUserCredential
metadata:
name: user-credential
spec:
userRef:
name: my-user
userSecret:
secretName: my-user-credentials
usernameKey: username
passwordKey: password
Auto-creating a Secret
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakUserCredential
metadata:
name: user-credential
spec:
userRef:
name: my-user
userSecret:
secretName: my-user-credentials
create: true
usernameKey: username
passwordKey: password
passwordPolicy:
length: 24
includeSymbols: true
includeNumbers: true
Spec
| Field | Type | Description | Required |
|---|---|---|---|
userRef | ResourceRef | Reference to the KeycloakUser resource | Yes |
userSecret.secretName | string | Name of the Kubernetes Secret | Yes |
userSecret.create | boolean | Create secret if it doesn’t exist | No (default: false) |
userSecret.usernameKey | string | Key in secret for username | No (default: “username”) |
userSecret.passwordKey | string | Key in secret for password | No (default: “password”) |
userSecret.emailKey | string | Key in secret for email | No |
userSecret.passwordPolicy.length | int | Length of generated password | No (default: 24) |
userSecret.passwordPolicy.includeNumbers | boolean | Include numbers in password | No (default: true) |
userSecret.passwordPolicy.includeSymbols | boolean | Include symbols in password | No (default: true) |
Status
| Field | Type | Description |
|---|---|---|
ready | boolean | Whether the credential is synced |
status | string | Current status (Synced, Error, SecretError) |
message | string | Additional status information |
resourcePath | string | Keycloak API path for the user |
secretCreated | boolean | Whether the secret was created by the operator |
passwordHash | string | Hash of the last synchronized password |
secretResourceVersion | string | Resource version of the secret when last synced |
instance | object | Resolved instance reference |
realm | object | Resolved realm reference |
observedGeneration | integer | Last observed generation |
conditions | []Condition | Kubernetes conditions |
Behavior
Secret Creation
When create: true is set:
- The operator creates a new Secret if it doesn’t exist
- A password is generated according to the password policy
- The username is set to match the Keycloak user’s username
Password Sync
When the Secret exists (created or pre-existing):
- The operator reads the password from the Secret
- The password is set in Keycloak for the referenced user
- The
passwordHashis updated for change detection
Cleanup
When the KeycloakUserCredential is deleted:
- If
secretCreated: truein status, the Secret is also deleted (via owner references) - Pre-existing secrets are not deleted
Use Cases
Initial User Setup
Create users with auto-generated passwords:
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakUser
metadata:
name: new-user
spec:
realmRef:
name: my-realm
username: new-user
definition:
email: user@example.com
enabled: true
---
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakUserCredential
metadata:
name: new-user-creds
spec:
userRef:
name: new-user
userSecret:
secretName: new-user-password
create: true
Service Account Passwords
Manage service account credentials that can be mounted into pods:
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakUserCredential
metadata:
name: service-account-creds
spec:
userRef:
name: service-account-user
userSecret:
secretName: app-keycloak-credentials
create: true
passwordPolicy:
length: 32
includeSymbols: false
KeycloakGroup
Identifier field: Set the group name in the
spec.namefield. It is required and immutable once set. Anameinsidespec.definitionis tolerated only when it matchesspec.name; a conflicting value is rejected.
A KeycloakGroup represents a group within a Keycloak realm.
Specification
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakGroup
metadata:
name: my-group
spec:
# Exactly one of realmRef, clusterRealmRef, or parentGroupRef must be specified
# Option 1: Reference to a namespaced KeycloakRealm (top-level group)
realmRef:
name: my-realm
# Option 2: Reference to a ClusterKeycloakRealm (top-level group)
clusterRealmRef:
name: my-cluster-realm
# Option 3: Reference to a parent group (nested group; the realm comes from
# the parent chain, so do not also set realmRef)
parentGroupRef:
name: parent-group
# Required: Group definition
name: my-group
definition:
# ... any other properties
Status
status:
ready: true
status: "Ready"
groupID: "12345678-1234-1234-1234-123456789abc"
message: "Group synchronized successfully"
resourcePath: "/admin/realms/my-realm/groups/12345678-..."
instance:
instanceRef: my-keycloak
realm:
realmRef: my-realm
conditions:
- type: Ready
status: "True"
reason: Synchronized
Example
Basic Group
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakGroup
metadata:
name: developers
spec:
realmRef:
name: my-realm
name: developers
definition: {}
Group with Attributes
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakGroup
metadata:
name: engineering
spec:
realmRef:
name: my-realm
name: engineering
definition:
attributes:
department:
- Engineering
cost_center:
- "1234"
Nested Group
First, create the parent group:
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakGroup
metadata:
name: organization
spec:
realmRef:
name: my-realm
name: organization
definition: {}
Then create child groups. A nested group names only its parent; the realm is inherited from the parent chain:
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakGroup
metadata:
name: team-alpha
spec:
parentGroupRef:
name: organization
name: team-alpha
definition: {}
Groups may nest arbitrarily deep. Each level names only its immediate parent, and the realm is resolved by following the chain up to the top-level group.
Parent Reference
A KeycloakGroup belongs to one of three parents:
| Reference | Scope | Use Case |
|---|---|---|
realmRef | Top-level group | Group directly under a realm |
clusterRealmRef | Top-level group | For cluster-scoped realms |
parentGroupRef | Nested group | Subgroup of another KeycloakGroup |
Note: Exactly one of these must be specified; setting more than one is rejected.
For a nested group, use parentGroupRef alone. The realm is inherited from the
parent chain, so realmRef and clusterRealmRef must not be combined with it.
Definition Properties
| Property | Type | Description |
|---|---|---|
name | string | Group name (required) |
path | string | Full group path (auto-generated) |
attributes | map | Custom group attributes |
realmRoles | string[] | Realm roles assigned to group |
clientRoles | map | Client roles assigned to group |
Short Names
| Alias | Full Name |
|---|---|
kcg | keycloakgroups |
kubectl get kcg
KeycloakRole
Identifier field: Set the role name in the
spec.namefield. It is required and immutable once set. Anameinsidespec.definitionis tolerated only when it matchesspec.name; a conflicting value is rejected.
A KeycloakRole manages Keycloak roles. Roles can be either realm-level (shared across all clients) or client-level (specific to a single client).
Specification
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRole
metadata:
name: my-role
spec:
# Exactly one of realmRef, clusterRealmRef, or clientRef must be specified
# For realm roles:
realmRef:
name: my-realm
# For client roles (the realm comes from the client; do not also set realmRef):
# clientRef:
# name: my-client
# Required: Role definition (Keycloak RoleRepresentation)
name: admin-role
definition:
description: Administrator role
Status
status:
ready: true
status: "Ready"
roleName: "admin-role"
roleID: "12345678-1234-1234-1234-123456789abc"
isClientRole: false
message: "Role synchronized successfully"
resourcePath: "/admin/realms/my-realm/roles/admin-role"
instance:
instanceRef: my-keycloak
realm:
realmRef: my-realm
conditions:
- type: Ready
status: "True"
reason: Synchronized
Examples
Realm Role
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRole
metadata:
name: my-realm-role
namespace: keycloak
spec:
realmRef:
name: my-realm
name: admin-role
definition:
description: Administrator role with full access
composite: false
Client Role
The realm is derived from the referenced client, so no realmRef is given:
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRole
metadata:
name: my-client-role
namespace: keycloak
spec:
clientRef:
name: my-client
name: editor
definition:
description: Can edit resources
Parent Reference
A KeycloakRole can belong to one of three parent types:
| Reference | Scope | Use Case |
|---|---|---|
realmRef | Realm role | Shared across all clients in the realm |
clusterRealmRef | Realm role | For cluster-scoped realms |
clientRef | Client role | Specific to a single client |
Note: Exactly one of these must be specified; setting more than one is rejected.
For a client role, use clientRef alone. The realm is taken from the referenced
client, which already belongs to exactly one realm, so realmRef and
clusterRealmRef must not be combined with clientRef.
Definition Properties
The definition field accepts any valid Keycloak RoleRepresentation:
| Field | Type | Description |
|---|---|---|
name | string | Role name (required) |
description | string | Role description |
composite | boolean | Whether this is a composite role |
clientRole | boolean | Whether this is a client role |
containerId | string | Container ID (realm or client ID) |
attributes | object | Custom attributes |
Status Fields
| Field | Type | Description |
|---|---|---|
ready | boolean | Whether the role is synchronized |
status | string | Current status (e.g., “Ready”, “Error”) |
message | string | Human-readable status message |
resourcePath | string | Keycloak API path for this role |
roleID | string | Keycloak internal role ID |
roleName | string | The role name in Keycloak |
isClientRole | boolean | Whether this is a client role |
clientID | string | Client ID (for client roles) |
instance | object | Resolved instance reference |
realm | object | Resolved realm reference |
observedGeneration | integer | Last observed generation |
conditions | []Condition | Kubernetes conditions |
Short Names
| Alias | Full Name |
|---|---|
kcr | keycloakroles |
kubectl get kcr
Notes
- Role names must be unique within their scope (realm or client)
- When using
clientRef, the role becomes a client role - Composite roles can reference other realm or client roles
KeycloakRoleMapping
The KeycloakRoleMapping resource assigns Keycloak roles to users, groups, or client service accounts.
Overview
This CRD provides a declarative way to:
- Assign realm roles to users
- Assign client roles to users
- Assign realm roles to groups
- Assign client roles to groups
- Assign roles to a client’s service account (via
serviceAccountRef)
Examples
Realm Role to User
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRoleMapping
metadata:
name: admin-role-mapping
spec:
subject:
userRef:
name: admin-user
roleRef:
name: admin-role
Client Role to User (using roleRef)
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRoleMapping
metadata:
name: client-admin-mapping
spec:
subject:
userRef:
name: service-user
roleRef:
name: manage-clients
Inline Client Role to User
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRoleMapping
metadata:
name: inline-client-role-mapping
spec:
subject:
userRef:
name: service-user
role:
name: manage-clients
clientRef:
name: my-client
Inline Role Reference
Instead of referencing a KeycloakRole resource, you can specify the role name directly:
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRoleMapping
metadata:
name: builtin-role-mapping
spec:
subject:
userRef:
name: my-user
role:
name: offline_access
Role to Group
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRoleMapping
metadata:
name: group-role-mapping
spec:
subject:
groupRef:
name: developers
roleRef:
name: developer-role
Role to a Client’s Service Account
serviceAccountRef assigns roles to the service account user that Keycloak auto-creates for clients with serviceAccountsEnabled: true, without an intermediate KeycloakUser resource:
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRoleMapping
metadata:
name: my-app-sa-manage-users
spec:
subject:
serviceAccountRef:
name: my-app # KeycloakClient
role:
name: manage-users
clientRef:
name: realm-management
Spec
| Field | Type | Description | Required |
|---|---|---|---|
subject.userRef | ResourceRef | Reference to KeycloakUser | Exactly one subject ref |
subject.groupRef | ResourceRef | Reference to KeycloakGroup | Exactly one subject ref |
subject.serviceAccountRef | ResourceRef | Reference to a KeycloakClient whose service account is the subject | Exactly one subject ref |
roleRef | ResourceRef | Reference to KeycloakRole resource | Either roleRef or role |
role.name | string | Keycloak role name (inline) | Either roleRef or role |
role.clientRef | ResourceRef | Reference to KeycloakClient for client roles (within inline role) | No (realm role if omitted) |
role.clientId | string | Client ID for client roles (alternative to clientRef) | No |
Status
| Field | Type | Description |
|---|---|---|
ready | boolean | Whether the mapping is synced |
status | string | Current status (Synced, Error, SubjectError, RoleError) |
message | string | Additional status information |
resourcePath | string | Keycloak API path for this role mapping |
subjectType | string | Subject type (“user” or “group”) |
subjectID | string | Keycloak ID of the user/group |
roleName | string | Resolved role name |
roleType | string | Role type (“realm” or “client”) |
instance | object | Resolved instance reference |
realm | object | Resolved realm reference |
observedGeneration | integer | Last observed generation |
conditions | []Condition | Kubernetes conditions |
Behavior
Role Resolution
Using roleRef:
- The operator looks up the referenced
KeycloakRoleresource - It reads
status.roleNamefrom that resource (which may differ from the CR name) - If the referenced
KeycloakRolehas its ownspec.clientRef, the mapping is automatically scoped to that client and the client’s UUID is resolved from theKeycloakClient’s status - The referenced role (and the client it points at, if any) must be Ready; otherwise the mapping requeues
- This is the recommended approach for roles managed by the operator
Using role.name:
- The operator queries Keycloak for a role with the given name
- This is useful for built-in roles like
offline_access
Mapping Types
| Subject | Role source | Result |
|---|---|---|
| userRef | inline role without clientRef/clientId | User realm role mapping |
| userRef | inline role with clientRef or clientId | User client role mapping |
| userRef | roleRef to a KeycloakRole without clientRef | User realm role mapping |
| userRef | roleRef to a KeycloakRole with clientRef | User client role mapping |
| groupRef | inline role without clientRef/clientId | Group realm role mapping |
| groupRef | inline role with clientRef or clientId | Group client role mapping |
| groupRef | roleRef to a KeycloakRole without clientRef | Group realm role mapping |
| groupRef | roleRef to a KeycloakRole with clientRef | Group client role mapping |
| serviceAccountRef | any role source | Role mapping on the client’s service account user |
Cleanup
When the KeycloakRoleMapping is deleted:
- The finalizer removes the role mapping from Keycloak
- The user/group no longer has the role assigned
Use Cases
RBAC Setup
Set up role-based access control with groups:
# Create a group
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakGroup
metadata:
name: admins
spec:
realmRef:
name: my-realm
name: admins
definition: {}
---
# Create a role
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRole
metadata:
name: admin-role
spec:
realmRef:
name: my-realm
name: admin
definition:
description: Full admin access
---
# Map role to group
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRoleMapping
metadata:
name: admins-admin-role
spec:
subject:
groupRef:
name: admins
roleRef:
name: admin-role
Service Account Roles
Assign specific client roles to service accounts:
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRoleMapping
metadata:
name: service-manage-users
spec:
subject:
userRef:
name: service-account
role:
name: manage-users
clientRef:
name: realm-management
Multiple Role Assignments
Assign multiple roles to the same user:
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRoleMapping
metadata:
name: user-role-1
spec:
subject:
userRef:
name: my-user
roleRef:
name: role-1
---
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRoleMapping
metadata:
name: user-role-2
spec:
subject:
userRef:
name: my-user
roleRef:
name: role-2
Notes
- Exactly one of
userRef,groupRef, orserviceAccountRefcan be specified - Only one of
roleReforrolecan be specified - When using
role.clientRef, the role must be a client role, not a realm role - Built-in Keycloak roles (like
offline_access,uma_authorization) should use inlinerole.name - Do not combine
KeycloakRoleMappingwith the authoritativespec.realmRoles/spec.clientRolesfields of aKeycloakUsertargeting the same user — the user’s reconciler would remove the mappings again
KeycloakComponent
Identifier field: Set the component name in the
spec.namefield. It is required and immutable once set. Anameinsidespec.definitionis tolerated only when it matchesspec.name; a conflicting value is rejected.
A KeycloakComponent manages Keycloak components such as LDAP user federation, custom storage providers, key providers, and other pluggable realm components.
Specification
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakComponent
metadata:
name: my-component
spec:
# One of realmRef or clusterRealmRef must be specified
# Option 1: Reference to a namespaced KeycloakRealm
realmRef:
name: my-realm
# Option 2: Reference to a ClusterKeycloakRealm
# clusterRealmRef:
# name: my-cluster-realm
# Optional: Secret whose keys are merged into definition.config
# configSecretRef:
# name: ldap-credentials
# Required: Component definition
name: corporate-ldap
definition:
providerId: ldap
providerType: org.keycloak.storage.UserStorageProvider
config:
enabled:
- "true"
connectionUrl:
- "ldap://ldap.example.com:389"
Status
status:
ready: true
status: "Ready"
componentID: "12345678-1234-1234-1234-123456789abc"
componentName: "corporate-ldap"
providerType: "org.keycloak.storage.UserStorageProvider"
message: "Component synchronized successfully"
resourcePath: "/admin/realms/my-realm/components/12345678-..."
instance:
instanceRef: my-keycloak
realm:
realmRef: my-realm
conditions:
- type: Ready
status: "True"
reason: Synchronized
Examples
LDAP User Federation
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakComponent
metadata:
name: ldap-federation
namespace: keycloak
spec:
realmRef:
name: my-realm
name: corporate-ldap
configSecretRef:
name: ldap-credentials
definition:
providerId: ldap
providerType: org.keycloak.storage.UserStorageProvider
config:
enabled:
- "true"
vendor:
- "ad"
connectionUrl:
- "ldap://ldap.example.com:389"
bindDn:
- "cn=admin,dc=example,dc=com"
usersDn:
- "ou=users,dc=example,dc=com"
userObjectClasses:
- "person, organizationalPerson, user"
editMode:
- "READ_ONLY"
The Secret key bindCredential is merged into config as ["…"]. See Secret references.
RSA Key Provider
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakComponent
metadata:
name: rsa-key
namespace: keycloak
spec:
realmRef:
name: my-realm
name: rsa-generated
definition:
providerId: rsa-generated
providerType: org.keycloak.keys.KeyProvider
config:
priority:
- "100"
algorithm:
- "RS256"
Definition Properties
The definition field accepts any valid Keycloak ComponentRepresentation:
| Field | Type | Description |
|---|---|---|
name | string | Component name (required) |
providerId | string | Provider ID (e.g., “ldap”, “rsa-generated”) |
providerType | string | Provider type (e.g., “org.keycloak.storage.UserStorageProvider”) |
parentId | string | Parent component ID (defaults to realm ID) |
subType | string | Optional component subtype |
config | object | Provider-specific configuration (array of strings per key) |
Common Provider Types
| Provider Type | Use Case |
|---|---|
org.keycloak.storage.UserStorageProvider | LDAP, custom user storage |
org.keycloak.keys.KeyProvider | Cryptographic keys (RSA, AES, etc.) |
org.keycloak.storage.ldap.mappers.LDAPStorageMapper | LDAP attribute mappers |
Short Names
| Alias | Full Name |
|---|---|
kcco | keycloakcomponents |
kubectl get kcco
Notes
- Component configuration uses arrays of strings for all values
- Put secrets such as
bindCredentialin a Secret and setconfigSecretRef(Secret references) - Some components may require specific ordering via
priorityconfig
KeycloakIdentityProvider
Identifier field: Set the alias in the
spec.aliasfield. It is required and immutable once set. Analiasinsidespec.definitionis tolerated only when it matchesspec.alias; a conflicting value is rejected.
A KeycloakIdentityProvider represents an external identity provider configuration within a Keycloak realm.
Specification
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakIdentityProvider
metadata:
name: my-idp
spec:
# One of realmRef or clusterRealmRef must be specified
# Option 1: Reference to a namespaced KeycloakRealm
realmRef:
name: my-realm
# Option 2: Reference to a ClusterKeycloakRealm
clusterRealmRef:
name: my-cluster-realm
# Optional: Reference to a Secret with config values (e.g. clientId, clientSecret)
configSecretRef:
name: my-idp-credentials
# Optional: Link this IdP exclusively to a KeycloakOrganization (Keycloak 26+)
organizationRef:
name: acme-corp
# Required: Identity provider definition
alias: my-idp
definition:
providerId: oidc
enabled: true
# ... any other properties
Status
status:
ready: true
status: "Ready"
message: "Identity provider synchronized successfully"
resourcePath: "/admin/realms/my-realm/identity-provider/instances/my-idp"
organizationID: "12345678-1234-1234-1234-123456789abc"
instance:
instanceRef: my-keycloak
realm:
realmRef: my-realm
conditions:
- type: Ready
status: "True"
reason: Synchronized
Example
OIDC Provider
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakIdentityProvider
metadata:
name: corporate-sso
spec:
realmRef:
name: my-realm
configSecretRef:
name: corporate-sso-credentials
alias: corporate-sso
definition:
displayName: Corporate SSO
providerId: oidc
enabled: true
trustEmail: true
firstBrokerLoginFlowAlias: first broker login
config:
authorizationUrl: https://sso.corp.example.com/auth
tokenUrl: https://sso.corp.example.com/token
userInfoUrl: https://sso.corp.example.com/userinfo
defaultScope: openid profile email
syncMode: IMPORT
Google Provider
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakIdentityProvider
metadata:
name: google
spec:
realmRef:
name: my-realm
configSecretRef:
name: google-idp-credentials
alias: google
definition:
displayName: Sign in with Google
providerId: google
enabled: true
trustEmail: true
config:
defaultScope: openid profile email
GitHub Provider
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakIdentityProvider
metadata:
name: github
spec:
realmRef:
name: my-realm
configSecretRef:
name: github-idp-credentials
alias: github
definition:
displayName: Sign in with GitHub
providerId: github
enabled: true
SAML Provider
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakIdentityProvider
metadata:
name: saml-idp
spec:
realmRef:
name: my-realm
alias: saml-idp
definition:
displayName: Corporate SAML
providerId: saml
enabled: true
config:
entityId: https://idp.example.com
singleSignOnServiceUrl: https://idp.example.com/sso
nameIDPolicyFormat: urn:oasis:names:tc:SAML:2.0:nameid-format:transient
signatureAlgorithm: RSA_SHA256
wantAssertionsSigned: "true"
wantAuthnRequestsSigned: "true"
Organization Link
To map an identity provider exclusively to an organization, set organizationRef to a KeycloakOrganization in the same namespace and realm. The operator waits until the organization is ready, then injects its status.organizationID as organizationId on the identity provider.
This requires Keycloak 26.0.0 or later. Do not set organizationId inside definition — that field is rejected with Ready=False and reason InvalidDefinition. Organizations created outside the operator can be adopted with a KeycloakOrganization CR, then referenced here.
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakIdentityProvider
metadata:
name: acme-sso
spec:
realmRef:
name: my-realm
organizationRef:
name: acme-corp
alias: acme-sso
definition:
providerId: oidc
enabled: true
config:
authorizationUrl: https://sso.acme.example.com/auth
tokenUrl: https://sso.acme.example.com/token
The organization and identity provider must reference the same realm CR (realmRef or clusterRealmRef). A mismatch sets Ready=False with reason OrganizationRealmMismatch. If the organization is missing or not yet ready, the identity provider soft-waits with reason OrganizationNotReady.
Config from Secret
To avoid storing sensitive configuration values (such as clientId and clientSecret) in plaintext in the CR, use configSecretRef to reference a Kubernetes Secret:
kubectl create secret generic corporate-sso-credentials \
--from-literal=clientId=my-oidc-client-id \
--from-literal=clientSecret=my-oidc-client-secret
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakIdentityProvider
metadata:
name: corporate-sso
spec:
realmRef:
name: my-realm
configSecretRef:
name: corporate-sso-credentials
alias: corporate-sso
definition:
providerId: oidc
enabled: true
config:
authorizationUrl: https://sso.corp.example.com/auth
tokenUrl: https://sso.corp.example.com/token
defaultScope: openid profile email
Every key-value pair in the referenced Secret is merged into definition.config before the identity provider is synced to Keycloak. Secret values take precedence over values defined inline in definition.config.
The Secret must exist in the same namespace as the KeycloakIdentityProvider. When the Secret changes, the operator automatically re-reconciles the identity provider to pick up the new values.
The same configSecretRef field is available on other CRs that have definition.config. See Secret references.
Token Exchange Permission
When this identity provider should also act as a Trusted Token Issuer — i.e. clients in the realm exchange a JWT from the upstream IdP for a Keycloak token using RFC 8693 Token Exchange with subject_issuer=<alias> — Keycloak needs a fine-grained-authz policy listing which clients are allowed to do so. Without that policy, any client in the realm could perform the exchange.
spec.tokenExchange lets the operator manage that policy declaratively. Omit the field to leave Keycloak permissions untouched (default — fully opt-in). Set allowedClients to a list of clientIds in the same realm:
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakIdentityProvider
metadata:
name: external-issuer
spec:
realmRef:
name: my-realm
alias: external-issuer
definition:
providerId: oidc
enabled: true
config:
issuer: https://issuer.example.com
jwksUrl: https://issuer.example.com/.well-known/jwks.json
useJwksUrl: "true"
validateSignature: "true"
hideOnLoginPage: "true"
tokenExchange:
allowedClients:
- my-backend-client
- my-app-client
On reconcile, the operator:
- Enables fine-grained-authz on the identity provider (PUT
/identity-provider/instances/{alias}/management/permissions). Keycloak auto-creates atoken-exchangescope-permission on therealm-managementauthz resource server. - Resolves each
allowedClientsentry to its Keycloak client UUID. - Ensures a Client-type authz policy named
hostzero-idp-<alias>-token-exchangeexists inrealm-management’s authz resource server, withclientsset to the resolved UUIDs. - Binds that policy to the auto-created scope-permission. The operator owns the policy list — any other policy bound manually will be replaced.
Any client not on allowedClients attempting subject_issuer=<alias> is rejected with 403 not_allowed. An empty allowedClients: [] is valid and means “deny all”.
Soft wait on referenced clients
On first apply, the identity provider and the clients it references are often siblings in the same wave. If the operator reconciles the identity provider before a referenced client exists in Keycloak, the token-exchange reconcile defers without flipping status.ready to false. status.tokenExchange.message carries the reason; the next reconcile picks up the client once it lands.
Status
status:
ready: true
tokenExchange:
enabled: true
permissionID: <UUID of the token-exchange scope-permission>
policyID: <UUID of the operator-managed policy>
policyName: hostzero-idp-<alias>-token-exchange
enabled: false with a non-empty message indicates the operator is still waiting on referenced state.
Cleanup
Deleting the identity provider also removes the operator-managed policy from realm-management’s authz resource server. The scope-permission itself is removed by Keycloak when management-permissions are toggled off (implicit on IdP delete).
Definition Properties
Common properties from Keycloak IdentityProviderRepresentation:
| Property | Type | Description |
|---|---|---|
alias | string | Unique alias (required) |
displayName | string | Display name |
providerId | string | Provider type (oidc, saml, google, etc.) |
enabled | boolean | Whether provider is enabled |
trustEmail | boolean | Trust email from provider |
storeToken | boolean | Store provider tokens |
config | map | Provider-specific configuration |
Short Names
| Alias | Full Name |
|---|---|
kcidp | keycloakidentityproviders |
kubectl get kcidp
Notes
- Use
configSecretRefto store sensitive values likeclientIdandclientSecretin a Kubernetes Secret (see Config from Secret and Secret references) - Use
organizationRefto bind the identity provider to a KeycloakOrganization (Keycloak 26+). Do not setorganizationIdindefinition. - Consider using
syncMode: IMPORTto import users on first login - Mappers must be managed via KeycloakIdentityProviderMapper. Embedding
mappersinside this CR’sdefinitionis silently ignored by Keycloak on update — the field is only consumed during realm import.
KeycloakIdentityProviderMapper
Identifier field: Set the mapper name in the
spec.namefield. It is required and immutable once set. Anameinsidespec.definitionis tolerated only when it matchesspec.name; a conflicting value is rejected.
A KeycloakIdentityProviderMapper declaratively manages a mapper attached to a KeycloakIdentityProvider. Identity provider mappers transform claims, attributes, or roles produced by an external identity provider as users authenticate through it.
This CRD exists because Keycloak’s PUT /admin/realms/{realm} endpoint silently ignores identityProviderMappers (mappers can only be imported with realm creation), and the IdentityProviderRepresentation itself has no mappers field. The dedicated mapper sub-resource at /admin/realms/{realm}/identity-provider/instances/{alias}/mappers is the only API path that allows updating mappers on existing realms (such as the master realm).
Specification
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakIdentityProviderMapper
metadata:
name: my-mapper
spec:
# Required: reference to the parent KeycloakIdentityProvider CR.
# The realm and Keycloak instance are derived from this IdP.
identityProviderRef:
name: oidc
# Optional: Secret whose keys are merged into definition.config
# configSecretRef:
# name: mapper-secrets
# Required: mapper definition (Keycloak IdentityProviderMapperRepresentation).
# `identityProviderAlias` is auto-injected from the parent IdP and does not
# need to be set here.
name: my-mapper
definition:
identityProviderMapper: oidc-role-idp-mapper
config:
syncMode: FORCE
claim: roles
claim.value: my-group
role: my-realm-role
Status
status:
ready: true
status: "Ready"
mapperID: "12345678-1234-1234-1234-123456789abc"
mapperName: "my-mapper"
identityProviderAlias: "oidc"
resourcePath: "/admin/realms/my-realm/identity-provider/instances/oidc/mappers/12345678-..."
message: "Identity provider mapper synchronized"
conditions:
- type: Ready
status: "True"
reason: Ready
Examples
OIDC role mapper
Maps an roles claim value of mdmsupport (delivered by the upstream IdP) to a Keycloak realm role mdm-realm.mdm-support:
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakIdentityProviderMapper
metadata:
name: mdm-support-role-mapper
namespace: keycloak
spec:
identityProviderRef:
name: oidc
name: mdm-support-role-mapper
definition:
identityProviderMapper: oidc-role-idp-mapper
config:
syncMode: FORCE
claim: roles
claim.value: mdmsupport
role: mdm-realm.mdm-support
Hardcoded attribute
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakIdentityProviderMapper
metadata:
name: oidc-source-attribute
namespace: keycloak
spec:
identityProviderRef:
name: oidc
name: source-attribute
definition:
identityProviderMapper: hardcoded-attribute-idp-mapper
config:
syncMode: INHERIT
attribute: source
attribute.value: oidc
Parent Reference
| Field | Description |
|---|---|
identityProviderRef.name | Name of the parent KeycloakIdentityProvider CR (required) |
The mapper inherits its realm and Keycloak instance from the referenced KeycloakIdentityProvider. The mapper’s reconciler waits for the parent IdP to reach Ready before creating the mapper, and is automatically requeued when the parent transitions to Ready.
Definition Properties
The definition field accepts any valid Keycloak IdentityProviderMapperRepresentation:
| Field | Type | Description |
|---|---|---|
name | string | Mapper name; set via spec.name (required, not in definition) |
identityProviderMapper | string | Mapper type (see below) |
identityProviderAlias | string | Auto-injected from the parent IdP; setting it manually is overridden |
config | object | Mapper-specific configuration (all values are strings) |
Common Identity Provider Mapper Types
| Mapper Type | Description |
|---|---|
oidc-role-idp-mapper | Grants a Keycloak role when a claim has a specific value |
oidc-username-idp-mapper | Sets the Keycloak username from a claim |
oidc-user-attribute-idp-mapper | Maps a claim to a user attribute |
oidc-advanced-role-idp-mapper | Advanced role mapping with claim conditions |
hardcoded-role-idp-mapper | Always grants a role |
hardcoded-attribute-idp-mapper | Always sets a user attribute |
oidc-hardcoded-user-session-attribute-idp-mapper | Adds a session note |
saml-role-idp-mapper | SAML equivalent of role mapping |
saml-user-attribute-idp-mapper | SAML attribute → user attribute |
Short Names
| Alias | Full Name |
|---|---|
kcidpm | keycloakidentityprovidermappers |
kubectl get kcidpm
Notes
- Mapper names must be unique within an identity provider.
- All
configvalues are strings (including boolean values like"true"/"false"). - Sensitive config values can come from a Secret via
configSecretRef(Secret references). - The
syncModeconfig key controls when the mapper runs:IMPORT(only on first login),FORCE(every login), orINHERIT(use the IdP’s own setting). - Mappers embedded in the
definitionofKeycloakRealmorKeycloakIdentityProviderare silently dropped by Keycloak on update — always use this CRD to declaratively manage mappers on existing realms. - Setting the
keycloak.hostzero.com/preserve-resource: "true"annotation prevents the operator from deleting the mapper in Keycloak when the CR is removed.
KeycloakOrganization
Identifier field: Set the organization name in the
spec.namefield. It is required and immutable once set. Anameinsidespec.definitionis tolerated only when it matchesspec.name; a conflicting value is rejected.
A KeycloakOrganization represents an organization within a Keycloak realm.
Note: Organizations require Keycloak 26.0.0 or later. Attempting to use this resource with earlier Keycloak versions will result in an error.
Specification
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakOrganization
metadata:
name: acme-corp
spec:
# One of realmRef or clusterRealmRef must be specified
realmRef:
name: my-realm
# Required: Organization definition (Keycloak OrganizationRepresentation)
name: ACME Corporation
definition:
alias: acme
description: ACME Corp organization
enabled: true
domains:
- name: acme.com
verified: true
attributes:
industry:
- Technology
Status
status:
ready: true
status: "Ready"
organizationID: "12345678-1234-1234-1234-123456789abc"
message: "Organization synchronized successfully"
resourcePath: "/admin/realms/my-realm/organizations/12345678-..."
instance:
instanceRef: my-keycloak
realm:
realmRef: my-realm
conditions:
- type: Ready
status: "True"
reason: Synchronized
Examples
Basic Organization
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakOrganization
metadata:
name: my-org
spec:
realmRef:
name: my-realm
name: My Organization
definition:
enabled: true
Organization with Domains
Organizations can be associated with email domains. Users with matching email domains can be automatically associated with the organization.
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakOrganization
metadata:
name: example-org
spec:
realmRef:
name: my-realm
name: Example Organization
definition:
alias: example
description: An example organization with verified domains
enabled: true
domains:
- name: example.com
verified: true
- name: example.org
verified: false
Organization with Custom Attributes
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakOrganization
metadata:
name: enterprise-org
spec:
realmRef:
name: my-realm
name: Enterprise Organization
definition:
alias: enterprise
enabled: true
attributes:
tier:
- enterprise
maxUsers:
- "1000"
supportLevel:
- premium
Organization with Cluster-Scoped Realm
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakOrganization
metadata:
name: global-org
spec:
clusterRealmRef:
name: shared-realm
name: Global Organization
definition:
enabled: true
Definition Properties
Common properties from Keycloak OrganizationRepresentation:
| Property | Type | Description |
|---|---|---|
name | string | Organization name (required) |
alias | string | URL-friendly identifier |
description | string | Description of the organization |
enabled | boolean | Whether organization is enabled |
domains | array | Associated email domains |
attributes | map | Custom organization attributes |
Domain Properties
| Property | Type | Description |
|---|---|---|
name | string | Domain name (e.g., “example.com”) |
verified | boolean | Whether the domain is verified |
Short Names
| Alias | Full Name |
|---|---|
kcorg | keycloakorganizations |
kubectl get kcorg
Requirements
- Keycloak 26.0.0+: Organizations are a feature introduced in Keycloak 26. The operator will report an error if you try to create an organization on an older Keycloak version.
- Organizations must be enabled: The organization feature must be enabled in the realm settings.
Reconciliation
Reconciliation is idempotent. On each periodic resync the operator fetches the current organization from Keycloak and compares it against the spec; the update PUT is skipped when they already match, so an unchanged organization does not generate redundant writes or log noise. domains[].verified is set by Keycloak on read and is ignored in the comparison, so leaving it unset (or out of sync with the server) does not trigger a perpetual update loop.
Linking Identity Providers
To map an identity provider exclusively to this organization, set organizationRef on the KeycloakIdentityProvider to this CR’s name. The operator injects status.organizationID into the identity provider; do not copy the ID by hand.
Notes
- Organizations are immutable by ID - once created, the
idfield cannot be changed - The
aliasis used in URLs and should be URL-safe - Verified domains can be used for automatic user association based on email
- Use attributes for custom metadata and configuration
KeycloakAuthenticationFlow
A KeycloakAuthenticationFlow manages a Keycloak authentication flow and its execution tree via the Admin REST API.
The top-level fields (alias, description, providerId, realm references) are typed; the executions tree is a free-form JSON value with arbitrary nesting depth.
Specification
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakAuthenticationFlow
metadata:
name: my-custom-browser
spec:
# One of realmRef or clusterRealmRef
realmRef:
name: my-realm
# Required: flow alias (unique within the realm)
alias: my-custom-browser
# Optional human-readable description
description: "Custom browser flow with MFA"
# Top-level flow type. "basic-flow" or "client-flow" for top-level flows;
# the controller does not constrain the value, so future Keycloak provider
# types are usable without an operator release.
providerId: basic-flow
# Ordered list of executions. Each entry is either a leaf authenticator
# or a sub-flow; sub-flows recurse to arbitrary depth.
executions:
- authenticator: auth-cookie
requirement: ALTERNATIVE
- authenticator: auth-spnego
requirement: DISABLED
- subFlow:
alias: my-browser-forms
providerId: basic-flow
executions:
- authenticator: auth-username-password-form
requirement: REQUIRED
requirement: ALTERNATIVE
Execution shape
Each entry in an executions list is one of two shapes.
Leaf authenticator
- authenticator: auth-cookie # Keycloak provider ID
requirement: ALTERNATIVE # REQUIRED | ALTERNATIVE | DISABLED | CONDITIONAL
authenticatorConfig: # optional, applied after creation
someKey: someValue
Sub-flow
- subFlow:
alias: forms # required, unique within the parent
providerId: basic-flow # "basic-flow", "client-flow", or "form-flow"
description: "Optional"
executions: # child executions live here (inline shape)
- authenticator: auth-username-password-form
requirement: REQUIRED
requirement: ALTERNATIVE
The same sub-flow can also be expressed with executions placed next to subFlow instead of inside it (this matches Keycloak’s own realm export format):
- subFlow:
alias: forms
providerId: basic-flow
requirement: ALTERNATIVE
executions: # child executions live here (sibling shape)
- authenticator: auth-username-password-form
requirement: REQUIRED
If both lists are present, the inline list precedes the sibling list. Within each list, declaration order is preserved.
Sub-flow providerId values
| Value | When to use |
|---|---|
basic-flow | A regular sequence of authenticator/sub-flow steps. The most common choice. |
client-flow | Used for client authentication flows (top-level). |
form-flow | A sub-flow that aggregates FormAction providers into a single rendered form. Required when the children are form actions such as registration-user-creation, registration-profile-action, registration-password-action, registration-recaptcha. These will not work inside a basic-flow sub-flow. |
The CRD does not enumerate the allowed values so future Keycloak releases that introduce new provider types do not require an operator update.
Examples
Direct grant flow
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakAuthenticationFlow
metadata:
name: custom-direct-grant
spec:
realmRef:
name: my-realm
alias: custom-direct-grant
providerId: basic-flow
executions:
- authenticator: direct-grant-validate-username
requirement: REQUIRED
- authenticator: direct-grant-validate-password
requirement: REQUIRED
- authenticator: direct-grant-validate-otp
requirement: REQUIRED
Browser flow with conditional OTP (deeply nested)
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakAuthenticationFlow
metadata:
name: custom-browser
spec:
realmRef:
name: my-realm
alias: custom-browser
providerId: basic-flow
executions:
- authenticator: auth-cookie
requirement: ALTERNATIVE
- subFlow:
alias: custom-browser-forms
providerId: basic-flow
executions:
- authenticator: auth-username-password-form
requirement: REQUIRED
- subFlow:
alias: custom-browser-conditional-otp
providerId: basic-flow
executions:
- authenticator: conditional-user-configured
requirement: REQUIRED
- authenticator: auth-otp-form
requirement: REQUIRED
authenticatorConfig:
otpHashAlgorithm: HmacSHA1
otpLength: "6"
requirement: CONDITIONAL
requirement: ALTERNATIVE
Registration flow with form-flow sub-flow
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakAuthenticationFlow
metadata:
name: custom-registration
spec:
realmRef:
name: my-realm
alias: custom-registration
providerId: basic-flow
executions:
- subFlow:
alias: custom-registration-form
providerId: form-flow
requirement: REQUIRED
executions:
- authenticator: registration-user-creation
requirement: REQUIRED
- authenticator: registration-password-action
requirement: REQUIRED
- authenticator: registration-terms-and-conditions
requirement: DISABLED
Status
status:
ready: true
status: "Ready"
message: "Authentication flow synchronized"
flowID: "12345678-1234-1234-1234-123456789abc"
resourcePath: "/admin/realms/my-realm/authentication/flows/12345678-..."
conditions:
- type: Ready
status: "True"
reason: Ready
If the executions payload is malformed (missing requirement, both authenticator and subFlow set, missing sub-flow alias/providerId, etc.) the controller sets status.status = "InvalidSpec" and status.message with a JSON-pointer-style path to the offending node, e.g. [1].executions[0].requirement is required.
Spec fields
| Field | Type | Required | Description |
|---|---|---|---|
realmRef | object | one of realmRef / clusterRealmRef | Reference to a KeycloakRealm |
clusterRealmRef | object | one of realmRef / clusterRealmRef | Reference to a ClusterKeycloakRealm |
alias | string | yes | Unique flow alias within the realm |
description | string | no | Human-readable description |
providerId | string | yes | Top-level flow type (basic-flow, client-flow, …) |
executions | JSON array | no | Ordered list of executions; see Execution shape |
Common authenticator provider IDs
| Provider ID | Description |
|---|---|
auth-cookie | Cookie-based authentication |
auth-spnego | Kerberos / SPNEGO |
auth-username-password-form | Username/password form |
auth-otp-form | OTP form |
conditional-user-configured | Condition: user has configured the authenticator |
direct-grant-validate-username | Validate username (direct grant) |
direct-grant-validate-password | Validate password (direct grant) |
direct-grant-validate-otp | Validate OTP (direct grant) |
identity-provider-redirector | Redirect to identity provider |
registration-user-creation | Form action: create user (registration form-flow only) |
registration-password-action | Form action: set password (registration form-flow only) |
registration-terms-and-conditions | Form action: terms & conditions (registration form-flow only) |
registration-recaptcha | Form action: reCAPTCHA (registration form-flow only) |
Short names
kubectl get kcaf # KeycloakAuthenticationFlow
Behavior on update
Every reconcile (spec change or periodic resync) reads the live execution tree from Keycloak and converges it with the spec, so external drift — admin UI edits, kcadm reorders, realm re-imports — is reverted on the next pass. The top-level flow ID stays stable across updates so flows referenced as a sub-flow execution or as a realm binding (browserFlow, registrationFlow, etc.) keep working.
The reconciler walks the desired tree against the live tree and applies the minimum set of API calls:
- Identity rules:
- leaf executions are matched by their
authenticatorprovider id (e.g.auth-cookie) - sub-flow executions are matched by
subFlow.alias - leaf and sub-flow with the same name are not matched against each other
- leaf executions are matched by their
- Adds: a desired entry with no live counterpart is created with
addExecution/addSubFlow. - Removes: a live entry with no desired counterpart is deleted via
DELETE /authentication/executions/{id}. Inline sub-flow executions are deleted the same way; the top-level flow itself is never deleted on a spec change. - Updates on matched entries:
- if
requirementdiffers, it is patched viaPUT /authentication/flows/{alias}/executions. - leaf
authenticatorConfigis converged: created when the spec adds it, deleted when the spec drops it, and updated in place viaPUT /authentication/config/{id}when the key/value contents change. - sub-flow nodes recurse — children of matched sub-flows are reconciled the same way.
- if
- Reorder: at the end of each level, the controller assigns explicit
priorityvalues to every child viaPUT /authentication/flows/{alias}/executionsso the live order matches the spec deterministically. See Known limitations for the Keycloak version requirement.
A one-line summary of what changed (added=N updated=M removed=K reorderedParents=R) is logged for each update.
Duplicate identities at the same level are handled occurrence-by-occurrence: the i-th desired entry with identity X matches the i-th unmatched live entry with the same identity. Extras on either side surface as adds or removes.
Hard limits
Two changes cannot be applied in place and are reported as failures with a clear status:
- Top-level
providerIdchange (e.g.basic-flow→client-flow): Keycloak does not support swapping the provider type of an existing flow. The status is set toProviderChangeUnsupportedand the message tells you to pick a new alias. - Renaming the top-level
alias: the controller treats this as “old flow + new flow”; the old flow has to be removed by deleting itsKeycloakAuthenticationFlowresource.
Known limitations
Execution ordering requires Keycloak 25+
Order enforcement relies on the priority field added to PUT /authentication/flows/{alias}/executions in keycloak/keycloak#27751. On Keycloak 24 and older the field is silently dropped, so the operator cannot enforce or repair execution order on those versions. Initial order still matches the spec there because pre-25 Keycloak assigned sequential priorities on add. Other drift (adds, removes, requirement changes, config changes) is detected and repaired on every Keycloak version.
Notes
- Deleting the CR deletes the flow from Keycloak unless the
keycloak.hostzero.com/preserve-resourceannotation is set. - Authentication flows created by this CRD are not built-in and can be freely managed.
- To use a custom flow as the realm’s
browserFlow/registrationFlow/directGrantFlow/resetCredentialsFlow/clientAuthenticationFlow/dockerAuthenticationFlow, set those bindings in theKeycloakRealmdefinition. Keycloak rejects realm imports referencing a flow alias that does not exist yet (see keycloak/keycloak#23980). The operator works around that by stripping these bindings on the firstCreateRealmcall, marking the realmReady, and re-applying them on subsequent reconciles. The realm controller also watchesKeycloakAuthenticationFlowresources and requeues the realm immediately when a referenced flow is created, so bindings converge without long retry windows.
KeycloakRequiredAction
Identifier field: Set the alias in the
spec.aliasfield. It is required and immutable once set. Analiasinsidespec.definitionis tolerated only when it matchesspec.alias; a conflicting value is rejected.
A KeycloakRequiredAction manages a required action provider within a Keycloak realm. Required actions are steps that users must complete (e.g. update password, configure OTP, verify email) and can be enabled, disabled, or set as default for new users.
Changes to requiredActions in KeycloakRealm.spec.definition only take effect on initial realm import. This CRD uses the dedicated required action API endpoints to allow changes after realm creation.
Specification
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRequiredAction
metadata:
name: my-terms-and-conditions
spec:
# One of realmRef or clusterRealmRef must be specified
# Option 1: Reference to a namespaced KeycloakRealm
realmRef:
name: my-realm
# Option 2: Reference to a ClusterKeycloakRealm
# clusterRealmRef:
# name: my-cluster-realm
# Optional: Secret whose keys are merged into definition.config
# configSecretRef:
# name: action-secrets
# Required: RequiredActionProviderRepresentation
alias: TERMS_AND_CONDITIONS
definition:
name: "Terms and Conditions"
providerId: TERMS_AND_CONDITIONS
enabled: true
defaultAction: true
priority: 20
Status
status:
ready: true
status: "Ready"
alias: "TERMS_AND_CONDITIONS"
message: "Required action synchronized"
resourcePath: "/admin/realms/my-realm/authentication/required-actions/TERMS_AND_CONDITIONS"
conditions:
- type: Ready
status: "True"
reason: Ready
Examples
Enable and Default Terms & Conditions
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRequiredAction
metadata:
name: terms-and-conditions
namespace: keycloak
spec:
realmRef:
name: my-realm
alias: TERMS_AND_CONDITIONS
definition:
name: "Terms and Conditions"
providerId: TERMS_AND_CONDITIONS
enabled: true
defaultAction: true
priority: 20
Configure OTP as Required
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRequiredAction
metadata:
name: configure-otp
namespace: keycloak
spec:
realmRef:
name: my-realm
alias: CONFIGURE_TOTP
definition:
name: "Configure OTP"
providerId: CONFIGURE_TOTP
enabled: true
defaultAction: true
priority: 10
Verify Email
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRequiredAction
metadata:
name: verify-email
namespace: keycloak
spec:
realmRef:
name: my-realm
alias: VERIFY_EMAIL
definition:
name: "Verify Email"
providerId: VERIFY_EMAIL
enabled: true
defaultAction: false
priority: 50
Update Password
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRequiredAction
metadata:
name: update-password
namespace: keycloak
spec:
realmRef:
name: my-realm
alias: UPDATE_PASSWORD
definition:
name: "Update Password"
providerId: UPDATE_PASSWORD
enabled: true
defaultAction: false
priority: 30
With ClusterKeycloakRealm
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRequiredAction
metadata:
name: verify-email
namespace: keycloak
spec:
clusterRealmRef:
name: my-cluster-realm
alias: VERIFY_EMAIL
definition:
name: "Verify Email"
providerId: VERIFY_EMAIL
enabled: true
defaultAction: true
Definition Properties
The definition field accepts any valid Keycloak RequiredActionProviderRepresentation:
| Field | Type | Description |
|---|---|---|
alias | string | Unique alias for the required action (e.g. VERIFY_EMAIL) |
name | string | Display name |
providerId | string | Provider ID (usually same as alias) |
enabled | boolean | Whether the required action is enabled |
defaultAction | boolean | Whether new users get this action by default |
priority | integer | Ordering priority (lower = higher priority) |
config | map | Provider-specific configuration |
Common Required Action Aliases
| Alias | Description |
|---|---|
UPDATE_PASSWORD | Force password update |
CONFIGURE_TOTP | Configure OTP authenticator |
VERIFY_EMAIL | Verify email address |
UPDATE_PROFILE | Update user profile |
VERIFY_PROFILE | Verify user profile |
TERMS_AND_CONDITIONS | Accept terms and conditions |
delete_account | Allow account self-deletion |
webauthn-register | Register WebAuthn security key |
webauthn-register-passwordless | Register WebAuthn passwordless credential |
update_user_locale | Update user locale |
Short Names
| Alias | Full Name |
|---|---|
kcra | keycloakrequiredactions |
kubectl get kcra
Notes
- Most built-in required actions are pre-registered in Keycloak. This CRD will update them if they already exist, or register and configure them if they don’t.
- Custom action config secrets go in
configSecretRef(Secret references). - Deleting the CR deletes the required action from Keycloak (unless the
keycloak.hostzero.com/preserve-resourceannotation is set). - The
priorityfield controls the order in which required actions are presented to the user.
Monitoring
The Keycloak Operator exposes Prometheus metrics to enable comprehensive monitoring and alerting for your Keycloak resources.
Metrics Endpoint
Metrics are exposed at :8080/metrics by default (configurable via --metrics-bind-address).
Available Metrics
Reconciliation Metrics
| Metric | Type | Labels | Description |
|---|---|---|---|
keycloak_operator_reconcile_total | Counter | controller, result | Total number of reconciliations per controller |
keycloak_operator_reconcile_duration_seconds | Histogram | controller | Time spent in reconciliation |
keycloak_operator_reconcile_errors_total | Counter | controller, error_type | Total errors by controller and type |
keycloak_operator_last_reconcile_timestamp_seconds | Gauge | controller | Timestamp of last successful reconciliation |
Resource Metrics
| Metric | Type | Labels | Description |
|---|---|---|---|
keycloak_operator_resources_managed | Gauge | resource_type, namespace | Number of managed resources |
keycloak_operator_resources_ready | Gauge | resource_type, namespace | Number of resources in ready state |
Keycloak Connection Metrics
| Metric | Type | Labels | Description |
|---|---|---|---|
keycloak_operator_keycloak_connection_status | Gauge | instance, namespace | Connection status (1=connected, 0=disconnected) |
keycloak_operator_keycloak_api_requests_total | Counter | instance, method, endpoint, status | Total Keycloak API requests |
keycloak_operator_keycloak_api_latency_seconds | Histogram | instance, method, endpoint | Keycloak API latency |
Controller Metrics
| Metric | Type | Labels | Description |
|---|---|---|---|
keycloak_operator_workqueue_depth | Gauge | controller | Work queue depth per controller |
Error Types
The error_type label can have the following values:
fetch_error- Failed to fetch the Kubernetes resourceconnection_error- Failed to connect to Keycloakinstance_not_ready- Referenced KeycloakInstance is not readyrealm_not_ready- Referenced KeycloakRealm is not readyinvalid_definition- Invalid resource definition (JSON parsing failed)keycloak_api_error- Keycloak API call failedsecret_sync_error- Failed to synchronize client secret
Monitoring Recommendations
Critical Alerts
Set up alerts for these critical conditions:
1. Keycloak Connection Failures
alert: KeycloakConnectionDown
expr: keycloak_operator_keycloak_connection_status == 0
for: 5m
labels:
severity: critical
annotations:
summary: "Keycloak connection lost"
description: "Instance {{ $labels.instance }} in {{ $labels.namespace }} has been disconnected for 5 minutes"
2. High Reconciliation Error Rate
alert: KeycloakOperatorHighErrorRate
expr: |
rate(keycloak_operator_reconcile_errors_total[5m])
/ rate(keycloak_operator_reconcile_total[5m]) > 0.1
for: 10m
labels:
severity: warning
annotations:
summary: "High reconciliation error rate"
description: "Controller {{ $labels.controller }} has >10% error rate"
3. Resources Not Ready
alert: KeycloakResourcesNotReady
expr: |
keycloak_operator_resources_managed - keycloak_operator_resources_ready > 0
for: 15m
labels:
severity: warning
annotations:
summary: "Keycloak resources not ready"
description: "{{ $value }} {{ $labels.resource_type }} resources are not ready in {{ $labels.namespace }}"
4. Slow Reconciliation
alert: KeycloakSlowReconciliation
expr: |
histogram_quantile(0.99,
rate(keycloak_operator_reconcile_duration_seconds_bucket[5m])
) > 30
for: 10m
labels:
severity: warning
annotations:
summary: "Slow reconciliation detected"
description: "Controller {{ $labels.controller }} p99 reconciliation time exceeds 30s"
5. Controller Stale
alert: KeycloakControllerStale
expr: |
time() - keycloak_operator_last_reconcile_timestamp_seconds > 600
for: 5m
labels:
severity: critical
annotations:
summary: "Controller not reconciling"
description: "Controller {{ $labels.controller }} has not reconciled for 10+ minutes"
Dashboard Recommendations
Create a Grafana dashboard with these panels:
-
Overview
- Total managed resources by type
- Ready vs non-ready resources
- Keycloak instance connection status
-
Reconciliation Performance
- Reconciliation rate per controller
- Reconciliation duration (p50, p95, p99)
- Error rate over time
-
Errors & Issues
- Error breakdown by type
- Recent error spikes
- Connection failures over time
-
Keycloak API
- API request rate by endpoint
- API latency distribution
- Error responses by status code
Key Metrics to Watch
| Metric | Normal Range | Action if Abnormal |
|---|---|---|
| Connection status | 1 | Check Keycloak availability, credentials |
| Error rate | < 5% | Review logs, check Keycloak health |
| Reconcile duration p99 | < 10s | Check Keycloak performance |
| Queue depth | < 50 | Scale operator or reduce resources |
| Resources not ready | 0 | Check individual resource status |
Prometheus ServiceMonitor
If using Prometheus Operator, create a ServiceMonitor:
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: keycloak-operator
labels:
app: keycloak-operator
spec:
selector:
matchLabels:
control-plane: controller-manager
endpoints:
- port: metrics
interval: 30s
path: /metrics
Helm Chart Configuration
Enable metrics in the Helm chart:
metrics:
enabled: true
serviceMonitor:
enabled: true
interval: 30s
labels: {}
OpenTelemetry
OTLP export of traces and logs is opt-in. Metrics stay on the Prometheus scrape endpoint above; point an OpenTelemetry Collector Prometheus receiver at it if you want metrics in the same pipeline.
Enable via Helm:
otel:
enabled: true
endpoint: http://otel-collector:4317
protocol: grpc # or http/protobuf
Or set the standard env vars directly (OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_PROTOCOL, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, OTEL_EXPORTER_OTLP_LOGS_ENDPOINT). Sampler, headers, and resource attributes use the usual OTEL_* variables (for example OTEL_TRACES_SAMPLER=parentbased_traceidratio and OTEL_TRACES_SAMPLER_ARG=0.1).
When enabled, the operator exports:
- A root
Reconcilespan per reconciliation (controller,k8s.namespace.name,k8s.name) - Child HTTP spans for Keycloak Admin API calls
- Logs over OTLP, in addition to stdout. Stdout format is unchanged (
logging.format). Reconcile logs includetrace_idandspan_idso stdout and OTLP logs correlate with traces.
Export is a no-op when no OTLP endpoint is set. Collector unavailability does not block startup.
Architecture
The Keycloak Operator follows the Kubernetes operator pattern to manage Keycloak resources declaratively.
Overview
┌─────────────────────────────────────────────────────────────────┐
│ Kubernetes Cluster │
│ │
│ ┌──────────────────────────────────────────────────────────────┐│
│ │ Custom Resources ││
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ ││
│ │ │ Keycloak │ │ Keycloak │ │ Keycloak │ ││
│ │ │ Instance │ │ Realm │ │ Client │ ... ││
│ │ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ ││
│ └────────┼──────────────┼──────────────┼───────────────────────┘│
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐│
│ │ Keycloak Operator ││
│ │ ┌────────────────────────────────────────────────────────┐ ││
│ │ │ Controller Manager │ ││
│ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ ││
│ │ │ │ Instance │ │ Realm │ │ Client │ │ ││
│ │ │ │ Controller │ │ Controller │ │ Controller │ │ ││
│ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ ││
│ │ └────────────────────────────────────────────────────────┘ ││
│ │ │ ││
│ │ ▼ ││
│ │ ┌────────────────────────────────────────────────────────┐ ││
│ │ │ Keycloak Client │ ││
│ │ │ (custom resty-based HTTP client) │ ││
│ │ └────────────────────────────────────────────────────────┘ ││
│ └──────────────────────────────────────────────────────────────┘│
│ │ │
└───────────────────────────────┼──────────────────────────────────┘
│
▼
┌───────────────────────┐
│ Keycloak Server │
│ (Admin REST API) │
└───────────────────────┘
Components
Controller Manager
The controller manager is the main component that runs all controllers. It is built using the controller-runtime library.
Key features:
- Leader election for high availability
- Health and readiness probes
- Metrics endpoint for Prometheus
- Graceful shutdown handling
Controllers
Each CRD type has a dedicated controller that implements the reconciliation logic:
| Controller | CRD | Responsibilities |
|---|---|---|
| Instance Controller | KeycloakInstance, ClusterKeycloakInstance | Connection management, health checking |
| Realm Controller | KeycloakRealm, ClusterKeycloakRealm | Realm CRUD, configuration sync |
| Client Controller | KeycloakClient | Client CRUD, secret management |
| ClientScope Controller | KeycloakClientScope | Scope CRUD |
| ProtocolMapper Controller | KeycloakProtocolMapper | Token claim mapper configuration |
| User Controller | KeycloakUser | User CRUD |
| UserCredential Controller | KeycloakUserCredential | Password management |
| Group Controller | KeycloakGroup | Group CRUD, hierarchy management |
| Role Controller | KeycloakRole | Realm and client role management |
| RoleMapping Controller | KeycloakRoleMapping | Role-to-subject assignments |
| IdentityProvider Controller | KeycloakIdentityProvider | External IDP configuration |
| Component Controller | KeycloakComponent | LDAP, key providers, etc. |
| Organization Controller | KeycloakOrganization | Organization management (KC 26+) |
Keycloak Client
The operator uses a custom HTTP client built on resty. Key features:
- Version-agnostic: Works with raw JSON to support all Keycloak versions
- Connection pooling: Multiple KeycloakInstances share clients via
ClientManager - Token management: Automatic token acquisition and refresh
- Retry logic: Exponential backoff for transient errors (5xx, network issues)
- Pass-through definitions: CR definitions are sent directly to Keycloak without field stripping
Reconciliation Flow
┌─────────────────┐
│ CR Created/ │
│ Updated/Deleted│
└────────┬────────┘
│
▼
┌─────────────────┐
│ Controller │
│ Triggered │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Get Current │
│ State from KC │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Compare Desired │
│ vs Actual State │
└────────┬────────┘
│
┌────────────────┼────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Create │ │ Update │ │ Delete │
│ in KC │ │ in KC │ │ from KC │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
└───────────────┴───────────────┘
│
▼
┌─────────────────┐
│ Update CR │
│ Status │
└─────────────────┘
Resource Dependencies
Resources form a hierarchy with parent-child relationships:
KeycloakInstance (connection to Keycloak)
│
└── KeycloakRealm (realm within instance)
│
├── KeycloakClient (client within realm)
│
├── KeycloakUser (user within realm)
│
├── KeycloakGroup (group within realm)
│ │
│ └── KeycloakGroup (nested child group)
│
├── KeycloakClientScope (scope within realm)
│
└── KeycloakIdentityProvider (IDP within realm)
Controllers resolve parent references and wait for parents to be ready before proceeding.
Tenancy and Namespace Boundaries
The operator treats the namespace as a hard tenancy boundary. All namespaced
resource references (realmRef, instanceRef, clientRef, clientScopeRef,
userRef, roleRef, groupRef, parentGroupRef, identityProviderRef)
resolve in the referring resource’s own namespace only — the namespace field
has been removed from ResourceRef. This means every child resource (clients,
users, groups, roles, …) must live in the same namespace as the realm it belongs
to.
The two valid deployment patterns are:
-
Namespaced realm —
KeycloakInstance+KeycloakRealm+ all child CRDs in the same namespace. NamespaceAcannot reach a realm in namespaceB. -
Cluster realm —
ClusterKeycloakInstance/ClusterKeycloakRealm(cluster-scoped) referenced viaclusterInstanceRef/clusterRealmReffrom child CRDs in any namespace. Use this for cross-namespace or cluster-wide sharing.
Every namespaced CRD that targets a realm supports both modes. The four CRDs
without a direct realm reference (KeycloakProtocolMapper,
KeycloakUserCredential, KeycloakRoleMapping, KeycloakIdentityProviderMapper)
inherit the realm transitively from the resource they reference, and that
resource must also be in the same namespace.
Finalizers
The operator uses finalizers to ensure proper cleanup:
- When a CR is created, a finalizer is added
- When a CR is deleted, the controller:
- Removes the resource from Keycloak
- Removes the finalizer
- Kubernetes then removes the CR
This ensures resources are properly cleaned up in Keycloak even if the cluster is disrupted.
High Availability
The operator supports running multiple replicas with leader election:
- Only the leader processes reconciliations
- Other replicas are hot standby
- Automatic failover on leader failure
- Configurable via
leaderElection.enabledHelm value
Performance Tuning
For large deployments with many resources, the operator provides tuning options:
Sync Period
The --sync-period flag controls how often successfully reconciled resources are re-checked for drift:
# Default: 5 minutes
--sync-period=5m
# For large deployments (100+ resources): 30 minutes
--sync-period=30m
# For very large deployments or slow networks: 1 hour
--sync-period=1h
Trade-offs:
- Shorter periods: Faster drift detection, higher Keycloak API load
- Longer periods: Lower API load, slower drift detection
In Helm:
performance:
syncPeriod: "30m"
Rate Limiting
The --max-concurrent-requests flag limits parallel requests to Keycloak:
# Default: 10 concurrent requests
--max-concurrent-requests=10
# For resource-constrained Keycloak instances
--max-concurrent-requests=5
# No limit (not recommended for large deployments)
--max-concurrent-requests=0
Trade-offs:
- Lower values: Less Keycloak load, slower reconciliation on startup
- Higher values: Faster reconciliation, more Keycloak load
In Helm:
performance:
maxConcurrentRequests: 5
Recommendations by Scale
| Resources | Sync Period | Max Concurrent Requests |
|---|---|---|
| < 50 | 5m (default) | 10 (default) |
| 50-200 | 15-30m | 10 |
| 200-500 | 30m | 5-10 |
| 500+ | 1h | 5 |
The exact values depend on your Keycloak instance capacity and acceptable drift detection latency.
Development
This section covers how to set up a development environment and contribute to the Keycloak Operator.
Prerequisites
- Go 1.22+
- Docker
- kubectl
- Kind or Minikube
- Make
Getting Started
-
Clone the repository:
git clone https://github.com/Hostzero-GmbH/keycloak-operator.git cd keycloak-operator -
Install dependencies:
go mod download -
Set up the development environment:
make kind-all
Project Structure
keycloak-operator/
├── api/v1beta1/ # CRD type definitions
├── cmd/main.go # Entry point
├── internal/
│ ├── controller/ # Reconciliation logic
│ └── keycloak/ # Keycloak client wrapper
├── config/
│ ├── crd/ # CRD manifests
│ ├── manager/ # Operator deployment
│ ├── rbac/ # RBAC configuration
│ └── samples/ # Example CRs
├── charts/ # Helm chart
├── hack/ # Development scripts
├── test/
│ └── e2e/ # End-to-end tests
└── docs/ # Documentation (mdBook)
Development Workflow
See the specific guides:
Local Setup
This guide explains how to set up a local development environment.
Prerequisites
- Go 1.22+
- Docker
- Kind (
brew install kindorgo install sigs.k8s.io/kind@latest) - kubectl
- Helm
Quick Start with Kind (Recommended)
The easiest way to develop is using Kind:
# Create cluster and deploy everything
make kind-all
This sets up:
- Kind cluster with 3 nodes
- Keycloak instance (admin/admin)
- Operator deployment
- Test resources
Iterating on Changes
# After code changes, rebuild and redeploy
make kind-redeploy
# Check operator logs
make kind-logs
Accessing Keycloak
To access Keycloak from your local machine:
# Port-forward Keycloak to localhost:8080
make kind-port-forward
Then open http://localhost:8080 (admin/admin).
Run Against External Keycloak
You can run the operator against any Keycloak instance:
- Configure kubeconfig for your cluster
- Install CRDs:
make install - Create a KeycloakInstance pointing to your Keycloak
- Run locally:
make run
Development Commands
| Command | Description |
|---|---|
make build | Build the operator binary |
make run | Run the operator locally |
make install | Install CRDs to cluster |
make generate | Generate DeepCopy methods |
make manifests | Generate CRD manifests |
make fmt | Format code |
make vet | Run go vet |
make lint | Run golangci-lint |
IDE Setup
VS Code
Recommended extensions:
- Go
- YAML
- Kubernetes
Settings (.vscode/settings.json):
{
"go.lintTool": "golangci-lint",
"go.lintFlags": ["--fast"],
"go.testFlags": ["-v"]
}
GoLand
- Enable Go modules integration
- Configure GOROOT to Go 1.22+
- Set up golangci-lint as external tool
Debugging
Local Debugging
- Set breakpoints in your IDE
- Run with debug configuration:
dlv debug ./cmd/main.go
Remote Debugging
For debugging in-cluster:
-
Build with debug symbols:
CGO_ENABLED=0 go build -gcflags="all=-N -l" -o manager ./cmd/main.go -
Use
kubectl port-forwardto access debugger port
Environment Variables
| Variable | Description | Default |
|---|---|---|
KUBECONFIG | Path to kubeconfig | ~/.kube/config |
KEYCLOAK_URL | Keycloak URL for tests | http://localhost:8080 |
Use --zap-log-level=debug --zap-encoder=console --zap-devel for verbose local logs.
Testing
The Keycloak Operator has two levels of testing:
- Unit Tests: Fast, isolated tests using
envtest - End-to-End Tests: Full cluster tests against Kind with Keycloak
Unit Tests
Run unit tests with:
make test
Unit tests use the controller-runtime’s envtest package to provide a lightweight Kubernetes API server. These don’t require a real Keycloak instance.
Coverage
make test
go tool cover -html=cover.out
End-to-End Tests
E2E tests run against a full Kind cluster with the operator and Keycloak deployed.
Understanding E2E Test Network Topology
E2E tests involve two different network perspectives:
- Operator’s perspective (inside the cluster): The operator connects to Keycloak using the in-cluster service URL (e.g.,
http://keycloak.keycloak.svc.cluster.local) - Test’s perspective (your local machine): When running tests locally, you need port-forwarding to access Keycloak directly for certain tests (drift detection, cleanup verification)
┌─────────────────────────────────────────────────────────┐
│ Kind Cluster │
│ ┌─────────────┐ ┌──────────────────┐ │
│ │ Operator │──────│ Keycloak │ │
│ │ │ │ (port 80/8080) │ │
│ └─────────────┘ └────────┬─────────┘ │
│ │ │
└────────────────────────────────┼────────────────────────┘
│ port-forward
▼
┌────────────────────────┐
│ localhost:8080 │
│ (your machine) │
└────────────────────────┘
Running E2E Tests
Development workflow:
# 1. Initial setup (only needed once)
make kind-all
# 2. In a separate terminal, start port-forward (keep this running)
make kind-port-forward
# 3. After making code changes, rebuild and redeploy the operator
make kind-redeploy
# 4. Run all e2e tests
make kind-test-run
# 5. Or run specific tests using TEST_RUN
make kind-test-run TEST_RUN=TestPreserveResourceAnnotation
The kind-redeploy target handles the full update cycle:
- Rebuilds the Docker image (layer caching detects source changes automatically)
- Removes old images from Kind nodes (avoids containerd tag caching)
- Loads the new image into the Kind cluster
- Restarts the operator deployment and waits for it to be ready
Manual setup (for full control):
# 1. Ensure cluster and operator are running
make kind-all
# 2. In a separate terminal, start port-forward
kubectl port-forward -n keycloak svc/keycloak 8080:80
# 3. Run tests with required environment variables
export USE_EXISTING_CLUSTER=true
export KEYCLOAK_URL="http://localhost:8080" # For test's direct Keycloak access
export KEYCLOAK_INTERNAL_URL="http://keycloak.keycloak.svc.cluster.local" # For operator (inside cluster)
go test -v -timeout 30m ./test/e2e/...
Note: Tests that require direct Keycloak access (drift detection, cleanup verification) will be automatically skipped if port-forward is not available. This allows running basic E2E tests without port-forwarding, while advanced tests require it.
Quick Reference: Make Targets
| Target | Description |
|---|---|
make kind-all | Create cluster and deploy everything |
make kind-redeploy | Rebuild and restart operator (fast iteration) |
make kind-test-run | Run e2e tests (requires port-forward) |
make kind-test-run TEST_RUN=TestFoo | Run specific test(s) |
make kind-port-forward | Port-forward Keycloak to localhost:8080 |
make kind-logs | Tail operator logs |
make kind-reset | Reset cluster to clean state |
E2E Test Configuration
| Variable | Description | Default |
|---|---|---|
USE_EXISTING_CLUSTER | Set to true to use current kubeconfig | false |
KEYCLOAK_INSTANCE_NAME | Name of existing KeycloakInstance to use | (creates new) |
KEYCLOAK_INSTANCE_NAMESPACE | Namespace of existing instance | keycloak-operator-e2e |
OPERATOR_NAMESPACE | Namespace where operator is deployed | keycloak-operator |
KEYCLOAK_URL | URL for test’s direct Keycloak access (via port-forward) | http://localhost:8080 |
KEYCLOAK_INTERNAL_URL | URL operator uses to connect (in-cluster) | http://keycloak.keycloak.svc.cluster.local |
TEST_NAMESPACE | Namespace for test resources | keycloak-operator-e2e |
KEEP_TEST_NAMESPACE | Don’t delete namespace after tests | false |
Test Categories
| Category | Requires Port-Forward | Description |
|---|---|---|
| Basic CRUD | No | Create, update, delete resources via Kubernetes API |
| Status verification | No | Verify .status.ready and conditions |
| Drift detection | Yes | Tests that modify Keycloak directly and verify reconciliation |
| Cleanup verification | Yes | Tests that verify resources are deleted from Keycloak |
| Edge cases | Mixed | Some require direct access, some don’t |
Unit Test Example
func TestRealmController_Reconcile(t *testing.T) {
// Setup
scheme := runtime.NewScheme()
_ = keycloakv1beta1.AddToScheme(scheme)
realm := &keycloakv1beta1.KeycloakRealm{
ObjectMeta: metav1.ObjectMeta{
Name: "test-realm",
Namespace: "default",
},
Spec: keycloakv1beta1.KeycloakRealmSpec{
InstanceRef: "test-instance",
},
}
client := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(realm).
Build()
// Test reconciliation...
}
E2E Test Example
func TestKeycloakRealmE2E(t *testing.T) {
skipIfNoCluster(t)
realm := &keycloakv1beta1.KeycloakRealm{
ObjectMeta: metav1.ObjectMeta{
Name: "e2e-realm",
Namespace: testNamespace,
},
Spec: keycloakv1beta1.KeycloakRealmSpec{
InstanceRef: instanceName,
Definition: rawJSON(`{"realm": "e2e-realm", "enabled": true}`),
},
}
require.NoError(t, k8sClient.Create(ctx, realm))
t.Cleanup(func() {
k8sClient.Delete(ctx, realm)
})
// Wait for ready
err := wait.PollUntilContextTimeout(ctx, interval, timeout, true,
func(ctx context.Context) (bool, error) {
updated := &keycloakv1beta1.KeycloakRealm{}
if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(realm), updated); err != nil {
return false, nil
}
return updated.Status.Ready, nil
})
require.NoError(t, err)
}
// Example: Test requiring direct Keycloak access (drift detection)
func TestDriftDetection(t *testing.T) {
skipIfNoCluster(t)
skipIfNoKeycloakAccess(t) // Skips if port-forward not available
// ... test that modifies Keycloak directly ...
}
CI/CD
Tests run automatically in GitHub Actions:
- Unit tests on every PR
- E2E tests on merge to main
Test Utilities
Common test utilities are in test/e2e/suite_test.go:
skipIfNoCluster(t): Skip test ifUSE_EXISTING_CLUSTERis not setskipIfNoKeycloakAccess(t): Skip test if direct Keycloak access (port-forward) is unavailablegetInternalKeycloakClient(t): Create authenticated Keycloak client for direct API accessrawJSON(s string): Createruntime.RawExtensionfrom JSON stringcanConnectToKeycloak(): Check if direct Keycloak connection is available
Contributing
Thank you for your interest in contributing to the Keycloak Operator!
Code of Conduct
Please be respectful and constructive in all interactions.
How to Contribute
Reporting Issues
- Search existing issues first
- Provide clear reproduction steps
- Include relevant logs and configuration
Submitting Changes
-
Fork the repository
-
Create a feature branch:
git checkout -b feature/my-feature -
Make your changes following the code style
-
Add tests for new functionality
-
Run checks:
make fmt make vet make lint make test -
Commit with a clear message:
git commit -m "feat: add support for X" -
Push and create a Pull Request
Commit Messages
Follow Conventional Commits:
feat:New featurefix:Bug fixdocs:Documentation onlyrefactor:Code change without feature/fixtest:Adding testschore:Maintenance
Code Style
- Follow standard Go conventions
- Use
gofmtandgolangci-lint - Add comments for exported types/functions
- Keep functions focused and small
- When adding CRD fields, follow the Spec Layout rules for what goes in spec versus
definition - Controllers must end
updateStatuswithwriteStatusIfChangedand build the Ready condition withsetReadyCondition. Writing status unconditionally re-triggers the controller’s own watch and produces a reconcile hot loop instead of honouring the sync period
Testing Requirements
- Unit tests for new logic
- E2E tests for new CRD features and Keycloak interactions
Development Setup
See Local Setup for environment setup.
Pull Request Process
- Ensure all tests pass
- Update documentation if needed
- Request review from maintainers
- Address feedback
- Squash commits if requested
Getting Help
- Open an issue for questions
- Check existing documentation
- Review similar PRs for patterns
License
By contributing, you agree that your contributions will be licensed under the MIT License.