> ## Documentation Index
> Fetch the complete documentation index at: https://controlplanecorporation-majid-docs-content-expansion.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Keycloak

> Deploy Keycloak on Control Plane using the Template Catalog. Covers the prerequisite admin credentials secret, clustering, PostgreSQL backing stores, access configuration, and optional database backups.

## Overview

Keycloak is an open-source identity and access management platform that provides single sign-on, OIDC/SAML, user federation, and fine-grained authorization. This template runs clustered Keycloak 26 in production mode with a highly available PostgreSQL backing store by default, delivering zero-downtime restarts and upgrades.

### What Gets Created

* **Stateful Keycloak Workload** — 2 replicas by default, clustered via embedded Infinispan (JGroups JDBC\_PING through the shared database — no extra clustering infrastructure). Setting `replicas: 1` runs a dev mode with clustering fully disabled.
* **Highly Available PostgreSQL Stack** (default) — The [PostgreSQL Highly Available template](/template-catalog/templates/postgres-highly-available) as a subchart: 3 Patroni PostgreSQL replicas, 3 etcd replicas, and an HAProxy leader-routing workload that Keycloak connects through.
* **Single-Instance PostgreSQL Workload** (optional) — The [PostgreSQL template](/template-catalog/templates/postgres) instead, for lighter dev/test deployments.
* **Volume Sets** — Persistent storage for the PostgreSQL (and etcd) data.
* **Backup Cron Workload** (optional) — Created by the backing store subchart when its backup pass-through is enabled.
* **Secrets** — An opaque startup script secret, plus the database credentials secret created by the PostgreSQL subchart. The bootstrap admin credentials are **not** created by this template: you create that dictionary secret yourself before installing and reference it by name (see [Prerequisites](#prerequisites)).
* **Identity & Policy** — An identity bound to the Keycloak workload with a least-privilege policy granting `reveal` access to exactly the secrets it mounts.

All durable state — realms, users, and active sessions — lives in PostgreSQL; the Keycloak tier is stateless on disk.

<Note>
  This template does not create a GVC. You must deploy it into an existing GVC.
</Note>

## Prerequisites

**One dictionary secret must exist before you install.** The bootstrap admin guards a login form on the public endpoint, so its credentials are a prerequisite secret rather than template values — a value would sit in plaintext in the Helm release for the life of the install. The template creates no admin secret of its own.

<Steps>
  <Step title="Create the admin credentials secret">
    A [dictionary secret](/guides/create-secret/dictionary) holding exactly the keys `username` and `password`. Secrets are org-level, so no GVC flag is involved:

    ```bash theme={null}
    cpln secret create-dictionary --name my-keycloak-admin \
      --entry username=admin \
      --entry password="$(openssl rand -hex 24)"
    ```

    Set `admin.secretName` to the name you used.
  </Step>

  <Step title="Read the password back later">
    `-o yaml` is required; without it the command prints the secret's metadata table rather than its contents:

    ```bash theme={null}
    cpln secret reveal my-keycloak-admin -o yaml
    ```
  </Step>
</Steps>

| Key        | What it is                                                        |
| ---------- | ----------------------------------------------------------------- |
| `username` | The temporary bootstrap admin's login name.                       |
| `password` | Its password. The login form it guards is on the public endpoint. |

<Warning>
  **A missing prerequisite secret wedges the install rather than failing it.** The install still exits 0 and reports success, every resource is created, and the workload then never starts. Because the container never ran, `cpln logs` returns **zero lines**, which reads as a broken platform rather than a missing prerequisite.

  The only diagnostic is `status.versions[].message`, which names the missing secret:

  ```bash theme={null}
  cpln workload get-deployments RELEASE_NAME-keycloak --gvc GVC_NAME -o yaml
  ```

  ```text theme={null}
  The secret my-keycloak-admin no longer exists. Workload updates are paused until
  the secret is added or the reference to the secret removed.
  ```

  It is **`get-deployments`** — plain `cpln workload get` has no `versions` key at all. Creating the missing secret clears the wedge on its own, but slowly: recovery has measured between 5.5 and 10.5 minutes across the catalog, so poll rather than giving up. `cpln workload force-redeployment RELEASE_NAME-keycloak --gvc GVC_NAME` shortcuts it to roughly 90 seconds.
</Warning>

If you enable the optional database backups, you need a cloud account and a bucket for the backing store's backup job. The backup configuration is a pass-through to the backing PostgreSQL template — follow the [PostgreSQL Highly Available backup prerequisites](/template-catalog/templates/postgres-highly-available#backup-prerequisites) (default store) or the [PostgreSQL backup prerequisites](/template-catalog/templates/postgres#backup-prerequisites) (dev/test store).

## Installation

To install, follow the instructions for your preferred method:

<CardGroup cols={2}>
  <Card title="UI" href="/template-catalog/install-manage/ui" icon="laptop">
    Browse, install, and manage templates visually
  </Card>

  <Card title="CLI" href="/template-catalog/install-manage/cli" icon="terminal">
    Manage templates from your terminal
  </Card>

  <Card title="Terraform" href="/template-catalog/install-manage/terraform" icon={<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><g fill-rule="evenodd"><path d="M77.941 44.5v36.836L46.324 62.918V26.082zm0 0" fill="#5c4ee5"/><path d="M81.41 81.336l31.633-18.418V26.082L81.41 44.5zm0 0" fill="#4040b2"/><path d="M11.242 42.36L42.86 60.776V23.941L11.242 5.523zm0 0M77.941 85.375L46.324 66.957v36.82l31.617 18.418zm0 0" fill="#5c4ee5"/></g></svg>}>
    Declare templates in your Terraform configurations
  </Card>

  <Card
    title="Pulumi"
    href="/template-catalog/install-manage/pulumi"
    icon={<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" id="Pulumi-Icon--Streamline-Svg-Logos" height="24" width="24">
    <desc>
        Pulumi Icon Streamline Icon: https://streamlinehq.com
    </desc>
    <path fill="#f26e7e" d="M4.683025 13.3318c0.869125 -0.5018 0.870575 -2.1264 0.003225 -3.62865s-2.27504 -2.313275 -3.1441725 -1.811475C0.672945 8.3935 0.6715 10.0181 1.53885 11.52035c0.86735 1.502275 2.27505 2.313275 3.144175 1.81145Zm0.0052 3.2167c0.86735 1.502275 0.865925 3.126875 -0.003225 3.628675 -0.86915 0.5018 -2.2768275 -0.309225 -3.144175 -1.81145 -0.8673525 -1.50225 -0.8659075 -3.126875 0.003225 -3.628675 0.8691325 -0.5018 2.276825 0.309225 3.144175 1.81145Zm5.922875 3.4243c0.86735 1.50225 0.8659 3.126775 -0.003225 3.62875 -0.869125 0.501775 -2.27685 -0.309325 -3.1442 -1.81155 -0.867325 -1.50225 -0.865875 -3.12685 0.00325 -3.628675 0.869125 -0.5018 2.276825 0.309225 3.144175 1.811475Zm-0.001925 -6.845275c0.86735 1.50225 0.8659 3.12685 -0.003225 3.628675 -0.869125 0.5018 -2.276825 -0.309225 -3.144175 -1.811475 -0.86735 -1.50225 -0.8659 -3.12685 0.003225 -3.62865 0.869125 -0.501825 2.276825 0.3092 3.144175 1.81145Z" stroke-width="0.25"></path>
    <path fill="#8a3391" d="M22.45775 11.524125c0.86725 -1.502225 0.865925 -3.12685 -0.003225 -3.62865 -0.869125 -0.501825 -2.276825 0.3092 -3.144175 1.811475 -0.86735 1.50225 -0.8659 3.126825 0.003225 3.62865 0.869125 0.501825 2.276825 -0.3092 3.144175 -1.811475Zm0.000175 3.2151c0.869075 0.5018 0.870625 2.1264 0.003225 3.62865 -0.86735 1.50225 -2.27505 2.313275 -3.144175 1.81145 -0.869125 -0.5018 -0.870575 -2.126425 -0.003225 -3.62865 0.86735 -1.50225 2.27505 -2.313275 3.144175 -1.81145ZM16.536225 18.157875c0.86915 0.501825 0.8706 2.126425 0.00325 3.628675 -0.86735 1.502125 -2.275075 2.313225 -3.1442 1.81145 -0.869125 -0.50175 -0.870575 -2.126425 -0.003225 -3.62865 0.867375 -1.502275 2.27505 -2.3133 3.144175 -1.811475Zm-0.003325 -6.843775c0.869125 0.5018 0.870575 2.126425 0.003225 3.628675s-2.27505 2.313275 -3.1442 1.811475c-0.869125 -0.501825 -0.870575 -2.126425 -0.003225 -3.628675 0.86735 -1.502275 2.27505 -2.313275 3.1442 -1.811475Z" stroke-width="0.25"></path>
    <path fill="#f7bf2a" d="M15.138225 2.06721c0 1.003615 -1.40625 1.817215 -3.14095 1.817215 -1.7347 0 -3.14095 -0.8136 -3.14095 -1.817215C8.856325 1.06359 10.262575 0.25 11.997275 0.25c1.7347 0 3.14095 0.81359 3.14095 1.81721ZM9.2166 5.482375c0 1.003625 -1.40625 1.8172 -3.14095 1.8172 -1.7347 0 -3.14095 -0.813575 -3.14095 -1.8172s1.40625 -1.817225 3.14095 -1.817225c1.7347 0 3.14095 0.8136 3.14095 1.817225Zm8.71005 1.8172c1.7347 0 3.14095 -0.813575 3.14095 -1.8172s-1.40625 -1.817225 -3.14095 -1.817225c-1.7347 0 -3.14095 0.8136 -3.14095 1.817225s1.40625 1.8172 3.14095 1.8172Zm-2.788425 1.605625c0 1.003625 -1.40625 1.8172 -3.14095 1.8172 -1.7347 0 -3.14095 -0.813575 -3.14095 -1.8172 0 -1.0036 1.40625 -1.8172 3.14095 -1.8172 1.7347 0 3.14095 0.8136 3.14095 1.8172Z" stroke-width="0.25"></path>
    </svg>}
  >
    Declare templates in your Pulumi programs
  </Card>
</CardGroup>

## Upgrading From 1.0.x

Version `1.1.0` moved the bootstrap admin login out of Helm values. Earlier versions shipped a username and a working password as values, used exactly as written — a published default guarding a login form on the public endpoint, sitting in the Helm release for the life of the install.

|                            | `1.0.x`                                      | `1.1.0`                                                                                |
| -------------------------- | -------------------------------------------- | -------------------------------------------------------------------------------------- |
| Bootstrap admin login      | `admin.username` and `admin.password` values | `admin.secretName` — a dictionary secret you create, holding `username` and `password` |
| Chart-created admin secret | Held the admin credentials                   | None; the template creates no admin secret at all                                      |
| Database password          | A value (`postgres.credentials.password`)    | Unchanged — still a value, because it is bundled plumbing nobody logs in with          |

<Warning>
  **An upgrade that still carries either removed key is rejected at render, before anything is applied.** Each guard names its replacement, and there is no compatibility fallback — the version bump is the migration path. A real upgrade carrying an old key failed at render, created no Helm revision, and left the running release healthy and untouched:

  ```text theme={null}
  keycloak: admin.password was REMOVED in 1.1.0. Put it in a `dictionary` secret (key: `password`)
  together with `username`, and set admin.secretName to that secret's name. Create the secret BEFORE
  installing; see Prerequisites in the README.
  ```

  Leaving `admin.secretName` empty is refused the same way.
</Warning>

<Note>
  **An existing install's admin password does not change on upgrade.** Keycloak consults `KC_BOOTSTRAP_ADMIN_*` only when no admin account exists yet, so the account already in the database is untouched by whatever the secret holds. Create the secret so the release renders, then change the password in the Keycloak admin console. If the install is still carrying the published `1.0.x` default (`change-me-keycloak-admin`), treat that password as compromised and change it in the console now.
</Note>

To upgrade an existing install:

<Steps>
  <Step title="Create the admin credentials secret">
    Follow [Prerequisites](#prerequisites). Putting your current credentials in it keeps your values file honest, but it does not re-seed the account.
  </Step>

  <Step title="Drop the removed keys from your values">
    Remove `admin.username` and `admin.password`, and set `admin.secretName` instead.
  </Step>

  <Step title="Upgrade">
    Realms, users, and sessions are in PostgreSQL and are untouched. The Keycloak replicas restart — see [Replicas and Clustering](#replicas-and-clustering).
  </Step>
</Steps>

## Configuration

The default `values.yaml` for this template:

```yaml theme={null}
image: quay.io/keycloak/keycloak:26.6.3

# Replica count. 2+ (default) forms an Infinispan cluster over JGroups JDBC_PING
# for zero-downtime restarts. 1 = dev mode: clustering fully disabled (local cache).
replicas: 2

resources: # per replica
  cpu: 1000m
  memory: 2Gi # JVM heap is sized to 70% of this limit; do not set below 1.5Gi
  minCpu: 500m
  minMemory: 1Gi

# ─── Admin Bootstrap ──────────────────────────────────────────────────────────
# REQUIRED PREREQUISITE SECRET — CREATE IT BEFORE YOU INSTALL. A `dictionary`
# secret holding exactly two keys, `username` and `password`; it is the
# temporary admin Keycloak creates on first boot, and its login form sits on the
# public endpoint. If the secret does not exist at install time the deployment
# WEDGES silently — `cpln logs` returns nothing at all. See Prerequisites in the
# README for the exact `cpln secret create-dictionary` command.
admin:
  secretName: my-keycloak-admin

# ─── Backing Store: Highly Available PostgreSQL (default) ────────────────────
# Deploys the postgres-highly-available template: 3 Patroni replicas, 3 etcd
# replicas, and an HAProxy leader-routing endpoint Keycloak connects to.
postgresHA:
  enabled: true
  config:
    # Name of the dictionary secret this chart creates and the bundled
    # postgres-highly-available reads. One secret feeds whichever store
    # is enabled, built from postgres.credentials.* above.
    credentialsSecretName: my-keycloak-db-credentials
  replicas: 3
  volumeset:
    capacity: 10 # initial capacity in GiB per replica (minimum is 10)

  backup: # pass-through to the postgres-highly-available template's backup feature
    enabled: false
    mode: logical # logical or wal-g
    resources:
      cpu: 100m
      memory: 128Mi
    logical:
      image: ghcr.io/controlplane-com/backup-images/postgres-backup:17.1.0
      schedule: "0 2 * * *" # daily at 2am UTC
    walg:
      intervalSeconds: 21600 # every 6 hours
    provider: aws # Options: aws, gcp, or minio
    aws:
      bucket: my-backup-bucket
      region: us-east-1
      cloudAccountName: my-backup-cloudaccount
      policyName: my-backup-policy
      prefix: postgres/backups
    gcp:
      bucket: my-backup-bucket
      cloudAccountName: my-backup-cloudaccount
      prefix: postgres/backups
    minio: # any S3-compatible endpoint, e.g. an internal MinIO template deployment
      endpoint: http://my-minio-workload:9000
      bucket: my-backup-bucket
      accessKey: my-minio-username
      secretKey: my-minio-password
      prefix: postgres/backups

# ─── Backing Store: Single-instance PostgreSQL (dev/test) ────────────────────
# Enable this and disable postgresHA for a lighter non-HA deployment.
postgres:
  enabled: false
  credentials: # the chart writes these into the secret named below — nothing for you to create
    username: keycloak
    password: change-me-keycloak-db # change before installing
    database: keycloak
  config:
    credentialsSecretName: my-keycloak-db-credentials # secret names are org-wide — give each release its own
  volumeset:
    capacity: 10 # initial capacity in GiB (minimum is 10)

  backup: # pass-through to the postgres template's backup feature (Postgres 17+)
    enabled: false
    image: ghcr.io/controlplane-com/backup-images/postgres-backup:18.1.0
    schedule: "0 2 * * *" # daily at 2am UTC
    resources:
      cpu: 100m
      memory: 128Mi
    provider: aws # Options: aws, gcp, or minio
    aws:
      bucket: my-backup-bucket
      region: us-east-1
      cloudAccountName: my-backup-cloudaccount
      policyName: my-backup-policy
      prefix: postgres/backups
    gcp:
      bucket: my-backup-bucket
      cloudAccountName: my-backup-cloudaccount
      prefix: postgres/backups
    minio:
      endpoint: http://my-minio-workload:9000
      bucket: my-backup-bucket
      credentialsSecretName: my-keycloak-minio-credentials # dictionary secret holding accessKey + secretKey
      prefix: postgres/backups

# ─── Access ───────────────────────────────────────────────────────────────────
publicAccess:
  enabled: true # HTTPS via the canonical *.cpln.app endpoint

internalAccess:
  type: same-gvc # options: none, same-gvc, same-org, workload-list
  workloads: [] # note: replicas > 1 requires type != none
  # workloads:
  #   - //gvc/GVC_NAME/workload/WORKLOAD_NAME
```

### Replicas and Clustering

* `replicas` — `2` or more (default) runs Keycloak in a cluster: the replicas form an Infinispan cluster over JGroups JDBC\_PING through the shared database, so restarts and upgrades are zero-downtime and in-flight logins and sessions survive a replica restart. `1` runs a dev mode with clustering fully disabled (local cache only).
* Clustered replicas must be able to reach each other on ports 7800/57800, so `replicas > 1` requires `internalAccess.type` other than `none` — the chart enforces this at render.
* Scaling is operator-driven: change `replicas` via a template upgrade. There is deliberately no autoscaling, so cluster membership only changes intentionally.

<Note>
  **A rolling upgrade is not constrained to one replica at a time.** Control Plane silently discards `maxUnavailableReplicas` on stateful workloads, so version `1.1.0` stopped declaring it and now renders only what the platform actually applies — nothing limits how many replicas restart together. Realms, users, and sessions live in PostgreSQL and survive regardless, but plan for a brief gap in availability during an upgrade rather than assuming a strictly serialized rollout.
</Note>

### Keycloak Resources

* `resources` — CPU and memory per replica. The JVM heap is sized to 70% of the memory limit; do not set `memory` below `1.5Gi`.

### Admin Bootstrap

* `admin.secretName` — Name of the dictionary secret you created in [Prerequisites](#prerequisites), holding the `username` and `password` of a temporary bootstrap admin. The credentials never pass through values, and the template creates no secret of its own — it references yours and grants the workload `reveal` on exactly that one secret.
* The account is created on **first boot only**. Keycloak reads those credentials when no admin exists yet, so editing the secret on a running install changes nothing — change the password in the admin console instead.
* After the first login, create a permanent admin account and remove the temporary one — Keycloak warns until you do.

### Backing Store

Exactly one of the two stores must be enabled — the chart enforces this at render.

* `postgresHA` (default) — A highly available PostgreSQL cluster from the [PostgreSQL Highly Available template](/template-catalog/templates/postgres-highly-available): 3 Patroni replicas, 3 etcd replicas, and an HAProxy endpoint that routes Keycloak's connections to the current leader. Do not disable the HA proxy (`postgresHA.proxy.enabled`) — Keycloak writes through the HAProxy leader endpoint, and the chart enforces this at render.
* `postgres` — A single-instance PostgreSQL from the [PostgreSQL template](/template-catalog/templates/postgres), for lighter dev/test deployments. Set `postgresHA.enabled: false` and `postgres.enabled: true`.
* `postgres.credentials.*` / `postgres.credentials.*` — Database credentials and database name. **Change the password before installing.**
* `postgresHA.volumeset.capacity` / `postgres.volumeset.capacity` — Initial volume size in GiB (minimum 10, per replica for the HA store).

<Warning>
  **Template version `1.0.0` did not compact the etcd cluster inside the bundled highly available database**, so etcd's backend grows with time alone and goes read-only once it reaches its 2 GiB quota — after roughly 110 days — taking PostgreSQL failover with it. Only installs running the HA database are affected (`postgresHA.enabled`, the default here); see [etcd History Compaction](/template-catalog/templates/postgres-highly-available#etcd-history-compaction) for the mechanism and the symptoms. Upgrade to `1.0.1` or later to turn compaction on: that stops further growth but cannot shrink a backend that has already grown, and a cluster that has already raised a `NOSPACE` alarm needs operator recovery rather than an upgrade.
</Warning>

### Access

* `publicAccess.enabled` — Exposes Keycloak over HTTPS at the automatically assigned canonical `*.cpln.app` endpoint. It is **deliberately on by default**: Keycloak is the identity provider your applications and their users authenticate against, so browsers must reach its login and OIDC endpoints for SSO to work at all, and the admin login is now a credential you created rather than a published default. Disable it only for pure service-to-service deployments; internal access keeps working. A firewall change takes roughly 30 seconds to a few minutes to propagate, so re-test rather than trusting the first response.
* `internalAccess.type` — Controls which workloads can reach Keycloak inside Control Plane (`none`, `same-gvc`, `same-org`, or `workload-list`). With `workload-list`, list the allowed workloads in `internalAccess.workloads`.

### Backup

The `postgresHA.backup` and `postgres.backup` sections are pass-throughs to the backup feature of the corresponding backing store template. Set `backup.enabled: true` and fill in the provider section (`aws`, `gcp`, or `minio`) to schedule database backups to object storage.

For provider setup, backup modes, and restore procedures, see the backing store's documentation:

* Default HA store: [PostgreSQL Highly Available — Backup](/template-catalog/templates/postgres-highly-available#backup)
* Dev/test store: [PostgreSQL — Backup](/template-catalog/templates/postgres#backup)

## Connecting

| What              | Value                                                                                                          |
| ----------------- | -------------------------------------------------------------------------------------------------------------- |
| Public URL        | `status.canonicalEndpoint` from `cpln workload get {release}-keycloak -o yaml`                                 |
| Admin console     | `https://{canonical-endpoint}/admin`                                                                           |
| OIDC discovery    | `https://{canonical-endpoint}/realms/{realm}/.well-known/openid-configuration`                                 |
| In-GVC (internal) | `http://{release}-keycloak.{gvc}.cpln.local:8080`                                                              |
| Admin credentials | The `username` / `password` in your `admin.secretName` secret — `cpln secret reveal my-keycloak-admin -o yaml` |

<Note>
  On a first install of the default HA stack, the Keycloak container waits for the PostgreSQL cluster to come up (logging `Waiting for PostgreSQL...`) before starting. A default install measured **303 seconds** from install to a ready workload, most of that spent waiting on the Patroni and etcd replicas; the dev-mode store (`postgres` with `replicas: 1`) is ready in about 2 minutes.
</Note>

## Important Notes

* **Create the admin secret before installing** — a missing prerequisite secret leaves the workload waiting on something that does not exist, with zero log lines. See [Prerequisites](#prerequisites) for how to diagnose it.
* **Change the database password before installing** (`postgres.credentials.password`) — it is bundled plumbing, used exactly as given, and it remains a value by design.

If you run more than one release of this template in the same organization, give each its own `postgres.config.credentialsSecretName`. Secret names are organization-wide, so a second release left on the default name is **refused at install** and creates nothing — the first release is unaffected.

* **The bootstrap admin is temporary by design** — log in, create a permanent admin, then remove it. Its credentials are seeded on first boot only; change the password in the admin console, not by editing the secret.
* **Keep `publicAccess` enabled for browser SSO** — end-user browsers must reach Keycloak's login endpoints; disable it only for pure service-to-service deployments.
* **Do not disable the HA proxy** (`postgresHA.proxy.enabled`) — Keycloak connects through the HAProxy leader endpoint for writes; the chart enforces this at render.
* **Scaling is operator-driven** — change `replicas` via a template upgrade; there is deliberately no autoscaling, so cluster membership only changes intentionally.
* **Database volumes survive reinstalls** — uninstalling and reinstalling under the same release name reuses the persisted data unless the volume sets are deleted.

## External References

<CardGroup cols={2}>
  <Card title="Keycloak Documentation" icon="book" href="https://www.keycloak.org/documentation">
    Official Keycloak documentation
  </Card>

  <Card title="Server Configuration Reference" icon="gear" href="https://www.keycloak.org/server/all-config">
    All Keycloak server configuration options
  </Card>

  <Card title="Caching and Clustering" icon="server" href="https://www.keycloak.org/server/caching">
    How Keycloak's Infinispan caching and clustering work
  </Card>

  <Card title="Cloud Accounts" icon="cloud" href="https://docs.controlplane.com/guides/create-cloud-account">
    Create a Control Plane Cloud Account for backup storage access
  </Card>

  <Card title="Keycloak Template" icon="github" href="https://github.com/controlplane-com/templates/tree/main/keycloak">
    View the source files, default values, and chart definition
  </Card>
</CardGroup>
