Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 automatically synced to Kubernetes Secrets
  • 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

Hostzero

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:

ServiceDescription
Enterprise SupportSLA-backed support with guaranteed response times
Security ConsultingHardening, compliance audits, and KRITIS certification support
On-Premises DeploymentAir-gapped and sovereign cloud deployments
Incident Response24/7 emergency support for production environments
TrainingWorkshops 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:

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
  • kubectl installed and configured
  • helm installed (optional, for Helm installation)
  • A Keycloak instance (or use the provided Kind setup)

Step 1: Install the Operator

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

Helm Chart Installation

The Keycloak Operator Helm chart provides a flexible way to deploy the operator with customizable settings.

Installation

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

ParameterDescriptionDefault
replicaCountNumber of operator replicas1
image.repositoryContainer image repositoryghcr.io/hostzero-gmbh/keycloak-operator
image.tagContainer image tagChart appVersion
image.pullPolicyImage pull policyIfNotPresent

Resources

ParameterDescriptionDefault
resources.limits.cpuCPU limit500m
resources.limits.memoryMemory limit256Mi
resources.requests.cpuCPU request100m
resources.requests.memoryMemory request128Mi

Features

ParameterDescriptionDefault
leaderElection.enabledEnable leader electiontrue
metrics.enabledEnable metrics endpointtrue
metrics.serviceMonitor.enabledCreate Prometheus ServiceMonitorfalse

CRDs

ParameterDescriptionDefault
crds.installInstall CRDs with Helmtrue
crds.keepKeep CRDs on uninstalltrue

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 kind or go 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

CommandDescription
make kind-allFull setup: cluster + Keycloak + operator
make kind-redeployRebuild and restart operator (fast iteration)
make kind-test-runRun e2e tests (use TEST_RUN=TestName to filter)
make kind-logsTail operator logs
make kind-port-forwardPort-forward Keycloak to localhost:8080
make kind-resetReset cluster to clean state
make kind-deleteDelete 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:

TypeDescription
realmThe realm itself
clientsOAuth2/OIDC clients
client-scopesClient scopes
usersUser accounts
groupsUser groups
rolesRealm and client roles
role-mappingsRole assignments to users/groups
identity-providersExternal identity providers (SAML, OIDC, etc.)
componentsLDAP federation, key providers, etc.
protocol-mappersToken claim mappers
organizationsOrganizations (Keycloak 26+)

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

You must create these secrets separately and configure the CRs to reference them.

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

  1. 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
  1. Review and customize manifests:
# Review generated files
ls -la manifests/

# Edit as needed (add client secrets, customize settings)
vim manifests/clients/my-app.yaml
  1. 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
  1. Deploy the operator (if not already installed):
helm install keycloak-operator oci://ghcr.io/hostzero-gmbh/charts/keycloak-operator \
  --namespace keycloak-operator \
  --create-namespace
  1. 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
  1. 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:

OptionDescriptionDefault
--metrics-bind-addressAddress for metrics endpoint:8080
--health-probe-bind-addressAddress for health probes:8081
--leader-electEnable leader electionfalse

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 can be configured using environment variables, which are automatically set when deploying via Helm.

Operator Settings

VariableDescriptionDefault
POD_NAMESPACENamespace where the operator is runningInjected by Kubernetes
POD_NAMEName of the operator podInjected by Kubernetes

Logging

VariableDescriptionDefault
LOG_LEVELLog level (debug, info, error)info
LOG_FORMATLog format (json, console)json

Metrics

VariableDescriptionDefault
METRICS_BIND_ADDRESSAddress for metrics endpoint:8080

Health Probes

VariableDescriptionDefault
HEALTH_PROBE_BIND_ADDRESSAddress for health probes:8081

Development

For local development, you can set these in your shell:

export LOG_LEVEL=debug
export LOG_FORMAT=console
make run

Or use a .env file with your IDE.

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

logging:
  level: info      # debug, info, error
  format: json     # json, console
  development: false

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

CRDDescriptionScope
KeycloakInstanceConnection to a Keycloak serverNamespaced
ClusterKeycloakInstanceCluster-scoped Keycloak connectionCluster

Realm Resources

CRDDescriptionParent
KeycloakRealmRealm configurationKeycloakInstance
ClusterKeycloakRealmCluster-scoped realmClusterKeycloakInstance

OAuth & Client Resources

CRDDescriptionParent
KeycloakClientOAuth2/OIDC clientKeycloakRealm
KeycloakClientScopeClient scope configurationKeycloakRealm
KeycloakProtocolMapperToken claim mappersKeycloakClient or KeycloakClientScope

Identity Resources

CRDDescriptionParent
KeycloakUserUser managementKeycloakRealm or KeycloakClient¹
KeycloakUserCredentialUser password managementKeycloakUser
KeycloakGroupGroup managementKeycloakRealm

Role & Access Control

CRDDescriptionParent
KeycloakRoleRealm and client rolesKeycloakRealm or KeycloakClient
KeycloakRoleMappingRole-to-subject mappingsKeycloakUser or KeycloakGroup

Federation & Infrastructure

CRDDescriptionParent
KeycloakComponentLDAP federation, key providersKeycloakRealm
KeycloakIdentityProviderExternal identity providersKeycloakRealm
KeycloakIdentityProviderMapperIdentity provider claim/role/attribute mappersKeycloakIdentityProvider
KeycloakAuthenticationFlowCustom authentication / registration flowsKeycloakRealm
KeycloakRequiredActionRequired action providers (e.g. update password, verify email)KeycloakRealm
KeycloakOrganizationOrganization management²KeycloakRealm

¹ KeycloakUser supports clientRef for managing service account users associated with a client
² KeycloakOrganization requires Keycloak 26.0.0 or later

Common Patterns

Definition Field

Most resources include a definition field that accepts the full Keycloak API representation:

