Skip to content

KeycloakInstance Guide

KeycloakInstance is the primary workload resource — one CR describes one Keycloak deployment. This guide covers each part of the spec that needs more than a one-line field reference; for the full field-by-field list see the CRD Reference.

Reconcile picture

flowchart LR
    U([kubectl apply]) -->|KeycloakInstance CR| K8s[(Kubernetes API)]
    K8s --> Op[Operator reconcile loop]
    Op -->|1| Val[Validate spec]
    Op -->|2| Ver[Resolve version\nvia KeycloakVersionMap]
    Op -->|3| PG[Provision/check Postgres\nmanaged or BYO]
    Op -->|4| Admin[Ensure admin bootstrap Secret]
    Op -->|5| Render[Render + SSA child resources]
    Op -->|6| Watch[Watch StatefulSet readiness]

Every child object carries a controller owner reference — Kubernetes garbage collection deletes them with the KeycloakInstanceexcept the managed PerconaPGCluster, which is deliberately retained. See Managed backend retention.

Hosts

spec:
  hosts:
    host: auth.example.com          # required
    adminHost: auth-admin.example.com   # optional

spec.hosts.host is structurally required, even when spec.ingress.enabled: false — it always drives KC_HOSTNAME. Omitting adminHost means no admin Ingress is created and KC_HOSTNAME_ADMIN stays unset (Keycloak serves the admin console on the same host as the public one).

Ingress & TLS

spec:
  ingress:
    enabled: true             # default; false = Services only (dev/kind)
    class: nginx              # default; public Ingress class
    adminClass: nginx-internal # default; admin Ingress class
    clusterIssuer: letsencrypt-prod  # empty = no TLS annotation, plain HTTP
    annotations:
      custom.example.com/foo: bar
  • enabled: false renders Services only — no Ingress objects at all. Use this for dev/kind clusters without an ingress controller; reach Keycloak via kubectl port-forward.
  • clusterIssuer is the only switch for TLS. Leaving it empty produces plain-HTTP Ingresses (no cert-manager annotation, no TLS secret). Set it to a ClusterIssuer name already provisioned on the cluster to get a cert-manager-issued certificate on both Ingresses.
  • annotations are merged onto both Ingresses, but the operator's own security-snippet annotation and (if set) the cluster-issuer annotation always take precedence — you cannot override them via annotations.
  • The admin Ingress is only created when spec.hosts.adminHost is set.

Postgres: BYO vs. Managed

BYO (managed: false)

spec:
  postgres:
    managed: false
    credentialsSecret: my-keycloak-pg-credentials
apiVersion: v1
kind: Secret
metadata:
  name: my-keycloak-pg-credentials
stringData:
  host: pg.example.com      # required
  dbname: keycloak          # required
  user: keycloak            # required
  password: <password>      # required
  port: "5432"               # optional; Keycloak defaults to 5432 when absent

These four/five keys are wired directly to KC_DB_URL_HOST / KC_DB_URL_DATABASE / KC_DB_USERNAME / KC_DB_PASSWORD (and KC_DB_URL_PORT when port is present) via secretKeyRef. A Secret missing a required key fails the instance permanently (CredentialsSecretInvalid) rather than starting Keycloak against a broken DB config — if the Secret does not exist yet, the instance instead stays Pending and retries.

Managed (managed: true)

spec:
  postgres:
    managed: true
    topology: ha        # "" (default, 1 replica) or "ha" (3 replicas)
    pgbouncer: true      # enables the PgBouncer proxy sidecar (2 replicas)
    nodes: 0             # 0 = topology default; >0 overrides it

Requires the pgv2.percona.com CRD — see Installation § Percona PostgreSQL Operator. The operator applies a PerconaPGCluster named <name>-pg and waits for Percona to write its generated credentials Secret, <name>-pg-db-credentials — that Secret is wired in place of a BYO Secret automatically; you never create or reference a Secret yourself in this path.

spec.postgres.nodes is managed-only and must be >= 0; setting it with managed: false is rejected at validation.

Admin bootstrap credentials

Keycloak's bootstrap admin username/password always come from a Secret via secretKeyRef (KC_BOOTSTRAP_ADMIN_USERNAME / KC_BOOTSTRAP_ADMIN_PASSWORD) — never inline values.

spec:
  adminSecret: my-keycloak-admin-credentials   # optional
  • Set: the operator uses that pre-existing Secret verbatim (keys username, password); it is not created or garbage-collected by the operator.
  • Empty (the default): the operator generates <name>-admin-credentials (username admin, a random 32-character password) once and never rotates it on subsequent reconciles. It is tracked in status.secrets for finalizer garbage collection.

Providers: theme + SPI JAR injection

Most b'nerd Keycloak instances need custom JARs — a login theme, an SPI authenticator/event-listener, or both. spec.providers is a first-class list; each entry becomes its own initContainer that copies *.jar from path inside the provider image into the shared /opt/keycloak/providers volume mounted into the Keycloak container:

