fix(apps/kubernetes): switch AuthenticationConfiguration username claim to email

Real bug caught by an end-to-end OIDC login test on dev3: apiserver
accepts the JWT (auth succeeds) but the RBAC check fails with 403
because the CRB `User:` subject was `alice-oidc@test3.example` (from
`users[].email`) but the apiserver-visible identity was `alice-oidc`
(from `preferred_username`, which in the cozy realm is the Keycloak
*username*, not the email).

The design proposal example used `claim: preferred_username`, but
the operator-facing `users[].email` field explicitly asks for the
email address. In the cozy realm — verified — `preferred_username`
is the Keycloak username (`kvaps`, `i.okhotnikov`), so the two do
NOT coincide. Every user whose Keycloak username differs from their
email would silently 403 after a successful OIDC handshake.

Switch the AuthenticationConfiguration `claimMappings.username.claim`
to `email`, aligning the apiserver-visible identity with the
operator-declared `users[].email`. Every conformant OIDC provider
emits `email` — cozy realm does (verified), and any BYO
`CustomConfig` issuer that follows OIDC does too.

The switch is invisible to installers where `preferred_username`
already equals the email (a common Keycloak setup); for installers
where it doesn't, this fix is required for RBAC to work at all.

Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
This commit is contained in:
Ivan Okhotnikov authored and Ivan Okhotnikov committed 2026-07-06 09:25:07 +03:00
1 parent 7257f8723e
commit fe3cfef90a
2 files changed
+24 -1

No files matched your search

@@ -29,7 +29,24 @@
{{- if eq .Values.oidc.mode "System" }}
{{- $issuer := include "kubernetes.oidc.systemIssuerURL" . | trim }}
{{- $audience := include "kubernetes.oidc.clientId" . | trim }}
{{- $authnConfig := printf "apiVersion: apiserver.config.k8s.io/v1beta1\nkind: AuthenticationConfiguration\njwt:\n- issuer:\n url: %s\n audiences:\n - %s\n claimMappings:\n username:\n claim: preferred_username\n prefix: \"\"\n groups:\n claim: groups\n prefix: \"\"\n" $issuer $audience }}
{{- /*
User identity claim: `email`, not `preferred_username`.
The design doc example used `claim: preferred_username`, but in the
cozystack `cozy` realm `preferred_username` is the Keycloak *username*
(`kvaps`, `i.okhotnikov`), NOT the email — and the operator-facing
`users[].email` field asks for the email address. Matching the `User:`
subject in the ClusterRoleBinding to `preferred_username` would
silently misidentify every user whose Keycloak username differs from
their email — verified on dev3 where token auth succeeded but the CRB
match failed with 403.
Switching the claim to `email` aligns the apiserver-visible identity
with the operator's declared `users[].email` value. Requires the
issuer to emit `email` — cozy realm (verified) and every conformant
OIDC provider do.
*/ -}}
{{- $authnConfig := printf "apiVersion: apiserver.config.k8s.io/v1beta1\nkind: AuthenticationConfiguration\njwt:\n- issuer:\n url: %s\n audiences:\n - %s\n claimMappings:\n username:\n claim: email\n prefix: \"\"\n groups:\n claim: groups\n prefix: \"\"\n" $issuer $audience }}
---
apiVersion: v1
kind: Secret
@@ -253,6 +253,12 @@ tests:
- matchRegex:
path: stringData["config.yaml"]
pattern: "url: https://keycloak.example.org/realms/cozy"
# Verified end-to-end on dev3: cozy realm's preferred_username is the
# Keycloak *username* (not the email), so a CRB subject sourced from
# users[].email would silently mis-authenticate. Lock the claim name.
- matchRegex:
path: stringData["config.yaml"]
pattern: "claim: email"
- matchRegex:
path: stringData["config.yaml"]
pattern: "- tenant-foo-kubernetes-app"