spec:
  definition:
    # Full Keycloak API object
    realm: my-realm
    enabled: true
    displayName: My Realm

This provides flexibility to configure any Keycloak property, even those not explicitly modeled in the CRD.

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

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.caCert references a PEM-encoded CA bundle from either a Secret or a ConfigMap. Exactly one of secretRef / configMapRef may be set; setting both is rejected by admission. The default key is ca.crt.
  • tls.insecureSkipVerify: true disables certificate verification. When set, caCert is 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

AliasFull name
kcikeycloakinstances
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

FieldTypeDescriptionRequired
baseUrlstringURL of the Keycloak serverYes
auth.passwordGrant / auth.clientCredentialsobjectAuthentication method (exactly one)Yes
auth.passwordGrant.usernamestringInline admin username (overrides secretRef.usernameKey)No
auth.passwordGrant.secretRef.namestringName of the credentials SecretYes
auth.passwordGrant.secretRef.namespacestringNamespace of the credentials SecretYes
auth.passwordGrant.secretRef.usernameKeystringSecret key for the usernameNo (default username)
auth.passwordGrant.secretRef.passwordKeystringSecret key for the passwordNo (default password)
auth.clientCredentials.clientIdstringInline client id (overrides secretRef.clientIdKey)No
auth.clientCredentials.secretRef.namestringName of the client-credentials SecretYes
auth.clientCredentials.secretRef.namespacestringNamespace of the client-credentials SecretYes
auth.clientCredentials.secretRef.clientIdKeystringSecret key for the client idNo (default client-id)
auth.clientCredentials.secretRef.clientSecretKeystringSecret key for the client secretNo (default client-secret)
realmstringAdmin realm nameNo (default master)
tls.caCert.secretRef / tls.caCert.configMapRefobjectPEM-encoded CA bundle source (exactly one)No
tls.insecureSkipVerifyboolDisable TLS verification (overrides caCert)No (default false)
token.*objectToken cache configurationNo

Comparison with KeycloakInstance

AspectKeycloakInstanceClusterKeycloakInstance
ScopeNamespacedCluster
Secret namespaceOptional (defaults to same as resource)Required
Accessible fromSame namespace onlyAny namespace
Short namekcickci

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

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
  
  # Optional: Realm name in Keycloak (defaults to metadata.name)
  realmName: my-realm
  
  # Required: Realm definition (Keycloak RealmRepresentation)
  definition:
    realm: my-realm
    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
  definition:
    realm: my-app
    displayName: My Application
    enabled: true

With ClusterKeycloakInstance

apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRealm
metadata:
  name: my-app-realm
spec:
  clusterInstanceRef:
    name: central-keycloak
  definition:
    realm: my-app
    displayName: My Application
    enabled: true

Full Configuration

apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRealm
metadata:
  name: production-realm
spec:
  instanceRef:
    name: production-keycloak
  definition:
    realm: production
    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
  definition:
    realm: my-realm
    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
  definition:
    realm: my-realm
    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:

PropertyTypeDescription
realmstringRealm name (required)
displayNamestringDisplay name for the realm
enabledbooleanWhether the realm is enabled
registrationAllowedbooleanAllow user registration
loginWithEmailAllowedbooleanAllow login with email
ssoSessionIdleTimeoutintegerSSO session idle timeout (seconds)
accessTokenLifespanintegerAccess 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:

  1. On the first CreateRealm call, 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 marked Ready; the operator records that bindings were deferred.
  2. The realm controller watches KeycloakAuthenticationFlow resources and requeues the realm immediately when a referenced flow becomes ready, instead of waiting for the next periodic resync.
  3. 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
  definition:
    realm: my-realm
    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:

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
  definition:
    realm: my-realm
    enabled: true

See Common Patterns for more details.

Short Names

AliasFull Name
kcrmkeycloakrealms
kubectl get kcrm

ClusterKeycloakRealm

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 ClusterKeycloakInstance for 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
  definition:
    realm: shared
    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
  definition:
    realm: company
    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:
    realm: prod
    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

FieldTypeDescriptionRequired
clusterInstanceRef.namestringReference to ClusterKeycloakInstanceOne of these
instanceRef.namestringReference to namespaced KeycloakInstanceOne of these
instanceRef.namespacestringNamespace of the KeycloakInstanceRequired if instanceRef
realmNamestringOverride realm name in KeycloakNo (defaults to metadata.name)
definitionobjectKeycloak RealmRepresentationYes

Definition Fields

The definition field accepts any valid Keycloak RealmRepresentation. Common fields include:

FieldTypeDescription
realmstringRealm identifier (required)
enabledbooleanWhether realm is enabled
displayNamestringDisplay name
sslRequiredstringSSL requirement: all, external, none
registrationAllowedbooleanAllow user self-registration
loginWithEmailAllowedbooleanAllow login with email
verifyEmailbooleanRequire email verification
resetPasswordAllowedbooleanEnable password reset
bruteForceProtectedbooleanEnable brute force protection
accessTokenLifespanintegerAccess token lifetime (seconds)
ssoSessionIdleTimeoutintegerSSO session idle timeout (seconds)
loginThemestringLogin page theme

Status

FieldTypeDescription
readybooleanWhether the realm is synced
statusstringCurrent status (Ready, InstanceNotReady, CreateFailed, etc.)
messagestringAdditional status information
resourcePathstringKeycloak API path for this realm
realmNamestringActual realm name in Keycloak
instanceobjectResolved instance reference
conditions[]ConditionKubernetes conditions

Behavior

Instance Resolution

The controller supports two instance reference types:

  1. clusterInstanceRef: References a ClusterKeycloakInstance by name
  2. instanceRef: References a namespaced KeycloakInstance by name and namespace

One of these must be specified.

Realm Synchronization

