feat(auth): Keycloak OIDC login + RBAC for kubectl (cert kubeconfig stays break-glass)

Lets users kubectl into the cluster via Keycloak SSO, mapping realm groups to
cluster RBAC. The cert-based admin kubeconfig is untouched and remains a fully
independent break-glass path (works even if Keycloak/OIDC is down).

- keycloak-realm-import.yaml: new `kubernetes` PUBLIC client + PKCE (S256) — no
  client secret in any kubeconfig; kubelogin runs auth-code+PKCE to localhost.
  groups mapper + email/profile/roles scopes.
- k8s-oidc-rbac.yaml (NEW, platform-config): ClusterRoleBindings
  oidc:platform-admins -> cluster-admin, oidc:viewers -> view.
- k3s_server role: kube-apiserver-arg OIDC flags (issuer=homelab realm,
  client-id=kubernetes, username-claim=email + oidc: prefix, groups-claim=groups
  + oidc: prefix). No oidc-ca-file (nodes reach the issuer via the gateway VIP
  and trust the public LE cert). Applied via the rolling Restart k3s handler.

Keycloak/RBAC land via Flux now; the API-server flags take effect on the next
`ansible-playbook playbooks/k3s.yml` rolling restart (deferred until cluster load
from the VM-import incident settles).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
jakman85andClaude Opus 4.8 committed 2026-08-26 17:37:05 -05:00
1 parent ab592028f6
commit 3472f1d161
4 files changed
+106 -1

No files matched your search

+46
View File
@@ -0,0 +1,46 @@
---
# Kubernetes RBAC for Keycloak-federated (OIDC) users. The k3s API server is configured
# (infra/ansible roles/k3s_server) with:
# oidc-issuer-url=https://keycloak.home.keithjackson.dev/realms/homelab
# oidc-client-id=kubernetes oidc-username-claim=email oidc-username-prefix=oidc:
# oidc-groups-claim=groups oidc-groups-prefix=oidc:
# so a Keycloak group `platform-admins` appears to RBAC as the group `oidc:platform-admins`, and a
# user with email [email protected] as `oidc:[email protected]`. These bindings map the Keycloak group
# claim to cluster roles. Group-based (not user-based) so access follows Keycloak group membership
# with no per-user k8s change. Applied by platform-config (dependsOn platform).
#
# BREAK-GLASS: the cert-based admin in the bootstrap kubeconfig is unaffected by any of this and
# keeps full access even if Keycloak/OIDC is down — do not remove it.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: oidc-platform-admins
labels:
app.kubernetes.io/part-of: keycloak
homelab.keithjackson.dev/oidc-rbac: "true"
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: Group
name: "oidc:platform-admins"
---
# Read-only tier: the Keycloak `viewers` group gets the built-in `view` ClusterRole (read most
# resources cluster-wide, excluding Secrets and RBAC escalation paths).
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: oidc-viewers
labels:
app.kubernetes.io/part-of: keycloak
homelab.keithjackson.dev/oidc-rbac: "true"
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: view
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: Group
name: "oidc:viewers"
@@ -103,6 +103,48 @@ spec:
"access.token.claim": "true"
"userinfo.token.claim": "true"
"claim.name": groups
# kubernetes — kubectl OIDC login via kubelogin (int128/kubelogin `oidc-login`). PUBLIC client +
# PKCE (S256) so NO client secret lives in any user's kubeconfig; kubelogin runs the auth-code
# +PKCE flow against a localhost callback and caches the ID token. The k3s API server validates
# that ID token (kube-apiserver oidc-issuer-url=this realm, oidc-client-id=kubernetes,
# oidc-username-claim=email, oidc-groups-claim=groups); RBAC binds the `groups` claim — see
# k8s-oidc-rbac.yaml (oidc:platform-admins -> cluster-admin, oidc:viewers -> view). The cert
# kubeconfig remains the break-glass admin, independent of Keycloak/OIDC availability.
- clientId: kubernetes
name: Kubernetes (kubectl)
protocol: openid-connect
enabled: true
publicClient: true
standardFlowEnabled: true
directAccessGrantsEnabled: false
attributes:
"pkce.code.challenge.method": "S256"
redirectUris:
- http://localhost:8000
- http://localhost:8000/
- http://localhost:18000
- http://localhost:18000/
- http://127.0.0.1:8000
- http://127.0.0.1:8000/
- http://127.0.0.1:18000
- http://127.0.0.1:18000/
webOrigins:
- http://localhost
- http://127.0.0.1
defaultClientScopes:
- email
- profile
- roles
protocolMappers:
- name: groups
protocol: openid-connect
protocolMapper: oidc-group-membership-mapper
config:
"full.path": "false"
"id.token.claim": "true"
"access.token.claim": "true"
"userinfo.token.claim": "true"
"claim.name": groups
# --- MFA for platform-admins (R-IDP-04): a browser flow whose OTP step is CONDITIONAL on the
# platform-admin role. Non-admins log in without OTP; platform-admins must enroll/present TOTP.
# Set as the realm browserFlow below. VERIFY at bring-up (see runbook): a malformed flow fails