HOMELAB-1681: fix(zitadel): assert project roles into the cockpit ID token #590
No reviewers
Labels
No labels
operator-review
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Homelab/infra-core!590
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "plane/HOMELAB-1681b-roleclaim"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
id_token_role_assertion = trueonzitadel_application_oidc.lab_director(+ comment correction). One resource, one attribute. No other resource touched, 0 to destroy.Why — the live LD3 break
Operator logs into https://lab-director.aaron.reynoza.org successfully, then gets "Signed in, but not an operator — your roles: none" (subject
363992137722757672).Diagnosis (read-only, evidence first):
GET /management/v1/users/363992137722757672→aaron@reynoza.org, ACTIVEoperatorgrant364000641170801240,roleKeys [jellyfin, admins, operator], ACTIVEGET /management/v1/projects/364000638805213784→projectRoleAssertion: trueid_token_role_assertionwasfalse; the plan proved it:~ id_token_role_assertion = false -> trueproject_role_assertion(project) only covers the userinfo endpoint. In Zitadel v4.10.1 — the version this cluster runs — the ID token's role claims are gated by the client flag:internal/api/oidc/userinfo.go assertRoles()returns early unlessidTokenRoleAssertionis set, and the projection default is false (internal/query/projection/app.go: handler.Default(false)). The cockpit readsurn:zitadel:iam:org:project:rolesoff the verified ID token and has no userinfo fallback (lab-director/cockpit/auth/roles.go), so it fails closed — exactly the 403 shown.Scopes are not a substitute:
urn:zitadel:iam:org:project:rolesis the claim name, not a Zitadel scope (internal/api/oidc/client.godefinesurn:zitadel:iam:org:projects:rolesand theurn:zitadel:iam:org:project:role:{key}prefix); role scopes are stripped from the ID token while the flag is false (client_converter.go RestrictAdditionalIdTokenScopes); andassertRolesstill gates on the flag. Hence a terraform-first app-level fix, not anOIDC_SCOPESchange in prod.Apply (it is already live)
Guarded local path, same discipline as the LD3.1b applies — targeted plan → verify scope → apply the reviewed saved plan:
Live re-read (non-secret):
GET /management/v1/projects/364000638805213784/apps/393380591770272287→idTokenRoleAssertion: true. Client id/secret unchanged (state hashes == the cockpit's runtimecockpit-oidcSecret), so the running pods keep working.Full-plan residual after the apply is only the two known items:
william@reynoza.org(HOMELAB-1867 drift) andjesus@reynoza.org(an operator-made console deletion — a full apply would resurrect it; deliberately not applied).Reviewer context
Only CI's own apply leg could conflict: it runs
terraform applywith no target, i.e. it would also resurrect jesus. Not a blocker for this diff (pre-existing, applies equally tolivetoday), but worth knowing before suggesting a merge-triggered apply.Apply + verification evidence (fix worker, non-secret)
The change is already live (local guarded path; CI's apply leg is a manual
workflow_dispatch, not a merge trigger — so this PR is for durability, not for the effect to happen).Guarded sequence (
core/terraform/live/zitadel, provider v3.8.7, state serial 59 → 60):Read back from live Zitadel after the apply:
Client credentials untouched — sha256 prefix of state's
lab_director_client_id/_secretequals the cockpit's runtimelab-director/cockpit-oidcSecret (c3e7f31a7b1036d1/906cb49dba2cb214). Nothing to re-seed in OpenBao.Residual full plan (deliberately not applied):
jesus@reynoza.orguser+grantcreate(operator-made console deletion — a full apply would resurrect it) andwilliam@reynoza.orgupdate(HOMELAB-1867). Noid_token_role_assertiondiff remains.CI: runs #1766/#1767 on head
7da8aea— all green (terraform-validate,cockpit-e2e,cockpit-test,dispatcher-test,helm-validate,shellcheck,yaml-lint,alert-receiver-test,receiver-test).Operator-facing: sign out on the 403 page (or clear
ld_cockpit_session) and sign in again — the session cookie is sealed for 24h, so the staleroles: nonecookie must go. Expected after: no 403 at all (shell loads); if the 403 were ever seen again,your rolesmust now listoperator, notnone.Review verdict — APPROVE (no blockers)
Reviewed at head
7da8aea7621a4f6658208e2fd46ab4de4f238a29(baseorigin/live=5215bcb0) by afresh, independent reviewer (separate context/process, primary sources only: the PR diff, the ticket's
acceptance criteria, and
.rpiv/artifacts/build/ld3-operator-role-fix.md).Diff scope verified.
git diff origin/live..7da8aea -- core/terraform/live/zitadel/= exactly onefile,
core/terraform/live/zitadel/main.tf, +28/−2. The only functional line isid_token_role_assertion = trueinsidezitadel_application_oidc.lab_director; every other added line isa comment.
origin/livehas 0 occurrences of the attribute, head has 1 — no duplicate, no secondresource, nothing outside that resource block, nothing in
prod/.Matches the applied live state. Diff value
true== liveGET /management/v1/projects/364000638805213784/apps/393380591770272287 → idTokenRoleAssertion: trueand
terraform state show … id_token_role_assertion = true. The apply telemetry (serial 59→60,0 added, 1 changed, 0 destroyed,clientId 393380591787049503unchanged, client id/secret hashes equalthe cockpit's runtime secret) shows an in-place update of this one attribute. The git change therefore
makes config match reality; it changes nothing at runtime today.
Not dangerous. No secret, RBAC, network-policy, module, provider, or schema change; no destroy /
force-new path. In-place
updateon an existing OIDC app is the only possible effect.Claim honesty. The prose does not claim to fix the project flag, does not claim a scope change, and
does not claim to touch other resources — it explicitly disclaims both (
project_role_assertioncoversuserinfo only;
urn:zitadel:iam:org:project:rolesis the claim name, not a Zitadel scope). Corroboratedin-repo:
project_role_assertion = trueon the HOMELAB project is untouched, andlab-director/cockpit/auth/roles.goparses the claim from the verified ID token with no userinfofallback. The comment correction is accurate: the old wording wrongly attributed the claim to the
project flag, and the new wording matches the attribute in the same block.
Merge-safety clarification (for the record). The full-apply hazard this PR mentions (an untargeted
terraform applyresurrectingjesus@reynoza.org, plus thewilliam@reynoza.orgdrift) is pre-existingand cannot trigger from this merge: the
zitadelroot was removed from Atlantis' remit (#582 /HOMELAB-1865) and the infra-apply workflow is broken (HOMELAB-1866). That risk is owned by
HOMELAB-1866/1867 and only materialises once the pipeline is repaired — a concern, not a blocker here.
The verbatim diff itself contains no jesus/william text (the attribution in the earlier diagnosis artifact
refers to the CI apply leg, not to lines added by this PR).
CI. Green on head: runs #1766 (alert-receiver-test, cockpit-e2e, cockpit-test, dispatcher-test,
helm-validate, receiver-test) and #1767 (shellcheck, terraform-validate, yaml-lint) all success;
combined commit status
success(10 statuses, including atlantis0/0 projects applied successfully).Blockers: none.
Concerns (non-blocking, tracked elsewhere): full-plan human-user drift → HOMELAB-1866/1867;
Zitadel provider constraint remains unpinned (
>= 2.10.0), pre-existing; the comment cites upstreamZitadel source paths not vendored in this repo (cosmetic — the behaviour is confirmed by the live
read-back).
Merging as harmless/additive (single in-place attribute, git-to-reality).