On each reconciliation:

  1. Connect to Keycloak using the referenced instance
  2. Check if the realm exists
  3. Create or update the realm with the specified definition
  4. Update status with the resource path

Cleanup

When a ClusterKeycloakRealm is deleted:

  1. The finalizer removes the realm from Keycloak
  2. All resources in Keycloak (clients, users, etc.) within that realm are deleted
  3. 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
  definition:
    realm: acme
    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
  definition:
    clientId: acme-web-app
    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
  definition:
    realm: app-dev
    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
  definition:
    realm: app-prod
    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
  definition:
    clientId: my-client
    # ...
---
# 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
  definition:
    username: myuser
    # ...

Comparison with KeycloakRealm

AspectKeycloakRealmClusterKeycloakRealm
ScopeNamespacedCluster
Instance refSame namespace or cross-namespaceCluster or any namespaced
Accessible fromSame namespaceAny namespace
Short namekcrmckcrm
Use caseSingle namespaceMulti-namespace/platform

Notes

  • Only one ClusterKeycloakRealm with 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 definition are applied on each reconciliation

KeycloakClient

A KeycloakClient represents an OAuth2/OIDC client within a Keycloak realm.

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
  
  # Optional: Client ID in Keycloak (defaults to metadata.name)
  clientId: my-app
  
  # Optional: Client definition (Keycloak ClientRepresentation)
  definition:
    clientId: my-app
    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:

FieldTypeDefaultDescription
namestring(required)Name of the Kubernetes Secret
clientIdKeystringclient-idKey for the client ID in the secret
clientSecretKeystringclient-secretKey for the client secret in the secret
createbooleantrueWhether 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): When clientSecretRef is set, the Secret is still materialized but only contains the client-id key — public OAuth clients have no client_secret to store. This lets consumer charts pull the client ID via envFrom or secretKeyRef regardless of whether the client is public or confidential. If clientSecretRef is 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
  definition:
    clientId: my-spa
    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
  definition:
    clientId: my-api
    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
  definition:
    clientId: my-service
    name: My Service Account
    enabled: true
    publicClient: false
    serviceAccountsEnabled: true
    standardFlowEnabled: false
    directAccessGrantsEnabled: false
    authorizationServicesEnabled: true
  clientSecretRef:
    name: my-service-credentials

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
  definition:
    clientId: my-app
    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 KeyResolves ToDescription
browserFlowAliasbrowserBrowser authentication flow
directGrantFlowAliasdirect_grantDirect 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
  definition:
    clientId: my-app
    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

Common properties from Keycloak ClientRepresentation:

PropertyTypeDescription
clientIdstringClient identifier (required)
namestringDisplay name
enabledbooleanWhether client is enabled
publicClientbooleanPublic or confidential client
standardFlowEnabledbooleanEnable Authorization Code flow
directAccessGrantsEnabledbooleanEnable Resource Owner Password flow
serviceAccountsEnabledbooleanEnable service account
redirectUrisstring[]Valid redirect URIs
webOriginsstring[]Allowed CORS origins
rootUrlstringRoot URL for relative URIs

Short Names

AliasFull Name
kcckeycloakclients
kubectl get kcc

KeycloakClientScope

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
  definition:
    name: my-scope
    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
  definition:
    name: profile-extended
    description: Extended profile information
    protocol: openid-connect

Scope with Protocol Mappers

apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakClientScope
metadata:
  name: department-scope
spec:
  realmRef:
    name: my-realm
  definition:
    name: department
    description: Department information
    protocol: openid-connect
    protocolMappers:
      - name: department
        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

PropertyTypeDescription
namestringScope name (required)
descriptionstringDescription
protocolstringProtocol (openid-connect, saml)
protocolMappersarrayProtocol mapper configurations
attributesmapAdditional attributes

Short Names

AliasFull Name
kccskeycloakclientscopes
kubectl get kccs

KeycloakProtocolMapper

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
  
  # Required: Mapper definition
  definition:
    name: department
    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
  definition:
    name: department
    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
  definition:
    name: groups
    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:

ReferenceUse Case
clientRefMapper applies to a specific client only
clientScopeRefMapper 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:

FieldTypeDescription
namestringMapper name (required)
protocolstringProtocol (usually “openid-connect” or “saml”)
protocolMapperstringMapper type (see common types below)
consentRequiredbooleanWhether user consent is required
configobjectMapper-specific configuration

Common Protocol Mapper Types

OpenID Connect

Mapper TypeDescription
oidc-usermodel-attribute-mapperMaps user attribute to token claim
oidc-usermodel-property-mapperMaps user property to token claim
oidc-group-membership-mapperIncludes group membership in token
oidc-role-name-mapperMaps role names
oidc-hardcoded-claim-mapperAdds hardcoded claim
oidc-audience-mapperAdds audience to token
oidc-full-name-mapperMaps full name

SAML

Mapper TypeDescription
saml-user-attribute-mapperMaps user attribute
saml-group-membership-mapperMaps group membership
saml-role-list-mapperMaps roles

Short Names

AliasFull Name
kcpmkeycloakprotocolmappers
kubectl get kcpm

Notes

  • Mapper names must be unique within the client or client scope
  • The config values are all strings (including boolean values like “true”/“false”)
  • Changes to mappers affect all tokens issued after the change

KeycloakUser

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
  definition:
    username: johndoe
    email: john.doe@example.com
    firstName: John
    lastName: Doe
    enabled: true
    # ... any other Keycloak user properties
  
  # Optional: Initial password (only set on creation)
  initialPassword:
    value: "temporary-password"
    temporary: true  # User must change on first login
  
  # Optional: Password configuration via Kubernetes Secret
  userSecret:
    secretName: john-doe-password
    usernameKey: username   # Default: username
    passwordKey: password   # Default: password
    generatePassword: true  # Auto-generate a password

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
  definition:
    username: admin
    email: admin@example.com
    firstName: Admin
    lastName: User
    enabled: true
    emailVerified: true

