OIDC the Hard Way: Deploying Keycloak on Bare-Metal Kubernetes (Part 2)
Production-grade identity infrastructure: Vault secrets, clustered Keycloak, Gateway API, and zero credentials in Git.
Overview
Part 1 established the foundation: HashiCorp Vault as the single source of truth for credentials, External Secrets Operator bridging Vault into Kubernetes-native Secrets, cert-manager automating TLS certificate lifecycle, and ArgoCD deploying everything declaratively from Git.
Part 2 builds the identity layer on top of that foundation: Keycloak — an open-source identity and access management solution deployed as a production-grade, 2-replica cluster with PostgreSQL persistence, every credential sourced from Vault, and exposed via the Cilium Gateway API with automatic TLS.
No secrets]:::git subgraph K8S [Kubernetes Cluster] direction TB Gateway[Cilium Gateway
192.168.2.204]:::k8s subgraph MGT [Management] ArgoCD[ArgoCD]:::k8s Vault[("Vault (Secrets)")]:::vault ESO[External Secrets]:::app Secret[K8s Secret]:::app end subgraph APPS [Apps] Keycloak[Keycloak x2]:::app DB[Postgres]:::app end end %% Flux User -->|HTTPS| Gateway Gateway -->|HTTP| Keycloak GitRepo -->|Sync| ArgoCD ArgoCD -->|Deploy| Keycloak ArgoCD -->|Config| ESO ESO -->|Auth & Pull| Vault ESO -->|Create| Secret Secret -.->|Inject| Keycloak Keycloak <--> DB
By the end of this article, you will have:
- A highly available Keycloak cluster with distributed session state via Infinispan
- PostgreSQL backend for persistent storage
- Admin and database credentials managed entirely through Vault
- TLS certificates issued and renewed automatically by cert-manager
- External access via Cilium Gateway API with proper proxy header handling
- Zero secrets visible in Git — complete GitOps compliance
Why Keycloak?
Keycloak provides enterprise-grade identity and access management with support for:
- OpenID Connect (OIDC) and SAML 2.0 protocols
- User Federation with LDAP, Active Directory, Kerberos
- Social Login (Google, GitHub, etc.)
- Multi-factor Authentication (TOTP, WebAuthn)
- Fine-grained Authorization with role-based and attribute-based access control
- Centralized Session Management across multiple applications
In a homelab or small enterprise environment, Keycloak eliminates the need to manage separate user databases in every application. Every service delegates authentication to Keycloak. Add a user once, grant them access to multiple services through realm roles. Revoke access in one place when they leave.
Prerequisites
The following components must be operational before proceeding:
- Vault initialized, unsealed, and Kubernetes auth configured
ClusterSecretStorenamedvault-backendinValidstate- cert-manager operational with
ClusterIssuermirecloud-ca-issuerinReadystate - ArgoCD connected to the repository
Verify:
kubectl get clustersecretstore vault-backend # NAME AGE STATUS CAPABILITIES READY # vault-backend 2d Valid ReadWrite True kubectl get clusterissuer mirecloud-ca-issuer # NAME READY AGE # mirecloud-ca-issuer True 2d
Layer 0 — PostgreSQL
Keycloak requires a relational database for persistent storage of realms, users, sessions, and configuration. The embedded H2 database is explicitly unsupported in clustered deployments and should never be used outside of local development.
PostgreSQL is deployed as a StatefulSet in a dedicated namespace:
helm install postgres oci://registry-1.docker.io/cloudpirates/postgres \ -n postgres --create-namespace
The chart generates a random password on first install and stores it in a Kubernetes Secret. This is the one time this credential is handled manually:
# Retrieve the generated password
kubectl -n postgres get secret postgres \
-o jsonpath='{.data.postgres-password}' | base64 -d
# Store it in Vault immediately
kubectl -n vault exec -ti vault-0 -- vault kv put secret/keycloak/db \
password='<retrieved-password>'
Layer 1 — Secrets in Vault
Every sensitive value is stored in Vault before any Kubernetes manifest is applied. This is the contract the entire pipeline depends on.
# Keycloak admin account
kubectl -n vault exec -ti vault-0 -- vault kv put secret/keycloak/admin \
password='StrongAdminPassword'
# Keycloak database credentials
kubectl -n vault exec -ti vault-0 -- vault kv put secret/keycloak/db \
password='DbPassword'
Two paths. Two credentials. Zero Git commits.
Layer 2 — External Secrets
The ClusterSecretStore from Part 1 is already in place. Two ExternalSecret resources declare which Vault paths to sync and what Kubernetes Secret objects to create.
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: keycloak-admin-es
namespace: keycloak
spec:
refreshInterval: 1m
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: keycloak-admin-password
creationPolicy: Owner
data:
- secretKey: password
remoteRef:
key: secret/keycloak/admin
property: password
Critical: The remoteRef.key path. ESO handles KV v2 path construction internally — it appends /data/ to the path when calling the Vault API. If you include /data/ in the key yourself, the resulting path becomes /data/data/secret/keycloak/admin, which returns a 404.
Layer 3 — Keycloak Helm Chart
Values Configuration
keycloakx:
command:
- "/opt/keycloak/bin/kc.sh"
- "start"
- "--http-enabled=true"
- "--http-port=8080"
- "--hostname-strict=false"
- "--proxy-headers=xforwarded"
replicas: 2
extraEnv: |
- name: KEYCLOAK_ADMIN
value: admin
- name: KEYCLOAK_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: keycloak-admin-password
key: password
- name: KC_PROXY
value: edge
dbchecker:
enabled: true
database:
vendor: postgres
hostname: postgres.postgres.svc
port: 5432
database: postgres
username: postgres
existingSecret: byo-db-creds
Configuration Rationale
--proxy-headers=xforwarded instructs Keycloak to respect X-Forwarded-For and X-Forwarded-Proto headers injected by the upstream Gateway. Without this flag, Keycloak ignores the forwarded headers and constructs redirect URIs based on what it sees directly — which is plain HTTP on port 8080.
KC_PROXY=edge is the complementary setting. It tells Keycloak it operates behind a TLS-terminating reverse proxy and should accept forwarded headers as authoritative. These two flags are paired — neither is sufficient without the other when TLS is terminated at the Gateway.
Layer 4 — Cilium Gateway API and TLS
This deployment uses the Kubernetes Gateway API rather than the legacy Ingress resource. Gateway API provides cleaner separation between infrastructure concerns (Gateway, GatewayClass) and application routing concerns (HTTPRoute).
Distributed Sessions: What Infinispan Provides
With replicas: 2, the keycloakx chart automatically configures Keycloak nodes to form a distributed Infinispan cache cluster. Pod discovery uses the headless Kubernetes service, which exposes individual pod addresses directly rather than load-balancing across them.
The clustering lifecycle is visible in the Keycloak logs:
ISPN100002: Starting rebalance with members
[keycloak-keycloakx-0-35812, keycloak-keycloakx-1-53189],
phase READ_OLD_WRITE_ALL, topology id 2
ISPN100010: Finished rebalance with members
[keycloak-keycloakx-0-35812, keycloak-keycloakx-1-53189],
topology id 5
Once complete, both nodes share distributed session state. A user authenticated through pod-0 can have their request served by pod-1 without being prompted to log in again.
Security Posture
At the completion of this deployment:
- No credential appears in Git in any form — no base64, no Helm
--setflags, no inlinestringData - Vault is the authoritative source for every sensitive value in the cluster
- TLS is enforced on all external endpoints, certificates issued and renewed automatically by cert-manager
- ExternalSecrets refresh every 60 seconds — a rotated Vault secret propagates to Kubernetes within one minute
- Session state is distributed across Keycloak replicas — no single point of failure
What's Next: Part 3
Keycloak is now operational as a standalone identity server. The next step is integrating it with an actual application to provide SSO.
Part 3 will cover the complete OIDC integration with Grafana, including:
- The OpenID Connect Authorization Code Flow (with diagram)
- Front-channel vs. back-channel URL configuration
- Client secret management via Vault and ESO
- Role mapping from Keycloak realm roles to Grafana permissions
- Eliminating the Grafana native login form entirely
The complete repository is available at github.com/mirecloud/home_lab.
Emmanuel Catin — Senior Platform Engineer | Kubernetes, GitOps, Zero Trust
CKA | Montréal, QC
#Kubernetes #Keycloak #GitOps #Vault #ExternalSecrets #CiliumGateway #GatewayAPI #PostgreSQL #Infinispan #DevSecOps #HomeLab #PlatformEngineering #ZeroTrust
Comments
Post a Comment