---
title: "Keycloak"
description: "Configure Keycloak for deployment"
source: "Guardian Platform"
---
## Overview

Keycloak is an open-source identity and access management platform. Unlike Auth0 and Microsoft Entra ID, Keycloak is self-hosted: you run a Keycloak instance (in the same cluster or externally) and the Guardian Platform authenticates against it.

The platform requires a dedicated Keycloak realm containing three OAuth clients:

- **Frontend client** (`guardian-platform-frontend`) - public SPA client used by Guardian for sign-in
- **Backend client** (`guardian-platform-backend`) - confidential client with a service account, used by the Auth Service for user management
- **Worker client** (`governance-worker`) - confidential machine-to-machine client used by the governance worker

The `keycloak-bootstrap` Helm chart automates all of this: it runs a one-time Kubernetes Job that creates the realm, the three clients, the platform's custom authorization scopes, and an initial `platform-admin` user via the Keycloak Admin REST API. Run it **before** deploying the platform chart - the services need valid client credentials at startup.

## Prerequisites

- A running Keycloak instance, or a cluster where one can be deployed (this guide includes an in-cluster option)
- Keycloak admin credentials for the master realm
- `kubectl` and `helm` access to the deployment namespace
- A clone of the [deployment repository](./prerequisites.mdx#deployment-repository) for the `keycloak-bootstrap` chart

## Helm Configuration

After completing the setup steps below, the platform configuration files will contain the following. `<keycloak-url>` is Keycloak's public base URL, including any path it is served under: with the deployment repository's Keycloak values that is `https://<keycloak-host>/keycloak`. The issuer must match the one Keycloak reports exactly.

`values.yaml`:

```yaml title="values.yaml"
auth-service:
  config:
    idp:
      provider: "keycloak"
      issuer: "<keycloak-url>/realms/guardian" # Verified in step 6
      keycloak:
        realm: "guardian"
        adminUrl: "<keycloak-url>"
        clientId: "guardian-platform-frontend"
        enableUserManagement: true
    tokenExchange:
      enabled: true
      keyId: "auth-service-001" # Identifier for the token exchange key

governance-service:
  config:
    keycloakUrl: "<keycloak-url>"
    keycloakRealm: "guardian"

guardian:
  config:
    keycloakUrl: "<keycloak-url>"
    keycloakRealm: "guardian"
    keycloakClientId: "guardian-platform-frontend"

# Post-install hook: seeds the governance database with the organization
# and the platform-admin user created by the bootstrap
keycloak:
  createOrganization: true
  realmName: "guardian"
  createPlatformAdmin: true
  platformAdminEmail: "admin@<platform-domain>" # Must match users.admin.email in bootstrap-values.yaml
  url: "<keycloak-url>" # Omit when Keycloak is at http://keycloak:8080/keycloak in this namespace
```

The hook signs in to Keycloak as the master realm admin. If the bootstrap uses a non-default admin user or password secret, set `keycloak.adminUsername` and `keycloak.adminPasswordSecret` (`name` and `key`) in `values.yaml` to match.

`secrets.yaml`:

```yaml title="secrets.yaml"
global:
  secrets:
    create: true
    auth:
      provider: "keycloak"
      keycloak:
        secretName: "platform-keycloak"
        values:
          serviceAccountClientId: "guardian-platform-backend"
          serviceAccountClientSecret: "<backend-client-secret>" # Retrieved in step 5
          tokenExchangePrivateKey: | # Generate with: openssl genrsa 2048
            -----BEGIN PRIVATE KEY-----
            ...
            -----END PRIVATE KEY-----
    governanceWorker:
      secretName: "platform-governance-worker"
      values:
        encryptionKey: "GENERATE_WORKER_KEY" # Generate with: openssl rand -base64 32
        clientId: "governance-worker"
        clientSecret: "<worker-client-secret>" # Retrieved in step 5
```

Note: the frontend client ID is configured in `values.yaml`, not `secrets.yaml` - it is a public client with no secret.

## Quick Start (Bootstrap Chart - Recommended)

Deploy or locate Keycloak, run the `keycloak-bootstrap` chart, and copy the client secrets it generates into the platform's chart values, in six steps.

### 1. Deploy or Identify a Keycloak Instance

If you already run Keycloak, note its URL and admin credentials and skip to step 2. Otherwise, deploy Keycloak into the cluster with the Bitnami chart, using this `keycloak-values.yaml`:

```yaml title="keycloak-values.yaml"
auth:
  adminUser: admin
  existingSecret: "keycloak-admin"
  passwordSecretKey: "password"

httpRelativePath: "/keycloak/"
production: true
proxyHeaders: xforwarded # TLS terminates at the ingress

postgresql:
  enabled: true
  auth:
    database: keycloak

ingress:
  enabled: true
  ingressClassName: "nginx"
  hostname: guardian.example.com # The platform domain, or a separate host such as keycloak.example.com
  path: /keycloak
  annotations:
    cert-manager.io/issuer: "letsencrypt-prod"
  tls: true

resources:
  requests:
    cpu: 500m
    memory: 512Mi
  limits:
    cpu: 1000m
    memory: 1Gi
```

```bash
# Keycloak server admin password (master realm)
kubectl create secret generic keycloak-admin \
  --from-literal=password="$(openssl rand -base64 32)" \
  --namespace guardian

helm upgrade --install keycloak oci://registry-1.docker.io/bitnamicharts/keycloak \
  --namespace guardian \
  --values keycloak-values.yaml \
  --wait \
  --timeout 10m
```

Verify Keycloak is healthy before continuing:

```bash
kubectl run curl-test --rm -it --image=curlimages/curl --restart=Never \
  --namespace guardian -- \
  curl -s -o /dev/null -w "%{http_code}" http://keycloak:9000/keycloak/health/ready
```

### 2. Create Pre-Bootstrap Secrets

The bootstrap job authenticates to Keycloak with the `keycloak-admin` secret (created in step 1, or create it now with your external Keycloak's admin password) and sets the initial Guardian admin user's password from `platform-admin`:

```bash
# Guardian admin user password (guardian realm user, not the Keycloak server admin)
kubectl create secret generic platform-admin \
  --from-literal=password="$(openssl rand -base64 32)" \
  --namespace guardian
```

### 3. Prepare the Bootstrap Values

From the deployment repository checkout, start from the example values file:

```bash
cp charts/keycloak-bootstrap/examples/values.yaml bootstrap-values.yaml
```

Edit `bootstrap-values.yaml`, replacing the domain placeholders with the platform domain:

```yaml title="bootstrap-values.yaml"
clients:
  frontend:
    redirectUris:
      - "https://<platform-domain>/*"
    webOrigins:
      - "https://<platform-domain>"
  backend:
    redirectUris:
      - "https://<platform-domain>/authService/*"
    webOrigins:
      - "https://<platform-domain>"

users:
  admin:
    email: "admin@<platform-domain>"
```

If Keycloak is not reachable at the in-cluster default (`http://keycloak:8080/keycloak`), set its URL:

```yaml
keycloak:
  url: "<keycloak-url>" # External or cross-namespace URL
```

### 4. Run the Bootstrap

```bash
helm upgrade --install keycloak-bootstrap ./charts/keycloak-bootstrap \
  --namespace guardian \
  --values bootstrap-values.yaml \
  --wait \
  --timeout 10m
```

The deployment repository also provides a helper script: `./scripts/keycloak/bootstrap-keycloak.sh -f bootstrap-values.yaml -n guardian`. It checks that the Keycloak pod is ready and that the secrets named in the values file exist, runs the chart and waits for the job, prints the job logs (also when the job fails), and then deletes the job. It prints the `kubectl get secret` commands that read the admin passwords, not the passwords. It finds the Keycloak pod by the `app=keycloak` label; pass `--keycloak-selector` (`-l`) with another label selector if your Keycloak pod is labeled differently.

Monitor the job and confirm it completes:

```bash
kubectl get jobs -l app.kubernetes.io/instance=keycloak-bootstrap \
  --namespace guardian -w
kubectl logs job/keycloak-bootstrap-keycloak-bootstrap --namespace guardian -f
```

The job is idempotent: re-running it updates existing clients from the values, and skips the realm, scopes, and users that already exist.

### 5. Retrieve the Auto-Generated Client Secrets

Keycloak generates the backend and worker client secrets during bootstrap. Copy both from the Keycloak admin console.

Get the master realm admin password:

```bash
kubectl get secret keycloak-admin --namespace guardian \
  -o jsonpath='{.data.password}' | base64 -d
```

If Keycloak has no external URL, port-forward its service:

```bash
kubectl port-forward svc/keycloak 8080:8080 --namespace guardian
```

Sign in to the admin console at `<keycloak-url>/admin` (or `http://localhost:8080/keycloak/admin`) as `admin`, select the **guardian** realm, then open **Clients** → **guardian-platform-backend** → **Credentials** and copy the **Client Secret**. Do the same for **governance-worker**.

### 6. Update Chart Values

Fill in the [Helm Configuration](#helm-configuration) sections:

- The two client secrets from step 5
- A token exchange private key: `openssl genrsa 2048`
- The issuer URL, which must equal the `issuer` field returned by:

```bash
curl "<keycloak-url>/realms/guardian/.well-known/openid-configuration"
```

Then proceed to the [Helm Deployment guide](./helm-deployment.mdx). The platform chart's post-install hook creates the organization, looks up the admin in the realm by `platformAdminEmail`, and links that user automatically. The hook fails if no user in the realm has that email.

The hook runs on every install and upgrade and never creates duplicate records. On an existing install it points the organization and the platform admin's user record at Keycloak and makes the admin an active `organization_owner` again: other organization roles are kept, and project roles and project assignments in the organization are removed. It does not change an existing organization's display name.

## Alternative: Manual Setup via Admin Console

The bootstrap chart is strongly recommended - it configures the realm, clients, protocol mappers, custom scopes, and service account roles consistently. If you must configure Keycloak manually, replicate what the bootstrap creates:

1. Create a `guardian` realm with brute force protection enabled
2. Create the three clients listed in the [Overview](#overview): frontend as a public client with the platform domain in redirect URIs and web origins; backend and worker as confidential clients with service accounts enabled
3. Grant the backend client's service account the `query-users` and `view-users` realm-management roles
4. Create two client scopes and add both as realm default scopes: `sub`, with a Subject (`oidc-sub-mapper`) mapper on the access token, and `platform-audience`, with an Audience mapper that adds `guardian-platform-frontend` to the access token. Assign the frontend client's default scopes: `openid`, `profile`, `email`, `roles`, `sub`, and `platform-audience`
5. Create a `platform-admin` realm user

See the [keycloak-bootstrap chart README](https://github.com/eqtylab/deployment/tree/main/charts/keycloak-bootstrap) for the complete list of scopes, mappers, and defaults to replicate.

## Verification

Confirm the realm's OIDC discovery endpoint responds and its `issuer` field matches `auth-service.config.idp.issuer` in `values.yaml`:

```bash
curl "<keycloak-url>/realms/guardian/.well-known/openid-configuration"
```

After deploying the platform, sign in to Guardian as `platform-admin` using the password from the `platform-admin` secret:

```bash
kubectl get secret platform-admin --namespace guardian \
  -o jsonpath='{.data.password}' | base64 -d
```

## Security Best Practices

- Require TLS on all Keycloak endpoints (`sslRequired: external` is the bootstrap default; use `all` if internal traffic should also be encrypted)
- Keep brute force protection enabled on the guardian realm (bootstrap default)
- Store the Keycloak admin password, client secrets, and token exchange private key only in Kubernetes secrets or an approved secret manager
- Leave self-registration disabled unless your onboarding process requires it
- Rotate the backend and worker client secrets periodically - regenerate in Keycloak, update the `platform-keycloak` and `platform-governance-worker` secrets in the same change window, then restart the services, which read the secrets only at startup: `kubectl rollout restart deployment/guardian-platform-auth-service deployment/guardian-platform-governance-service --namespace guardian`
- Keep the Keycloak version patched; it is part of your authentication attack surface

## Official Documentation

- [Keycloak Documentation](https://www.keycloak.org/documentation)
- [Keycloak Admin REST API](https://www.keycloak.org/docs-api/latest/rest-api/index.html)
- [Bitnami Keycloak Helm Chart](https://github.com/bitnami/charts/tree/main/bitnami/keycloak)
- [Helm Chart Examples](https://github.com/eqtylab/deployment/tree/main/charts/guardian-platform/examples)

## Related

- [Authentication](./authentication.mdx) — how Keycloak compares with the other supported providers
- [Identity Providers](../auth-service/identity-providers.mdx) — what Auth Service supports in Keycloak mode
- [Token Exchange and Refresh Tokens](../auth-service/token-exchange.mdx) — what token exchange needs, including its signing key, and how refresh tokens rotate
- [Chart Values](./chart-values.mdx) — the full values and secrets files these settings go into
- [Helm Deployment](./helm-deployment.mdx) — deploying the platform after the bootstrap job