User with Credentials

First, create a secret with the password:

apiVersion: v1
kind: Secret
metadata:
  name: john-password
type: Opaque
stringData:
  password: "secure-password-123"

Then create the user:

apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakUser
metadata:
  name: john-doe
spec:
  realmRef:
    name: my-realm
  definition:
    username: johndoe
    email: john.doe@example.com
    firstName: John
    lastName: Doe
    enabled: true
    emailVerified: true
  userSecret:
    secretName: john-password

User with Attributes

apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakUser
metadata:
  name: employee
spec:
  realmRef:
    name: my-realm
  definition:
    username: jsmith
    email: jsmith@company.com
    firstName: Jane
    lastName: Smith
    enabled: true
    attributes:
      department:
        - Engineering
      employee_id:
        - "12345"
      manager:
        - "jdoe"

User with Groups

apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakUser
metadata:
  name: developer
spec:
  realmRef:
    name: my-realm
  definition:
    username: developer1
    email: dev@example.com
    enabled: true
    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 Role Mapping

Combine KeycloakUser (via clientRef) with KeycloakRoleMapping to assign roles to a service account:

# First, define the service account user
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakUser
metadata:
  name: my-service-sa
spec:
  clientRef:
    name: my-service-client
---
# Assign a realm role to the service account
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRoleMapping
metadata:
  name: my-service-sa-admin
spec:
  subject:
    userRef:
      name: my-service-sa
  role:
    name: admin
---
# Assign a client role to the service account
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRoleMapping
metadata:
  name: my-service-sa-manage-users
spec:
  subject:
    userRef:
      name: my-service-sa
  role:
    name: manage-users
    clientRef:
      name: realm-management

Definition Properties

Common properties from Keycloak UserRepresentation:

PropertyTypeDescription
usernamestringUsername (required)
emailstringEmail address
firstNamestringFirst name
lastNamestringLast name
enabledbooleanWhether user is enabled
emailVerifiedbooleanEmail verified flag
attributesmapCustom user attributes
groupsstring[]Group paths to join
requiredActionsstring[]Required actions on login

Short Names

AliasFull Name
kcukeycloakusers
kubectl get kcu

Parent Reference

A KeycloakUser can belong to one of three parent types:

ReferenceUse CaseParent Type
realmRefRegular realm usersKeycloakRealm
clusterRealmRefUsers in cluster-scoped realmsClusterKeycloakRealm
clientRefService account usersKeycloakClient

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 KeycloakRoleMapping to 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

  • Passwords are only set on user creation
  • To update a password, delete and recreate the user, or use Keycloak’s admin console
  • Group memberships specified in groups are resolved by path
  • 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

FieldTypeDescriptionRequired
userRefResourceRefReference to the KeycloakUser resourceYes
userSecret.secretNamestringName of the Kubernetes SecretYes
userSecret.createbooleanCreate secret if it doesn’t existNo (default: false)
userSecret.usernameKeystringKey in secret for usernameNo (default: “username”)
userSecret.passwordKeystringKey in secret for passwordNo (default: “password”)
userSecret.emailKeystringKey in secret for emailNo
userSecret.passwordPolicy.lengthintLength of generated passwordNo (default: 24)
userSecret.passwordPolicy.includeNumbersbooleanInclude numbers in passwordNo (default: true)
userSecret.passwordPolicy.includeSymbolsbooleanInclude symbols in passwordNo (default: true)

Status

FieldTypeDescription
readybooleanWhether the credential is synced
statusstringCurrent status (Synced, Error, SecretError)
messagestringAdditional status information
resourcePathstringKeycloak API path for the user
secretCreatedbooleanWhether the secret was created by the operator
passwordHashstringHash of the last synchronized password
secretResourceVersionstringResource version of the secret when last synced
instanceobjectResolved instance reference
realmobjectResolved realm reference
observedGenerationintegerLast observed generation
conditions[]ConditionKubernetes conditions

Behavior

Secret Creation

When create: true is set:

  1. The operator creates a new Secret if it doesn’t exist
  2. A password is generated according to the password policy
  3. The username is set to match the Keycloak user’s username

Password Sync

When the Secret exists (created or pre-existing):

  1. The operator reads the password from the Secret
  2. The password is set in Keycloak for the referenced user
  3. The passwordHash is updated for change detection

Cleanup

When the KeycloakUserCredential is deleted:

  • If secretCreated: true in 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
  definition:
    username: new-user
    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

A KeycloakGroup represents a group within a Keycloak realm.

Specification

apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakGroup
metadata:
  name: my-group
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 parent group (for nested groups)
  parentGroupRef:
    name: parent-group
  
  # Required: Group definition
  definition:
    name: my-group
    # ... 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
  definition:
    name: developers

Group with Attributes

apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakGroup
metadata:
  name: engineering
spec:
  realmRef:
    name: my-realm
  definition:
    name: engineering
    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
  definition:
    name: organization

Then create child groups:

apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakGroup
metadata:
  name: team-alpha
spec:
  realmRef:
    name: my-realm
  parentGroupRef:
    name: organization
  definition:
    name: team-alpha

Definition Properties

PropertyTypeDescription
namestringGroup name (required)
pathstringFull group path (auto-generated)
attributesmapCustom group attributes
realmRolesstring[]Realm roles assigned to group
clientRolesmapClient roles assigned to group

Short Names

AliasFull Name
kcgkeycloakgroups
kubectl get kcg

KeycloakRole

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:
  # One of realmRef, clusterRealmRef, or clientRef must be specified
  
  # For realm roles:
  realmRef:
    name: my-realm
  
  # For client roles:
  # clientRef:
  #   name: my-client
  
  # Required: Role definition (Keycloak RoleRepresentation)
  definition:
    name: admin-role
    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
  definition:
    name: admin-role
    description: Administrator role with full access
    composite: false

