Skip to main content

Keycloak

Keycloak is an open-source identity and access management server — OIDC, OAuth2, and SAML on the front, user federation (LDAP), social login, fine-grained authorization on the back. In this homelab it's the central SSO broker: cluster apps that need login delegate to Keycloak via OIDC, Keycloak federates user accounts from the in-cluster LLDAP directory, and Keycloak itself stores its state in a dedicated CNPG Postgres cluster.

Why Keycloak

  • Mature OIDC + SAML in one binary. Most lighter alternatives are OIDC-only; SAML still matters for a handful of legacy apps (and for federating outward to corporate IdPs).
  • Federation, not user copying. Connecting to LLDAP means user accounts live in one place (LDAP) and Keycloak just brokers protocol translation. No double bookkeeping.
  • Per-realm isolation. Different sets of clients (personal apps vs. test realms) live in separate realms with separate token signing keys, without running multiple instances.
  • Battle-tested. Boring is a feature for the thing that gates access to everything else. Red Hat's been driving the project for over a decade; the release cadence is predictable.

Alternatives considered

OptionHostedSelf-hostedWhy not
Auth0 / OktaPer-MAU billing; identity data sitting in a vendor
Microsoft Entra IDTied to a Microsoft tenant; per-seat costs
AuthentikLighter footprint, slicker UI — strong contender; tracked, not yet swapped in
PocketIDPasskey-only OIDC; runs alongside Keycloak for the passkey-friendly subset
ZitadelTracked; Keycloak's maturity + SAML keeps it in place for now

Installation

The Keycloak install is a single Deployment, but most of the work is in the surrounding wiring — Postgres, ConfigMap, two HTTPRoutes, network policies:

  • Single Deployment in the keycloak namespace, image digest-pinned. Renovate keeps the tag fresh.
  • Postgres via CNPG. A dedicated Cluster for Keycloak's realm + user + session state, on the longhorn-encrypted storage class.
  • Two HTTPRoutes target the same Service: one public-facing on keycloak.web.kueber.eu (the login / OIDC endpoints), one admin-only on a separate host with a stricter network policy. The split keeps the admin console off the public hostname.
  • KC_* config from a ConfigMap — hostname, proxy mode, KC_PROXY_HEADERS=xforwarded, KC_PROXY_TRUSTED_ADDRESSES pointing at the Envoy CIDR. This is what makes the audit log and rate-limiter see the real client IP rather than the gateway's — see Topics → Real client IPs.
  • Secrets via SOPS — admin credentials, database connection. The CNPG-generated DB secret is mounted directly; the rest is committed encrypted and decrypted at apply time.
  • Security context. Runs as runAsUser: 1000, non-root, capabilities dropped. readOnlyRootFilesystem: false because Keycloak needs to write to /tmp during startup for the Quarkus build phase.

Administration

  • First-time setup. A bootstrap admin (credentials from a SOPS secret) creates the master realm; from there every app realm is created manually or via the Keycloak CLI. Realm exports are committed to the repo for reproducibility.
  • Adding a client app. Realm → Clients → Create, set the redirect URIs (https://<app>.web.kueber.eu/oauth2/callback for most), enable PKCE, set the access type. Copy the client ID + secret into the target app's SOPS-encrypted secret.
  • LDAP federation. Realm → User Federation → ldap → bind DN to LLDAP. User accounts then live in LLDAP; Keycloak handles protocol translation. The sync mode is read-only — write-through would mean Keycloak mutating LLDAP, which is more surface than necessary.
  • Theme. Keycloak themes are mounted via a ConfigMap (the login page picks them up at startup). Customizing branding here is one of the more visible wins for end users.
  • Realm export / import is the disaster-recovery path. The CNPG backup captures everything, but realm JSONs in the repo are the human-readable version. See Topics → Disaster recovery.
  • Upgrades. Renovate auto-merges patch / digest per the Renovate policy. Major bumps wait for a human read — Keycloak occasionally renames realm settings or rotates default crypto, both worth reading release notes for.
  • Backups. CNPG cluster is backed up via k8up.io/backupcommand: pg_dump with a PreBackupPod running the same Postgres image — restic captures a consistent SQL dump.

Usage

  • Apps delegate login to https://keycloak.web.kueber.eu/realms/<realm> via standard OIDC. Most apps just need issuer URL, client ID, client secret — the same three fields, copy-pasted into a SOPS-encrypted secret per app.
  • Admin console lives on the admin-only HTTPRoute, intentionally not on the public hostname. Day-to-day admin work happens here — realms, clients, users, sessions.
  • Account console at /realms/<realm>/account is where end users manage their own password, sessions, and (where enabled) WebAuthn / TOTP.
  • Federated identities. The LLDAP federation means most users never log into Keycloak directly to change a password — they change it in LLDAP and Keycloak picks it up. Internal users (admin, service principals) live in Keycloak only.

Cluster Deployment

Keycloak — Talos cluster

Cluster-specific notes only. General product info, "why we use it", and alternatives live in docusaurus/docs/apps/keycloak.mdx.

Deviations from defaults

Defaults live in docusaurus/docs/apps/keycloak.mdx — document anything this cluster does differently here, with a one-line reason.

Kubernetes Metadata
  • Image: quay.io/keycloak/keycloak:26.6.3@sha256:5fdbf2dbb5897cc34e82de49d13e23db011f9925089dbc555fc095f2c8bc1dac
Rendered manifests (kustomize build)
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
kustomize.toolkit.fluxcd.io/force: enabled
labels:
app: keycloak
name: keycloak
namespace: keycloak
spec:
replicas: 1
selector:
matchLabels:
app: keycloak
ingress: all
strategy:
type: Recreate
template:
metadata:
labels:
app: keycloak
ingress: all
spec:
containers:
- args:
- start
- '--verbose'
env:
- name: KC_HEALTH_ENABLED
value: 'true'
- name: KC_DB_URL
valueFrom:
secretKeyRef:
key: jdbc-uri
name: cnpg-app
- name: KC_DB
value: postgres
envFrom:
- configMapRef:
name: keycloak-env
- secretRef:
name: keycloak-env
image: quay.io/keycloak/keycloak:26.6.3@sha256:5fdbf2dbb5897cc34e82de49d13e23db011f9925089dbc555fc095f2c8bc1dac
livenessProbe:
failureThreshold: 3
httpGet:
path: /health/live
port: metrics
initialDelaySeconds: 60
periodSeconds: 5
successThreshold: 1
timeoutSeconds: 2
name: keycloak
ports:
- containerPort: 8080
name: web
protocol: TCP
- containerPort: 9000
name: metrics
protocol: TCP
readinessProbe:
failureThreshold: 3
httpGet:
path: /health/ready
port: metrics
initialDelaySeconds: 60
periodSeconds: 5
successThreshold: 1
timeoutSeconds: 2
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
privileged: false
readOnlyRootFilesystem: false
startupProbe:
failureThreshold: 3
httpGet:
path: /health
port: metrics
initialDelaySeconds: 60
periodSeconds: 5
successThreshold: 1
timeoutSeconds: 2
securityContext:
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
runAsGroup: 1000
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault