Skip to content

Keycloak

Keycloak provides OpenID Connect (OIDC) single sign-on for all HomeKube services. Once configured, you log in once via Keycloak and access Grafana (and future apps) without separate credentials.

See ADR-009 and ADR-011 for the full decision records.

Concepts

Term Meaning
Realm An isolated namespace for users, clients, and roles (homekube)
Client An application that delegates authentication to Keycloak (grafana)
User A person who logs in
Role A permission level (admin, viewer) mapped to Grafana roles
OIDC OpenID Connect — the protocol used for login flows

Architecture

sequenceDiagram
    participant B as Browser
    participant G as Grafana pod
    participant K as Keycloak pod

    B->>G: GET http://grafana.local
    G->>B: 302 redirect to Keycloak login
    B->>K: GET /realms/homekube/.../auth (keycloak.local)
    K->>B: Show login form
    B->>K: POST credentials
    K->>B: 302 redirect back to Grafana with auth code
    B->>G: GET /login/generic_oauth?code=...
    G->>K: POST /token (keycloak.identity.svc.cluster.local)
    K->>G: id_token + access_token
    G->>B: Set session cookie, show dashboard

Split-DNS

The browser uses keycloak.local (resolved via /etc/hosts). The Grafana pod uses keycloak.identity.svc.cluster.local for server-side token exchange — pods cannot resolve /etc/hosts entries.

Deployment Model

Keycloak runs as a plain Kubernetes Deployment using the official image:

Property Value
Image quay.io/keycloak/keycloak:26.3
Mode start-dev (embedded H2 database)
Realm import --import-realm reads from /opt/keycloak/data/import/ on startup
Admin credentials From keycloak-admin-secret (SOPS-encrypted)
Realm config From realm-config.sops.yaml (SOPS-encrypted ConfigMap)
Manifest identity/keycloak-deployment.yaml

Ephemeral realm data

start-dev mode uses an in-memory H2 database. Any changes made via the Keycloak admin UI are lost on pod restart. All realm changes must go through identity/realm-config.sops.yaml — see Updating the realm config below.

Access Keycloak

Ensure /etc/hosts has:

127.0.0.1  keycloak.local

Ensure minikube tunnel is running in a terminal (see ingress-nginx).

Open http://keycloak.local — admin console is at http://keycloak.local/admin.

Credentials are stored in identity/keycloak-secret.sops.yaml. Default (dev): admin / admin.

Pre-configured Realm

The homekube realm is auto-imported on every Keycloak startup from identity/realm-config.sops.yaml. It includes:

Resource Details
Realm homekube
Client grafana (confidential OIDC, maps roles to Grafana roles)
Roles admin (full access), viewer (read-only)
Default user admin / admin — change after first login

Updating the Realm Config

All realm changes must be committed to Git and applied via Flux:

# 1. Edit the encrypted realm JSON
sops identity/realm-config.sops.yaml

# 2. Commit and push
git add identity/realm-config.sops.yaml
git commit -m "feat: add new OIDC client for my-app"
git push origin main

# 3. Force Flux to apply
flux reconcile kustomization identity --with-source

# 4. Restart Keycloak to reimport
kubectl rollout restart deployment/keycloak -n identity
kubectl rollout status deployment/keycloak -n identity

Grafana SSO

Grafana is pre-configured for Keycloak OIDC:

  • Local login is disabled — Keycloak is the only login method
  • Role mapping: Keycloak admin role → Grafana Admin; anything else → Viewer
  • Client secret stored in observability/grafana-oidc-secret.sops.yaml

To log in: open http://grafana.local → you are redirected to Keycloak automatically.

Verify SSO is Working

# Check Keycloak pod is healthy
kubectl get pods -n identity

# Check Grafana can reach Keycloak (server-side)
kubectl exec -n monitoring deploy/kube-prometheus-stack-grafana -c grafana -- \
  curl -s http://keycloak.identity.svc.cluster.local/realms/homekube/.well-known/openid-configuration \
  | python3 -m json.tool | grep issuer

Open Grafana in an incognito window: http://grafana.local → sign in with admin / admin.

Operations

# View Keycloak logs
kubectl logs -n identity deployment/keycloak --tail=50 -f

# Force realm reimport (restart pod)
kubectl rollout restart deployment/keycloak -n identity

# Check admin console is reachable
curl -s -o /dev/null -w "%{http_code}" http://keycloak.local/realms/master
# Expected: 200

Phase 4: kubectl OIDC Auth

With OIDC configured, kubectl commands can authenticate via Keycloak instead of client certificates:

brew install int128/kubelogin/kubelogin

kubectl oidc-login setup \
  --oidc-issuer-url=http://keycloak.local/realms/homekube \
  --oidc-client-id=kubectl \
  --oidc-client-secret=<secret>

This requires a kubectl client in Keycloak and kube-apiserver OIDC flags in minikube — covered in Phase 4.

Phase 4: oauth2-proxy

For apps that don't natively support OIDC, oauth2-proxy enforces authentication via ingress-nginx annotations:

annotations:
  nginx.ingress.kubernetes.io/auth-url: "http://oauth2-proxy.identity.svc/oauth2/auth"
  nginx.ingress.kubernetes.io/auth-signin: "http://oauth2-proxy.identity.svc/oauth2/start"