Client Role

apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRole
metadata:
  name: my-client-role
  namespace: keycloak
spec:
  clientRef:
    name: my-client
  definition:
    name: editor
    description: Can edit resources

Parent Reference

A KeycloakRole can belong to one of three parent types:

ReferenceScopeUse Case
realmRefRealm roleShared across all clients in the realm
clusterRealmRefRealm roleFor cluster-scoped realms
clientRefClient roleSpecific to a single client

Note: Exactly one of these must be specified.

Definition Properties

The definition field accepts any valid Keycloak RoleRepresentation:

FieldTypeDescription
namestringRole name (required)
descriptionstringRole description
compositebooleanWhether this is a composite role
clientRolebooleanWhether this is a client role
containerIdstringContainer ID (realm or client ID)
attributesobjectCustom attributes

Status Fields

FieldTypeDescription
readybooleanWhether the role is synchronized
statusstringCurrent status (e.g., “Ready”, “Error”)
messagestringHuman-readable status message
resourcePathstringKeycloak API path for this role
roleIDstringKeycloak internal role ID
roleNamestringThe role name in Keycloak
isClientRolebooleanWhether this is a client role
clientIDstringClient ID (for client roles)
instanceobjectResolved instance reference
realmobjectResolved realm reference
observedGenerationintegerLast observed generation
conditions[]ConditionKubernetes conditions

Short Names

AliasFull Name
kcrkeycloakroles
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 or groups.

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

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

Spec

FieldTypeDescriptionRequired
subject.userRefResourceRefReference to KeycloakUserEither userRef or groupRef
subject.groupRefResourceRefReference to KeycloakGroupEither userRef or groupRef
roleRefResourceRefReference to KeycloakRole resourceEither roleRef or role
role.namestringKeycloak role name (inline)Either roleRef or role
role.clientRefResourceRefReference to KeycloakClient for client roles (within inline role)No (realm role if omitted)
role.clientIdstringClient ID for client roles (alternative to clientRef)No

Status

FieldTypeDescription
readybooleanWhether the mapping is synced
statusstringCurrent status (Synced, Error, SubjectError, RoleError)
messagestringAdditional status information
resourcePathstringKeycloak API path for this role mapping
subjectTypestringSubject type (“user” or “group”)
subjectIDstringKeycloak ID of the user/group
roleNamestringResolved role name
roleTypestringRole type (“realm” or “client”)
instanceobjectResolved instance reference
realmobjectResolved realm reference
observedGenerationintegerLast observed generation
conditions[]ConditionKubernetes conditions

Behavior

Role Resolution

Using roleRef:

  1. The operator looks up the referenced KeycloakRole resource
  2. It reads status.roleName from that resource (which may differ from the CR name)
  3. If the referenced KeycloakRole has its own spec.clientRef, the mapping is automatically scoped to that client and the client’s UUID is resolved from the KeycloakClient’s status
  4. The referenced role (and the client it points at, if any) must be Ready; otherwise the mapping requeues
  5. This is the recommended approach for roles managed by the operator

Using role.name:

  1. The operator queries Keycloak for a role with the given name
  2. This is useful for built-in roles like offline_access

Mapping Types

SubjectRole sourceResult
userRefinline role without clientRef/clientIdUser realm role mapping
userRefinline role with clientRef or clientIdUser client role mapping
userRefroleRef to a KeycloakRole without clientRefUser realm role mapping
userRefroleRef to a KeycloakRole with clientRefUser client role mapping
groupRefinline role without clientRef/clientIdGroup realm role mapping
groupRefinline role with clientRef or clientIdGroup client role mapping
groupRefroleRef to a KeycloakRole without clientRefGroup realm role mapping
groupRefroleRef to a KeycloakRole with clientRefGroup client role mapping

Cleanup

When the KeycloakRoleMapping is deleted:

  1. The finalizer removes the role mapping from Keycloak
  2. 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
  definition:
    name: admins
---
# Create a role
apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakRole
metadata:
  name: admin-role
spec:
  realmRef:
    name: my-realm
  definition:
    name: admin
    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

  • Only one of userRef or groupRef can be specified
  • Only one of roleRef or role can 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 inline role.name

KeycloakComponent

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
  
  # Required: Component definition
  definition:
    name: corporate-ldap
    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
  definition:
    name: corporate-ldap
    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"
      bindCredential:
        - "secret"
      usersDn:
        - "ou=users,dc=example,dc=com"
      userObjectClasses:
        - "person, organizationalPerson, user"
      editMode:
        - "READ_ONLY"

RSA Key Provider

apiVersion: keycloak.hostzero.com/v1beta1
kind: KeycloakComponent
metadata:
  name: rsa-key
  namespace: keycloak
spec:
  realmRef:
    name: my-realm
  definition:
    name: rsa-generated
    providerId: rsa-generated
    providerType: org.keycloak.keys.KeyProvider
    config:
      priority:
        - "100"
      algorithm:
        - "RS256"

Definition Properties

The definition field accepts any valid Keycloak ComponentRepresentation:

FieldTypeDescription
namestringComponent name (required)
providerIdstringProvider ID (e.g., “ldap”, “rsa-generated”)
providerTypestringProvider type (e.g., “org.keycloak.storage.UserStorageProvider”)
parentIdstringParent component ID (defaults to realm ID)
subTypestringOptional component subtype
configobjectProvider-specific configuration (array of strings per key)

Common Provider Types

Provider TypeUse Case
org.keycloak.storage.UserStorageProviderLDAP, custom user storage
org.keycloak.keys.KeyProviderCryptographic keys (RSA, AES, etc.)
org.keycloak.storage.ldap.mappers.LDAPStorageMapperLDAP attribute mappers

Short Names

