This document describes how to deploy and remove the Keycloak OCM component on a Kubernetes cluster, starting from the component in an OCI registry.
The Keycloak solution is distributed as a signed OCM component archive via an OCI registry. The component bundles the declared runtime dependency images, including the Keycloak Operator image reference, plus Kubernetes manifests, CRDs, RGD, chart, and SBOM required by the current deployment model. No external network access should be needed at deploy time after all required images and manifests have been mirrored internally.
Deployment happens in three phases:
1. Retrieve 2. Cluster Setup (once) 3. Per Instance
──────────────── ────────────────────── ──────────────────
Download component CloudNativePG Operator --> Namespace
Verify signature Prometheus Operator PostgreSQL Cluster
Extract resources (optional monitoring) Keycloak Application
Keycloak Operator
- Kubernetes cluster 1.28+
kubectlwith cluster-admin permissions- Helm 3+ (for the Keycloak Operator)
- OCM CLI installed (ocm.software)
- Access credentials for the OCI registry hosting the component
The component is published to an OCI registry by the CI/CD pipeline (see CICD.md). On the target environment, download and inspect it using the OCM CLI.
Air-Gapped Deployment: If deploying into a disconnected/air-gapped environment, use
scripts/ocm/ocm-transfer.shorocm transfer component --copy-resourcesto mirror the entire bundle—including all referenced OCI images and Helm charts—to your internal registry.ocm transfer component --copy-resources \ ghcr.io/opendefensecloud//opendefense.cloud/keycloak-bundle:0.3.1 \ your-internal-registry.local/mirrorAfterward, target your internal registry for the subsequent steps.
COMPONENT=opendefense.cloud/keycloak-bundle
VERSION=0.3.1
REGISTRY=ghcr.io/opendefensecloud
ocm get componentversions "$REGISTRY//$COMPONENT:$VERSION"
ocm get resources "$REGISTRY//$COMPONENT:$VERSION"The component contains:
| Resource | Type | Description |
|---|---|---|
keycloak-image |
ociImage | Keycloak server |
busybox-image |
ociImage | wait-for-db initContainer used by the standalone Keycloak Deployment |
postgres-image |
ociImage | PostgreSQL database |
cnpg-operator-image |
ociImage | CloudNativePG operator |
prometheus-operator-image |
ociImage | Prometheus Operator for optional monitoring |
keycloak-config-cli-image |
ociImage | Declarative Keycloak import engine used by the operator Jobs |
keycloak-operator-image |
ociImage | Keycloak reconciliation operator image reference injected by CI or release tooling |
keycloak-operator |
helmChart | Helm chart for the Keycloak operator (includes all seven CRDs) |
keycloak-instance-rgd |
blueprint | KRO ResourceGraphDefinition for one-CR stack instantiation |
keycloak-bundle-sbom |
sbom | CycloneDX SBOM for offline vulnerability and license analysis |
manifests |
directory | Kubernetes manifests (PostgreSQL cluster, Keycloak deployment, monitoring) |
The Keycloak Operator image is represented as the keycloak-operator-image OCM resource.
CI sets OPERATOR_IMAGE_REF to the immutable sha-<commit-sha> image produced by the same
run. For manually assembled releases, set OPERATOR_IMAGE_REF to the mirrored digest-pinned
operator image before running scripts/ocm/ocm-create.sh.
Signature verification is mandatory before any installation step.
Use the repository-published public key:
security/ocm-signing-public-key.pub
Compute its SHA256 locally:
sha256sum security/ocm-signing-public-key.pubocm verify componentversions \
--signature keycloak-bundle-sig \
--public-key security/ocm-signing-public-key.pub \
"$REGISTRY//$COMPONENT:$VERSION"If you deploy from a downloaded CTF archive, verify it first:
./scripts/ocm/ocm-verify.sh \
./keycloak-bundle-ctf.tar.gz \
./security/ocm-signing-public-key.pub \
keycloak-bundle-sigExtract the resources from the component to a local directory:
# Kubernetes manifests (Keycloak, PostgreSQL, monitoring)
ocm download resources "$REGISTRY//$COMPONENT:$VERSION" \
manifests -O manifests.tar
tar xf manifests.tar -C manifests/
# KRO ResourceGraphDefinition (for KRO-based one-CR instantiation)
ocm download resources "$REGISTRY//$COMPONENT:$VERSION" \
keycloak-instance-rgd -O keycloak-instance-rgd.yaml
# Operator Helm chart (includes all seven CRDs)
ocm download resources "$REGISTRY//$COMPONENT:$VERSION" \
keycloak-operator -O operator-chart.tgzIn air-gapped environments, transfer the container images from the component to the cluster-local registry:
ocm transfer componentversions \
"$REGISTRY//$COMPONENT:$VERSION" \
<cluster-local-registry>The CloudNativePG operator manages PostgreSQL clusters declaratively. It is installed once and shared by all Keycloak instances. The operator image is bundled in the component (cnpg-operator-image).
For connected development clusters, the helper script installs the upstream release manifest:
./scripts/deploy/install-cnpg.sh 1.29.1
kubectl wait --for=condition=available --timeout=120s \
deployment/cnpg-controller-manager -n cnpg-systemThis creates the cnpg-system namespace, the CNPG CRDs, and the controller deployment.
For production air-gapped clusters, do not fetch the upstream URL from the target environment. Transfer the signed component to the internal registry, mirror the cnpg-operator-image, and apply an internally approved CNPG manifest that references the mirrored image.
The Prometheus Operator is required to activate the ServiceMonitor, PodMonitor, and
PrometheusRule resources bundled with each instance. Skip this step if Prometheus
Operator is already installed or if monitoring is not needed.
For connected development clusters, the helper script installs the upstream bundle manifest. The operator image is bundled in the component (prometheus-operator-image).
./scripts/deploy/install-prometheus-operator.sh 0.91.0
kubectl wait --for=condition=available --timeout=120s \
deployment/prometheus-operator -n defaultThis registers the monitoring.coreos.com/v1 CRDs (ServiceMonitor, PodMonitor,
PrometheusRule, etc.) and starts the operator deployment in the default namespace.
Production note: For production clusters, deploy Prometheus Operator from an internally approved bundle/image mirror. Consider deploying the full kube-prometheus-stack Helm chart, which adds Prometheus, Alertmanager, and Grafana alongside the operator.
Once the operator is running, deploy the monitoring manifests into each instance namespace:
NAMESPACE=identity-<name>
kubectl -n "$NAMESPACE" apply -f manifests/monitoring/keycloak-service-monitor.yaml
kubectl -n "$NAMESPACE" apply -f manifests/monitoring/cnpg-pod-monitor.yaml
kubectl -n "$NAMESPACE" apply -f manifests/monitoring/keycloak-prometheus-rules.yamlWhy per-namespace? The monitoring resources are namespace-scoped and belong to the
instance they observe. This is consistent with the namespace-per-instance isolation
model (see ARCHITECTURE.md): each ServiceMonitor selects only pods
within its own namespace; there are no cluster-wide selectors. Prometheus must therefore
be configured to discover ServiceMonitor and PodMonitor resources across all
instance namespaces selected by your namespace selector — see OBSERVABILITY.md for details.
See OBSERVABILITY.md for scraping verification and alert tuning.
With KRO installed on the cluster, a single custom resource creates the entire stack — namespace, PostgreSQL cluster, Keycloak server, and operator — in the correct dependency order. Install the KRO RGD once per cluster:
kubectl apply -f kro/rgd/keycloak-instance-rgd.yamlProvision the bootstrap admin Secret in the target namespace through your approved secret-management workflow before the Keycloak pods start. For a local non-production test, create a namespace and Secret manually:
kubectl create namespace identity-dev
kubectl create secret generic keycloak-admin \
-n identity-dev \
--from-literal=KEYCLOAK_ADMIN=admin \
--from-literal=KEYCLOAK_ADMIN_PASSWORD='<rendered-secret-value>'Then create an instance by applying a KeycloakInstance CR that references the Secret
name and keys, but never contains the password value:
apiVersion: kro.run/v1alpha1
kind: KeycloakInstance
metadata:
name: dev
spec:
namespace: identity-dev # administrator chooses the exact namespace name
replicas: 1 # Keycloak replica count (set ≥2 for HA)
dbInstances: 1 # PostgreSQL instances (set 3 for HA)
dbStorageSize: 5Gi
adminSecretName: keycloak-admin
adminUserSecretKey: KEYCLOAK_ADMIN
adminPasswordSecretKey: KEYCLOAK_ADMIN_PASSWORDCredential warning: the KRO schema does not accept an admin password value. It references an existing Secret through
adminSecretName,adminUserSecretKey, andadminPasswordSecretKey. Do not commit rendered production Secrets in Git. For defense or other high-assurance environments, provision this Secret via an external secret manager or protected rendering workflow and rotate/remove the bootstrap admin after setup.
kubectl apply -f keycloak-instance.yaml
# Watch progress
kubectl get keycloakinstance dev
kubectl get pods -n identity-devDeleting the KeycloakInstance CR removes the resources managed by KRO, including the namespace, PostgreSQL cluster, Keycloak Deployment, and operator resources. Externally provisioned Secrets may be controlled by your secret-management workflow and should be handled according to that ownership model. The operator itself (CRDs) must be removed separately.
Note: KRO must be installed on the cluster before the RGD is applied. See KRO installation.
To verify the KRO deployment path without waiting for a full Keycloak health check, run the materialization test against a cluster with KRO and CloudNativePG CRDs installed:
scripts/tests/test-kro-deployment.shThe test applies the RGD, creates a minimal KeycloakInstance, waits for the
KRO-managed Namespace, CNPG Cluster, Deployments, Service, ServiceAccounts, Role,
RoleBinding, and PodDisruptionBudget, and fails if rendered resources still contain
literal template delimiters such as {{ ... }}.
Each instance lives in its own administrator-controlled namespace, providing isolation for data, configuration, network, and RBAC. The namespace name is chosen by the administrator — no prefix is enforced. Repeat the following steps for each instance.
NAMESPACE="identity-poc" # administrator chooses the exact namespace name
kubectl create namespace "$NAMESPACE"kubectl apply -n "$NAMESPACE" -f manifests/postgres/Wait for the primary pod:
kubectl wait pod -n "$NAMESPACE" \
-l cnpg.io/cluster=keycloak-db,cnpg.io/instanceRole=primary \
--for=condition=Ready --timeout=600sCNPG creates a keycloak-db-app secret with auto-generated database credentials and a keycloak-db-rw service pointing to the primary.
kubectl apply -n "$NAMESPACE" -f manifests/keycloak/This deploys the Keycloak server and its ClusterIP service on port 8080. The deployment expects a keycloak-admin secret for bootstrap credentials; the helper script scripts/deploy/deploy-keycloak.sh creates it automatically when missing (using KEYCLOAK_ADMIN_USERNAME/KEYCLOAK_ADMIN_PASSWORD or a generated random password).
Note: In production, provision
keycloak-adminout-of-band via your secret management flow and rotate credentials regularly.
Wait for Keycloak:
kubectl wait -n "$NAMESPACE" --for=condition=ready pod \
-l app=keycloak --timeout=300sIf the helper script generated a random password, you can retrieve it from the Kubernetes secret via:
kubectl get secret keycloak-admin -n "$NAMESPACE" -o jsonpath='{.data.KEYCLOAK_ADMIN_PASSWORD}' | base64 -dkubectl port-forward -n "$NAMESPACE" svc/keycloak 8080:8080Open http://localhost:8080 and log in with the admin credentials.
The Keycloak Operator enables declarative management for all seven Keycloak CRDs. It must run in the same namespace as the Keycloak instance it manages. The chart sets WATCH_NAMESPACE to the operator pod namespace, so installing one central release in keycloak-operator will only watch that namespace and will not reconcile CRs in instance namespaces.
The Helm chart bundles all seven CRDs (Realm, AuthFlow, ClientScope, Group, Client, User, IdentityProvider) and installs them automatically. The helper script copies the keycloak-admin bootstrap Secret into the chart's default keycloak-admin-creds shape and deploys the operator into the instance namespace:
./scripts/deploy/deploy-operator.sh "$NAMESPACE"When installing the chart manually, first create the operator admin Secret in the instance namespace. The default chart expects keycloak-admin-creds with username and password keys:
ADMIN_USER="$(kubectl get secret keycloak-admin -n "$NAMESPACE" -o jsonpath='{.data.KEYCLOAK_ADMIN}' | base64 -d)"
ADMIN_PASSWORD="$(kubectl get secret keycloak-admin -n "$NAMESPACE" -o jsonpath='{.data.KEYCLOAK_ADMIN_PASSWORD}' | base64 -d)"
kubectl create secret generic keycloak-admin-creds \
--namespace "$NAMESPACE" \
--from-literal=username="$ADMIN_USER" \
--from-literal=password="$ADMIN_PASSWORD"Then install the chart into the same namespace:
helm upgrade --install keycloak-operator \
charts/keycloak-operator \
--namespace "$NAMESPACE" \
--wait --timeout 120sWhen deploying from the OCM component, extract the chart first:
ocm download resources "$REGISTRY//$COMPONENT:$VERSION" \
keycloak-operator -O operator-chart.tgz
helm upgrade --install keycloak-operator operator-chart.tgz \
--namespace "$NAMESPACE" \
--wait --timeout 120sApply any Keycloak CR into the instance namespace. The operator reconciles all seven resource types (realms, auth flows, client scopes, groups, clients, users, identity providers) and syncs state back to Keycloak. See USAGE.md for full examples and field reference.
Deleting the namespace removes all instance resources (PostgreSQL, Keycloak, secrets, PVCs):
kubectl delete namespace <namespace>When the instance was created with KRO, delete the KeycloakInstance CR instead — KRO will remove all resources including the namespace:
kubectl delete keycloakinstance <instance-name>List existing instances:
kubectl get keycloakinstance
kubectl get nsRemove these only after all instances have been deleted:
# Keycloak Operator release for an instance namespace
helm uninstall keycloak-operator -n <namespace>
# Keycloak configuration CRDs (cluster-wide)
kubectl delete crd \
realms.keycloak.opendefense.cloud \
clients.keycloak.opendefense.cloud \
clientscopes.keycloak.opendefense.cloud \
groups.keycloak.opendefense.cloud \
users.keycloak.opendefense.cloud \
authflows.keycloak.opendefense.cloud \
identityproviders.keycloak.opendefense.cloud
# CloudNativePG
kubectl delete -f "<internal-cnpg-1.29.1-manifest.yaml>"Keycloak stuck in init container -- PostgreSQL is not ready. Check the CNPG cluster status:
kubectl get cluster keycloak-db -n "$NAMESPACE"
kubectl logs -n "$NAMESPACE" -l app=keycloak -c wait-for-dbKeycloak pod restarting -- Check for database connectivity or configuration errors:
kubectl logs -n "$NAMESPACE" -l app=keycloak -c keycloakKeycloak Operator not reconciling -- Verify the operator pod is running in the same namespace as the target Keycloak instance and can reach Keycloak:
kubectl logs -n "$NAMESPACE" -l app=keycloak-operatorNamespace stuck in Terminating -- Finalizers or running pods can block deletion:
kubectl get all -n "$NAMESPACE"| Topic | Document |
|---|---|
| Architecture and multi-instance model | ARCHITECTURE.md |
| PostgreSQL and CloudNativePG decision | DATABASE.md |
| Client operator and CRD strategy | CLIENT.md |
| CI/CD pipeline and automated deployment | CICD.md |
| CRD usage and GitOps workflow | USAGE.md |
| Metrics, alerts, and tracing | OBSERVABILITY.md |