Add Headlamp (kubernetes-sigs/headlamp Helm chart) at headlamp.home.keithjackson.dev with native Keycloak OIDC login and PER-USER RBAC: Headlamp forwards each user's id_token to the k3s apiserver, so the existing oidc:* group bindings (platform-admins->cluster-admin, viewers->view) apply per user. The ServiceAccount is deliberately NOT bound to cluster-admin (clusterRoleBinding.create=false), since user requests ride the forwarded token. Key integration nuance: the apiserver is configured oidc-client-id=kubernetes, so it only accepts tokens whose aud contains `kubernetes`. The new `headlamp` Keycloak client carries an oidc-audience-mapper (included.client.audience= kubernetes) so tokens carry aud=[headlamp, kubernetes] -- accepted by both Headlamp's own verifier and the apiserver, leaving the apiserver config untouched. A groups mapper emits the `groups` claim the apiserver maps to RBAC. - platform/headlamp: namespace, HelmRepository/HelmRelease, SOPS OIDC secret (externalSecret envFrom), default-deny NetworkPolicy + Cilium ingress (gateway)/egress (apiserver+Keycloak) policies, README. Wired into platform/kustomization.yaml (platform tier). - platform/headlamp/httproute.yaml wired into platform-config (Gateway API CR, dependsOn platform), modeled on grafana-httproute. - keycloak-realm-import: add `headlamp` confidential client + groups/audience mappers + HEADLAMP_CLIENT_SECRET placeholder. Client also created LIVE via kcadm (realm import no-ops on the existing realm). - keycloak-realm-clients.sops: add headlamp-client-secret (mirrored into headlamp-oidc-credentials clientSecret). Both encrypted to the repo age key. Co-Authored-By: Claude Opus 4.8 <[email protected]>
3.3 KiB
Headlamp — Kubernetes dashboard (Keycloak OIDC + per-user RBAC)
Headlamp is served at https://headlamp.home.keithjackson.dev behind the shared Gateway
(wildcard TLS), with native Keycloak OIDC login and per-user RBAC (ADR-0014, docs/15).
How per-user RBAC works (end to end)
- A user logs in at Keycloak (
homelabrealm,headlampconfidential client, auth-code flow). - Headlamp's backend keeps the user's
id_tokenand forwards it as the bearer on every request it proxies to the k3s API server (this is Headlamp's default OIDC behaviour). - The apiserver validates that token — it is configured (
infra/ansibleroles/k3s_server) withoidc-issuer-url= this realm,oidc-client-id=kubernetes,oidc-username-claim=email,oidc-groups-claim=groups, prefixesoidc:. - RBAC is applied per user via the existing group bindings in
../keycloak/k8s-oidc-rbac.yaml:oidc:platform-admins→cluster-admin,oidc:viewers→view.
So each user gets exactly their own rights — not a shared Headlamp identity. That is why the
Headlamp ServiceAccount is not bound to cluster-admin: the chart default
clusterRoleBinding.create: true (→ cluster-admin) is overridden to false in
helmrelease.yaml. User requests ride the forwarded user token; the SA needs no cluster rights.
The audience nuance (why the client has an audience mapper)
The apiserver only accepts ID tokens whose aud contains kubernetes (oidc-client-id= kubernetes). A plain headlamp client issues aud=[headlamp], which the apiserver would reject.
The headlamp client therefore carries an oidc-audience-mapper (see
../keycloak/keycloak-realm-import.yaml) with included.client.audience: kubernetes, so tokens
carry aud=[headlamp, kubernetes]:
- Headlamp's own verifier accepts it (aud contains
headlamp), and - the apiserver accepts it (aud contains
kubernetes).
This keeps the apiserver OIDC config untouched (preferred over widening --api-audiences).
Secrets
- The confidential client secret lives SOPS-encrypted in
../keycloak/keycloak-realm-clients.sops.yaml(headlamp-client-secret, injected into the realm import via theHEADLAMP_CLIENT_SECRETplaceholder) and mirrored in this namespace'sheadlamp-oidc-credentials.sops.yaml(clientSecret), which the chartconfig.oidc.externalSecretenvFrom's. Same cross-namespace mirror pattern as grafana/gitea.
Break-glass
The cert-based admin in the bootstrap kubeconfig is unaffected by any of this and retains full access if Keycloak/OIDC is down — the standard break-glass path.
Files
| File | Tier | Purpose |
|---|---|---|
namespace.yaml |
platform | headlamp namespace (gateway-access=shared; platform, not app-namespace) |
helmrepository.yaml / helmrelease.yaml |
platform | Official kubernetes-sigs/headlamp chart + OIDC/RBAC values |
headlamp-oidc-credentials.sops.yaml |
platform | SOPS OIDC config the chart envFrom's (decrypted by the platform Flux Kustomization) |
networkpolicy.yaml |
platform | default-deny baseline + Cilium ingress(gateway)/egress(apiserver+Keycloak) allows |
httproute.yaml |
platform-config | Gateway API HTTPRoute for headlamp.home.keithjackson.dev (needs Gateway CRDs) |