AliasFull Name
kccokeycloakcomponents
kubectl get kcco

Notes

  • Component configuration uses arrays of strings for all values
  • LDAP credentials should be managed via Kubernetes Secrets (not directly in the CR)
  • Some components may require specific ordering via priority config

KeycloakIdentityProvider

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
  
  # Required: Identity provider definition
  definition:
    alias: my-idp
    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"
  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
  definition:
    alias: corporate-sso
    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
  definition:
    alias: google
    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
  definition:
    alias: github
    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
  definition:
    alias: saml-idp
    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"

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
  definition:
    alias: corporate-sso
    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.

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
  definition:
    alias: external-issuer
    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:

  1. Enables fine-grained-authz on the identity provider (PUT /identity-provider/instances/{alias}/management/permissions). Keycloak auto-creates a token-exchange scope-permission on the realm-management authz resource server.
  2. Resolves each allowedClients entry to its Keycloak client UUID.
  3. Ensures a Client-type authz policy named hostzero-idp-<alias>-token-exchange exists in realm-management’s authz resource server, with clients set to the resolved UUIDs.
  4. 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:

PropertyTypeDescription
aliasstringUnique alias (required)
displayNamestringDisplay name
providerIdstringProvider type (oidc, saml, google, etc.)
enabledbooleanWhether provider is enabled
trustEmailbooleanTrust email from provider
storeTokenbooleanStore provider tokens
configmapProvider-specific configuration

Short Names

AliasFull Name
kcidpkeycloakidentityproviders
kubectl get kcidp

Notes

  • Use configSecretRef to store sensitive values like clientId and clientSecret in a Kubernetes Secret (see Config from Secret)
  • Consider using syncMode: IMPORT to import users on first login
  • Mappers must be managed via KeycloakIdentityProviderMapper. Embedding mappers inside this CR’s definition is silently ignored by Keycloak on update — the field is only consumed during realm import.

KeycloakIdentityProviderMapper

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

  # Required: mapper definition (Keycloak IdentityProviderMapperRepresentation).
  # `identityProviderAlias` is auto-injected from the parent IdP and does not
  # need to be set here.
  definition:
    name: my-mapper
    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
  definition:
    name: mdm-support-role-mapper
    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
  definition:
    name: source-attribute
    identityProviderMapper: hardcoded-attribute-idp-mapper
    config:
      syncMode: INHERIT
      attribute: source
      attribute.value: oidc

Parent Reference

FieldDescription
identityProviderRef.nameName 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:

FieldTypeDescription
namestringMapper name (defaults to metadata.name if omitted)
identityProviderMapperstringMapper type (see below)
identityProviderAliasstringAuto-injected from the parent IdP; setting it manually is overridden
configobjectMapper-specific configuration (all values are strings)

Common Identity Provider Mapper Types

Mapper TypeDescription
oidc-role-idp-mapperGrants a Keycloak role when a claim has a specific value
oidc-username-idp-mapperSets the Keycloak username from a claim
oidc-user-attribute-idp-mapperMaps a claim to a user attribute
oidc-advanced-role-idp-mapperAdvanced role mapping with claim conditions
hardcoded-role-idp-mapperAlways grants a role
hardcoded-attribute-idp-mapperAlways sets a user attribute
oidc-hardcoded-user-session-attribute-idp-mapperAdds a session note
saml-role-idp-mapperSAML equivalent of role mapping
saml-user-attribute-idp-mapperSAML attribute → user attribute

Short Names

AliasFull Name
kcidpmkeycloakidentityprovidermappers
kubectl get kcidpm

Notes

  • Mapper names must be unique within an identity provider.
  • All config values are strings (including boolean values like "true"/"false").
  • The syncMode config key controls when the mapper runs: IMPORT (only on first login), FORCE (every login), or INHERIT (use the IdP’s own setting).
  • Mappers embedded in the definition of KeycloakRealm or KeycloakIdentityProvider are 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

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)
  definition:
    name: ACME Corporation
    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
  definition:
    name: My Organization
    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
  definition:
    name: Example Organization
    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
  definition:
    name: Enterprise Organization
    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
  definition:
    name: Global Organization
    enabled: true

Definition Properties

Common properties from Keycloak OrganizationRepresentation:

PropertyTypeDescription
namestringOrganization name (required)
aliasstringURL-friendly identifier
descriptionstringDescription of the organization
enabledbooleanWhether organization is enabled
domainsarrayAssociated email domains
attributesmapCustom organization attributes

Domain Properties

PropertyTypeDescription
namestringDomain name (e.g., “example.com”)
verifiedbooleanWhether the domain is verified

Short Names

AliasFull Name
kcorgkeycloakorganizations
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.

Notes

  • Organizations are immutable by ID - once created, the id field cannot be changed
  • The alias is 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

ValueWhen to use
basic-flowA regular sequence of authenticator/sub-flow steps. The most common choice.
client-flowUsed for client authentication flows (top-level).
form-flowA 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

FieldTypeRequiredDescription
realmRefobjectone of realmRef / clusterRealmRefReference to a KeycloakRealm
clusterRealmRefobjectone of realmRef / clusterRealmRefReference to a ClusterKeycloakRealm
aliasstringyesUnique flow alias within the realm
descriptionstringnoHuman-readable description
providerIdstringyesTop-level flow type (basic-flow, client-flow, …)
executionsJSON arraynoOrdered list of executions; see Execution shape

Common authenticator provider IDs

