The kube-apiserver still authenticates OIDC against Authentik, which now runs zero pods. That issuer cannot be discovered, so token-based kubectl login is broken today. The template carried no OIDC block at all, so a render would have removed the stale config rather than replaced it.
This adds the ZITADEL issuer, client id and claims.
Three details verified against real tokens first (not assumed)
#
Finding
Consequence
1
Issuer must be https://zitadel.riotpiao.com — no trailing slash, no/application/o/<app>/ suffix
That suffix was the Authentik scheme. The apiserver compares iss by exact string equality
2
ZITADEL does not put profile/email claims in the id_token by default
oidc-username-claim: email resolved to nothing. idTokenUserinfoAssertion is now enabled on the kubernetes app
3
ZITADEL asserts roles as a nested urn:...:project:roles claim the apiserver cannot read
The groupsClaim action mirrors bare role names into a flat groups claim
Proof
A full PKCE authorization-code flow against the kubernetes client returns an id_token containing:
iss = https://zitadel.riotpiao.com
sub = 396924923823522416
email = [email protected]
preferred_username = riotpiao
groups = [homelab-admins, llm:inference, memory:read, ...]
With oidc-groups-prefix: "oidc:" that maps to the existing ClusterRoleBinding homelab-admin-oidc → cluster-admin.
Also in this change
groupsClaim action created and bound to the Complement Token flow (triggers 4 and 5), with allowedToFail: true.
Note: an earlier draft of that script read ctx.v1.user.grants.count unguarded. Machine users have no grants, so it threw and ZITADEL answered 500 to every/oauth/v2/token request — a full auth outage. The committed script guards every field and is wrapped in try/catch, and allowedToFail ensures a future script bug degrades to "no groups claim" instead of blocking token issuance.
DNS
The issuer hostname is pinned in extraHostEntries to the ingress LoadBalancer. The apiserver's static pod resolves through the node's DNS, not cluster CoreDNS, and the LB serves the publicly-trusted Let's Encrypt *.riotpiao.com certificate (verified Verify return code: 0).
Safety
Client-certificate auth is unchanged — a wrong value degrades OIDC logins, it cannot lock anyone out.
terraform plan touches only local_file resources (renders cluster-config/*.yaml); it does not contact the nodes.
The rolling control-plane reboot (make apply-cp) is a separate, deliberate step and is not run by this PR.
## Why
The kube-apiserver still authenticates OIDC against **Authentik**, which now runs zero pods. That issuer cannot be discovered, so token-based `kubectl` login is broken today. The template carried no OIDC block at all, so a render would have *removed* the stale config rather than replaced it.
This adds the ZITADEL issuer, client id and claims.
## Three details verified against real tokens first (not assumed)
| # | Finding | Consequence |
|---|---|---|
| 1 | Issuer must be `https://zitadel.riotpiao.com` — no trailing slash, **no** `/application/o/<app>/` suffix | That suffix was the Authentik scheme. The apiserver compares `iss` by exact string equality |
| 2 | ZITADEL does **not** put profile/email claims in the id_token by default | `oidc-username-claim: email` resolved to nothing. `idTokenUserinfoAssertion` is now enabled on the `kubernetes` app |
| 3 | ZITADEL asserts roles as a nested `urn:...:project:roles` claim the apiserver cannot read | The `groupsClaim` action mirrors bare role names into a flat `groups` claim |
## Proof
A full PKCE authorization-code flow against the `kubernetes` client returns an id_token containing:
```
iss = https://zitadel.riotpiao.com
sub = 396924923823522416
email = [email protected]
preferred_username = riotpiao
groups = [homelab-admins, llm:inference, memory:read, ...]
```
With `oidc-groups-prefix: "oidc:"` that maps to the existing ClusterRoleBinding `homelab-admin-oidc` → `cluster-admin`.
## Also in this change
`groupsClaim` action created and bound to the Complement Token flow (triggers 4 and 5), with `allowedToFail: true`.
> Note: an earlier draft of that script read `ctx.v1.user.grants.count` unguarded. Machine users have no `grants`, so it threw and ZITADEL answered **500 to every** `/oauth/v2/token` request — a full auth outage. The committed script guards every field and is wrapped in try/catch, and `allowedToFail` ensures a future script bug degrades to "no groups claim" instead of blocking token issuance.
## DNS
The issuer hostname is pinned in `extraHostEntries` to the ingress LoadBalancer. The apiserver's static pod resolves through the **node's** DNS, not cluster CoreDNS, and the LB serves the publicly-trusted Let's Encrypt `*.riotpiao.com` certificate (verified `Verify return code: 0`).
## Safety
- Client-certificate auth is unchanged — a wrong value degrades OIDC logins, it cannot lock anyone out.
- `terraform plan` touches only `local_file` resources (renders `cluster-config/*.yaml`); it does **not** contact the nodes.
- The rolling control-plane reboot (`make apply-cp`) is a separate, deliberate step and is **not** run by this PR.
The apiserver's live OIDC config still pointed at
https://authentik.riotpiao.com/application/o/kubernetes/. Authentik now runs
zero pods, so that issuer cannot be discovered and every attempt to login with
a token instead of a client certificate fails. The template itself carried no
OIDC block at all, so a render would have removed the stale config rather than
replace it.
Add the ZITADEL issuer, the `kubernetes` client id, and the groups/username
claims. Three details had to be verified against a real token first:
1. The issuer is https://zitadel.riotpiao.com with no trailing slash and no
/application/o/<app>/ suffix. That suffix belonged to the Authentik scheme;
the apiserver compares `iss` by exact string equality.
2. ZITADEL does NOT put profile/email claims in the id_token by default, so
`oidc-username-claim: email` resolved to nothing. idTokenUserinfoAssertion is
now enabled on the `kubernetes` application.
3. ZITADEL asserts roles as a nested urn claim the apiserver cannot read. The
`groupsClaim` action (created alongside this change) mirrors the bare role
names into a flat `groups` claim.
Verified with a full PKCE authorization-code flow against this client: the
id_token carries iss=https://zitadel.riotpiao.com, email, preferred_username and
groups=[homelab-admins, ...]. With oidc-groups-prefix=oidc: that maps to the
existing ClusterRoleBinding homelab-admin-oidc, which grants cluster-admin.
The issuer hostname is pinned in extraHostEntries because the apiserver's static
pod resolves through the node's DNS, not cluster CoreDNS; the ingress
LoadBalancer serves the publicly-trusted Let's Encrypt wildcard certificate.
Client-certificate auth is unchanged, so a wrong value degrades OIDC logins
without locking anyone out.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Why
The kube-apiserver still authenticates OIDC against Authentik, which now runs zero pods. That issuer cannot be discovered, so token-based
kubectllogin is broken today. The template carried no OIDC block at all, so a render would have removed the stale config rather than replaced it.This adds the ZITADEL issuer, client id and claims.
Three details verified against real tokens first (not assumed)
https://zitadel.riotpiao.com— no trailing slash, no/application/o/<app>/suffixissby exact string equalityoidc-username-claim: emailresolved to nothing.idTokenUserinfoAssertionis now enabled on thekubernetesappurn:...:project:rolesclaim the apiserver cannot readgroupsClaimaction mirrors bare role names into a flatgroupsclaimProof
A full PKCE authorization-code flow against the
kubernetesclient returns an id_token containing:With
oidc-groups-prefix: "oidc:"that maps to the existing ClusterRoleBindinghomelab-admin-oidc→cluster-admin.Also in this change
groupsClaimaction created and bound to the Complement Token flow (triggers 4 and 5), withallowedToFail: true.DNS
The issuer hostname is pinned in
extraHostEntriesto the ingress LoadBalancer. The apiserver's static pod resolves through the node's DNS, not cluster CoreDNS, and the LB serves the publicly-trusted Let's Encrypt*.riotpiao.comcertificate (verifiedVerify return code: 0).Safety
terraform plantouches onlylocal_fileresources (renderscluster-config/*.yaml); it does not contact the nodes.make apply-cp) is a separate, deliberate step and is not run by this PR.