spec:
  providers:
    - name: theme        # initContainer name: provider-theme
      image: registry.bnerd.com/my-org/keycloak-theme:1.4.0
      path: /theme        # dir inside the image holding *.jar; default "/providers"
    - name: audit-spi     # initContainer name: provider-audit-spi
      image: registry.bnerd.com/my-org/keycloak-audit-spi:2.1.0
      # path omitted -> defaults to /providers
  imagePullSecrets:
    - name: registry-bnerd-com   # pod-level; covers the Keycloak image + all providers

Provider names must be unique, non-empty, and DNS-label-safe — they become the provider-<name> initContainer name.

Provider images must run as a numeric non-root user

The pod runs under runAsNonRoot: true, so the kubelet rejects any provider (or Keycloak) image whose configured user is root — the pod never starts. Build provider images with an explicit numeric USER (e.g. USER 1000:1000); a username the kubelet cannot resolve to a non-zero UID is also rejected.

Each provider initContainer copies *.jar from path into the shared providers volume, which it mounts internally at /bnerd-providers (a distinct path from the in-image path, so the empty volume never shadows the image's JARs); the Keycloak container sees the result at /opt/keycloak/providers.

See examples/keycloakinstance-full.yaml for a full theme + SPI example.

Realm Import

spec:
  realmImport:
    secretName: my-keycloak-realm-import

The named Secret's contents are mounted read-only at /opt/keycloak/data/import; its presence adds --import-realm to the start command. Omit realmImport entirely to skip import (the default).

Resources & placement

spec:
  resources:
    requests:
      cpu: 500m
      memory: 1700Mi
    limits:
      cpu: 2000m
      memory: 2000Mi
  placement:
    nodeSelector:
      dedicated: my-keycloak
    tolerations:
      - key: dedicated
        operator: Equal
        value: my-keycloak
        effect: NoSchedule
  env:
    - name: KC_LOG_LEVEL
      value: DEBUG

spec.resources defaults to the reference envelope shown above if omitted. spec.env is a raw escape hatch merged last onto the container env — it can override any operator-set environment variable by name; use it sparingly.

Optional metrics

spec:
  metrics:
    serviceMonitor: true

Creates a Prometheus Operator ServiceMonitor (<name>) scraping the client Service's management port at /metrics every 30s. Capability-gated on the monitoring.coreos.com ServiceMonitor CRD: if absent, the operator sets MetricsExporterReady=False/ServiceMonitorCRDMissing and continues — it does not fail the instance. Toggling back to false removes the ServiceMonitor.

Child resource names

For a KeycloakInstance named <name>:

Object Name
StatefulSet <name>
Client Service (:8080) <name>
Headless discovery Service (JGroups DNS_PING) <name>-discovery
Public Ingress <name>
Admin Ingress (only when spec.hosts.adminHost is set) <name>-admin
PodDisruptionBudget (only when desired replicas >= 2, not while draining) <name>
ServiceMonitor (only when spec.metrics.serviceMonitor: true + CRD present) <name>
Generated admin bootstrap Secret (only when spec.adminSecret is empty) <name>-admin-credentials
Managed Postgres cluster (only when spec.postgres.managed: true) <name>-pg
Managed Postgres credentials Secret <name>-pg-db-credentials

Managed backend retention

The managed PerconaPGCluster is retained on KeycloakInstance deletion — it carries no owner reference to the KeycloakInstance. Only operator-owned Secrets (tracked in status.secrets, e.g. the generated admin credentials) are garbage-collected by the finalizer. This prevents accidental data loss; remove the PerconaPGCluster manually once you've verified the data is no longer needed.

HA / production hardening

Applied automatically — there is no spec surface to disable these:

  • PodDisruptionBudget <name> (maxUnavailable: 1) when desired replicas >= 2. Removed on scale-to-1 and while a cross-minor upgrade drains, so the drain-to-zero recreate is never blocked.
  • Soft pod anti-affinity spreads replicas across nodes while still scheduling on a single-node cluster.
  • Security context: pod-level runAsNonRoot + seccompProfile: RuntimeDefault; allowPrivilegeEscalation: false + capabilities.drop: [ALL] on the Keycloak container and every provider initContainer. runAsUser is left unset — the image declares its own non-root user, and pinning a UID breaks provider init containers.
  • Explicit RollingUpdate StatefulSet strategy (one-by-one) and terminationGracePeriodSeconds: 60.
  • Events on every status.phase transition (reason: Phase<NewPhase>).

Reconcile phases

status.phase moves through PendingProvisioningDeployingReady, or Failed on a permanent error (invalid spec, missing Percona CRD, an invalid BYO credentials Secret). A missing or not-yet-installed KeycloakVersionMap, or a version not (yet) present in it, is treated as transient — the instance stays Pending with condition reason VersionResolutionPending and requeues after 30 seconds rather than failing permanently.