Provider IDDescription
auth-cookieCookie-based authentication
auth-spnegoKerberos / SPNEGO
auth-username-password-formUsername/password form
auth-otp-formOTP form
conditional-user-configuredCondition: user has configured the authenticator
direct-grant-validate-usernameValidate username (direct grant)
direct-grant-validate-passwordValidate password (direct grant)
direct-grant-validate-otpValidate OTP (direct grant)
identity-provider-redirectorRedirect to identity provider
registration-user-creationForm action: create user (registration form-flow only)
registration-password-actionForm action: set password (registration form-flow only)
registration-terms-and-conditionsForm action: terms & conditions (registration form-flow only)
registration-recaptchaForm 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 authenticator provider 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
  • 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 requirement differs, it is patched via PUT /authentication/flows/{alias}/executions.
    • leaf authenticatorConfig is converged: created when the spec adds it, deleted when the spec drops it, and updated in place via PUT /authentication/config/{id} when the key/value contents change.
    • sub-flow nodes recurse — children of matched sub-flows are reconciled the same way.
  • Reorder: at the end of each level, the controller assigns explicit priority values to every child via PUT /authentication/flows/{alias}/executions so 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 providerId change (e.g. basic-flowclient-flow): Keycloak does not support swapping the provider type of an existing flow. The status is set to ProviderChangeUnsupported and 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 its KeycloakAuthenticationFlow resource.

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-resource annotation 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 the KeycloakRealm definition. 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 first CreateRealm call, marking the realm Ready, and re-applying them on subsequent reconciles. The realm controller also watches KeycloakAuthenticationFlow resources and requeues the realm immediately when a referenced flow is created, so bindings converge without long retry windows.

KeycloakRequiredAction

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

  # Required: RequiredActionProviderRepresentation
  definition:
    alias: TERMS_AND_CONDITIONS
    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
  definition:
    alias: TERMS_AND_CONDITIONS
    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
  definition:
    alias: CONFIGURE_TOTP
    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
  definition:
    alias: VERIFY_EMAIL
    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
  definition:
    alias: UPDATE_PASSWORD
    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
  definition:
    alias: VERIFY_EMAIL
    name: "Verify Email"
    providerId: VERIFY_EMAIL
    enabled: true
    defaultAction: true

Definition Properties

The definition field accepts any valid Keycloak RequiredActionProviderRepresentation:

FieldTypeDescription
aliasstringUnique alias for the required action (e.g. VERIFY_EMAIL)
namestringDisplay name
providerIdstringProvider ID (usually same as alias)
enabledbooleanWhether the required action is enabled
defaultActionbooleanWhether new users get this action by default
priorityintegerOrdering priority (lower = higher priority)
configmapProvider-specific configuration

Common Required Action Aliases

AliasDescription
UPDATE_PASSWORDForce password update
CONFIGURE_TOTPConfigure OTP authenticator
VERIFY_EMAILVerify email address
UPDATE_PROFILEUpdate user profile
VERIFY_PROFILEVerify user profile
TERMS_AND_CONDITIONSAccept terms and conditions
delete_accountAllow account self-deletion
webauthn-registerRegister WebAuthn security key
webauthn-register-passwordlessRegister WebAuthn passwordless credential
update_user_localeUpdate user locale

Short Names

AliasFull Name
kcrakeycloakrequiredactions
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.
  • Deleting the CR deletes the required action from Keycloak (unless the keycloak.hostzero.com/preserve-resource annotation is set).
  • The priority field 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

MetricTypeLabelsDescription
keycloak_operator_reconcile_totalCountercontroller, resultTotal number of reconciliations per controller
keycloak_operator_reconcile_duration_secondsHistogramcontrollerTime spent in reconciliation
keycloak_operator_reconcile_errors_totalCountercontroller, error_typeTotal errors by controller and type
keycloak_operator_last_reconcile_timestamp_secondsGaugecontrollerTimestamp of last successful reconciliation

Resource Metrics

MetricTypeLabelsDescription
keycloak_operator_resources_managedGaugeresource_type, namespaceNumber of managed resources
keycloak_operator_resources_readyGaugeresource_type, namespaceNumber of resources in ready state

Keycloak Connection Metrics

MetricTypeLabelsDescription
keycloak_operator_keycloak_connection_statusGaugeinstance, namespaceConnection status (1=connected, 0=disconnected)
keycloak_operator_keycloak_api_requests_totalCounterinstance, method, endpoint, statusTotal Keycloak API requests
keycloak_operator_keycloak_api_latency_secondsHistograminstance, method, endpointKeycloak API latency

Controller Metrics

MetricTypeLabelsDescription
keycloak_operator_workqueue_depthGaugecontrollerWork queue depth per controller

Error Types

The error_type label can have the following values:

  • fetch_error - Failed to fetch the Kubernetes resource
  • connection_error - Failed to connect to Keycloak
  • instance_not_ready - Referenced KeycloakInstance is not ready
  • realm_not_ready - Referenced KeycloakRealm is not ready
  • invalid_definition - Invalid resource definition (JSON parsing failed)
  • keycloak_api_error - Keycloak API call failed
  • secret_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:

  1. Overview

    • Total managed resources by type
    • Ready vs non-ready resources
    • Keycloak instance connection status
  2. Reconciliation Performance

    • Reconciliation rate per controller
    • Reconciliation duration (p50, p95, p99)
    • Error rate over time
  3. Errors & Issues

    • Error breakdown by type
    • Recent error spikes
    • Connection failures over time
  4. Keycloak API

    • API request rate by endpoint
    • API latency distribution
    • Error responses by status code

Key Metrics to Watch

MetricNormal RangeAction if Abnormal
Connection status1Check Keycloak availability, credentials
Error rate< 5%Review logs, check Keycloak health
Reconcile duration p99< 10sCheck Keycloak performance
Queue depth< 50Scale operator or reduce resources
Resources not ready0Check 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: {}

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:

ControllerCRDResponsibilities
Instance ControllerKeycloakInstance, ClusterKeycloakInstanceConnection management, health checking
Realm ControllerKeycloakRealm, ClusterKeycloakRealmRealm CRUD, configuration sync
Client ControllerKeycloakClientClient CRUD, secret management
ClientScope ControllerKeycloakClientScopeScope CRUD
ProtocolMapper ControllerKeycloakProtocolMapperToken claim mapper configuration
User ControllerKeycloakUserUser CRUD
UserCredential ControllerKeycloakUserCredentialPassword management
Group ControllerKeycloakGroupGroup CRUD, hierarchy management
Role ControllerKeycloakRoleRealm and client role management
RoleMapping ControllerKeycloakRoleMappingRole-to-subject assignments
IdentityProvider ControllerKeycloakIdentityProviderExternal IDP configuration
Component ControllerKeycloakComponentLDAP, key providers, etc.
Organization ControllerKeycloakOrganizationOrganization 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:

  1. Namespaced realmKeycloakInstance + KeycloakRealm + all child CRDs in the same namespace. Namespace A cannot reach a realm in namespace B.

  2. Cluster realmClusterKeycloakInstance / ClusterKeycloakRealm (cluster-scoped) referenced via clusterInstanceRef / clusterRealmRef from 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:

  1. When a CR is created, a finalizer is added
  2. When a CR is deleted, the controller:
    • Removes the resource from Keycloak
    • Removes the finalizer
  3. 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.enabled Helm 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

ResourcesSync PeriodMax Concurrent Requests
< 505m (default)10 (default)
50-20015-30m10
200-50030m5-10
500+1h5

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

  1. Clone the repository:

    git clone https://github.com/Hostzero-GmbH/keycloak-operator.git
    cd keycloak-operator
    
  2. Install dependencies:

    go mod download
    
  3. 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 kind or go install sigs.k8s.io/kind@latest)
  • kubectl
  • Helm

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:

  1. Configure kubeconfig for your cluster
  2. Install CRDs: make install
  3. Create a KeycloakInstance pointing to your Keycloak
  4. Run locally: make run

Development Commands

CommandDescription
make buildBuild the operator binary
make runRun the operator locally
make installInstall CRDs to cluster
make generateGenerate DeepCopy methods
make manifestsGenerate CRD manifests
make fmtFormat code
make vetRun go vet
make lintRun 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

  1. Set breakpoints in your IDE
  2. Run with debug configuration:
    dlv debug ./cmd/main.go
    

Remote Debugging

For debugging in-cluster:

  1. Build with debug symbols:

    CGO_ENABLED=0 go build -gcflags="all=-N -l" -o manager ./cmd/main.go
    
  2. Use kubectl port-forward to access debugger port

Environment Variables

VariableDescriptionDefault
KUBECONFIGPath to kubeconfig~/.kube/config
KEYCLOAK_URLKeycloak URL for testshttp://localhost:8080
LOG_LEVELLog levelinfo

Testing

The Keycloak Operator has two levels of testing:

  1. Unit Tests: Fast, isolated tests using envtest
  2. 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:

  1. 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)
  2. 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:

  1. Rebuilds the Docker image (layer caching detects source changes automatically)
  2. Removes old images from Kind nodes (avoids containerd tag caching)
  3. Loads the new image into the Kind cluster
  4. 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

TargetDescription
make kind-allCreate cluster and deploy everything
make kind-redeployRebuild and restart operator (fast iteration)
make kind-test-runRun e2e tests (requires port-forward)
make kind-test-run TEST_RUN=TestFooRun specific test(s)
make kind-port-forwardPort-forward Keycloak to localhost:8080
make kind-logsTail operator logs
make kind-resetReset cluster to clean state

E2E Test Configuration

VariableDescriptionDefault
USE_EXISTING_CLUSTERSet to true to use current kubeconfigfalse
KEYCLOAK_INSTANCE_NAMEName of existing KeycloakInstance to use(creates new)
KEYCLOAK_INSTANCE_NAMESPACENamespace of existing instancekeycloak-operator-e2e
OPERATOR_NAMESPACENamespace where operator is deployedkeycloak-operator
KEYCLOAK_URLURL for test’s direct Keycloak access (via port-forward)http://localhost:8080
KEYCLOAK_INTERNAL_URLURL operator uses to connect (in-cluster)http://keycloak.keycloak.svc.cluster.local
TEST_NAMESPACENamespace for test resourceskeycloak-operator-e2e
KEEP_TEST_NAMESPACEDon’t delete namespace after testsfalse

Test Categories

CategoryRequires Port-ForwardDescription
Basic CRUDNoCreate, update, delete resources via Kubernetes API
Status verificationNoVerify .status.ready and conditions
Drift detectionYesTests that modify Keycloak directly and verify reconciliation
Cleanup verificationYesTests that verify resources are deleted from Keycloak
Edge casesMixedSome 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 if USE_EXISTING_CLUSTER is not set
  • skipIfNoKeycloakAccess(t): Skip test if direct Keycloak access (port-forward) is unavailable
  • getInternalKeycloakClient(t): Create authenticated Keycloak client for direct API access
  • rawJSON(s string): Create runtime.RawExtension from JSON string
  • canConnectToKeycloak(): 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

  1. Fork the repository

  2. Create a feature branch:

    git checkout -b feature/my-feature
    
  3. Make your changes following the code style

  4. Add tests for new functionality

  5. Run checks:

    make fmt
    make vet
    make lint
    make test
    
  6. Commit with a clear message:

    git commit -m "feat: add support for X"
    
  7. Push and create a Pull Request

Commit Messages

Follow Conventional Commits:

  • feat: New feature
  • fix: Bug fix
  • docs: Documentation only
  • refactor: Code change without feature/fix
  • test: Adding tests
  • chore: Maintenance

Code Style

  • Follow standard Go conventions
  • Use gofmt and golangci-lint
  • Add comments for exported types/functions
  • Keep functions focused and small

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

  1. Ensure all tests pass
  2. Update documentation if needed
  3. Request review from maintainers
  4. Address feedback
  5. 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.