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
| Option | Hosted | Self-hosted | Why not |
|---|---|---|---|
| Auth0 / Okta | ✓ | Per-MAU billing; identity data sitting in a vendor | |
| Microsoft Entra ID | ✓ | Tied to a Microsoft tenant; per-seat costs | |
| Authentik | ✓ | Lighter footprint, slicker UI — strong contender; tracked, not yet swapped in | |
| PocketID | ✓ | Passkey-only OIDC; runs alongside Keycloak for the passkey-friendly subset | |
| Zitadel | ✓ | ✓ | Tracked; 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
keycloaknamespace, image digest-pinned. Renovate keeps the tag fresh. - Postgres via CNPG. A dedicated
Clusterfor Keycloak's realm + user + session state, on thelonghorn-encryptedstorage class. - Two
HTTPRoutes target the same Service: one public-facing onkeycloak.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_ADDRESSESpointing 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: falsebecause Keycloak needs to write to/tmpduring 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/callbackfor 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_dumpwith aPreBackupPodrunning 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>/accountis 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.
- 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