HOMELAB-2479: feat(zitadel): aaron-site OIDC app (separate project, role_check=false) #904
No reviewers
Labels
No labels
operator-review
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Homelab/infra-core!904
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "plane/HOMELAB-2479-aaron-site-zitadel"
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?
HOMELAB-2479 PR #1 of 3 — Zitadel half of the OIDC gate for Aaron's personal
site (
aaron.reynoza.orgapex) behind oauth2-proxy. This PR touches ONLY thezitadel terraform root (
core/terraform/live/zitadel/); no cluster manifests,no OpenBao writes, nothing applies on merge (the zitadel root runs only by
manual
Infrastructure Applydispatch).What
zitadel_project.aaron_site— a new SEPARATE project namedaaron-sitewith
project_role_check = falseANDproject_role_assertion = false.This is Aaron's resolved decision (via El Director, 2026-11-09: "Guests
and anyone with a role" — every registered Zitadel user can log in). The
HOMELAB project is NOT reused: its role model (role_check=true,
role_assertion=true) would demand a project role the site's users do not
have.
project_role_checkis already used at provider>= 2.10.0in thisfile (
zitadel_project.homelab, line ~66), so the attribute is first-classat the version in use, not a guess. Nothing about the HOMELAB project, its
roles, or user grants is touched.
zitadel_application_oidc.aaron_site— WEB app, code flow,OIDC_AUTH_METHOD_TYPE_POST(oauth2-proxy posts the secret in the tokenrequest, the sonarr/bazarr precedent), redirect
https://aaron.reynoza.org/oauth2/callback, post-logouthttps://aaron.reynoza.org/,dev_mode = false(Zitadel rejects httpredirects on WEB apps). No
id_token_role_assertion— the site reads noroles (no
--allowed-group), unlike the cockpit.random_password.aaron_site_oauth2_cookie_secret— 32 chars,special = false(sonarr's shape), plus a sensitive output for it, so thecookie secret has a named source in the seed contract.
aaron_site_urllocal at the END of the locals block (the block's owncomment documents why:
terraform fmtre-padding). Apex, no subdomain.aaron_site_client_id,aaron_site_client_secret,aaron_site_cookie_secret, eachdescriptionnaming its seed propertyexactly — the description IS the contract (cockpit/OmniRoute precedent).
kubernetes_secret_v1(cockpit precedent, quoted in the file): theaaron-sitenamespace does not exist until ArgoCD creates it, and theclient secret stays out of terraform state.
Secrets path — deviation from the original brief, with reasoning
The original brief said OpenBao
kv/homelab/aaron-site-oidc. That path cannotwork as written, on measured evidence:
kv/homelabpath. The KV v2 mount is namedkv(prodterraform/openbao.tf,path = "kv"); objects arekv/<object-name>.kv/data/app-credentials(prodterraform/openbao.tf, policyagent-pi, verbatim: scoped RW on exactlyONE kv object, no delete, no other paths). No agent identity can create or
write a new
kv/aaron-site-oidcobject — doing it properly would meanwidening an OpenBao policy, a dangerous-class change with no benefit here:
ESO reads any
kv/*object, so a new object buys nothing for the consumer.it": properties of the shared
kv/app-credentialsobject.Seed properties:
aaron_site_oidc_client_id,aaron_site_oidc_client_secret,aaron_site_cookie_secret.Exact post-merge sequence (nothing applies automatically)
Terraform here writes nothing to OpenBao, and the
Infrastructure Applyworkflow (prod
.forgejo/workflows/infra-apply.yaml,target: zitadel) hasno
baostep — there is no automation for the seed and there cannot be inthe current design. After this PR merges:
Infrastructure Applywithtarget: zitadel. This createsthe
aaron-siteproject (role check off) and the OIDC app. Client secretsare
sensitiveand will not appear in the apply log.VM, as the pi-agent AppRole —
bao login -method=approle, env at~/.config/openbao/approle.env; HOMELAB-1485 pattern, same asjellyfin/cockpit):
kv patch, NOTkv put— a bare put REPLACES the shared object and dropsevery other app's credentials.
ExternalSecret (
refreshInterval: 1h) reads the three properties fromkv/app-credentialsand createsaaron-site-oidc-secretsin theaaron-sitenamespace; the oauth2-proxy pod then leavesCreateContainerConfigError(expected, loud and harmless until then).Verification done
terraform fmtclean (fmt -checkpasses).terraform init -backend=false+terraform validate: Success! Theconfiguration is valid. (Full backend init needs the prod S3 backend +
credentials; deliberately not attempted from this session — no backend creds
were improvised.)
base_domain,zitadel_org_idalreadyexist); nothing outside
main.tf/outputs.tftouched.Out of scope (follow-ups, named not smuggled)
aaron-siteapp manifests — namespace, static-siteDeployment, oauth2-proxy Deployment, Service, ExternalSecret,
CiliumNetworkPolicy, HTTPRoute.
check-fqdns.shapexsupport.
Verdict of record — ld-reviewer — head
049e16aabb: APPROVEmodel: omniroute/deepseek-flash
session: 01a1c227-6f91-76de-ae4b-7810b6a079a3
Blockers: None.
What I reviewed (primary sources only)
origin/live...HEADin a read-only worktree at head049e16aa;git diff --name-only=core/terraform/live/zitadel/main.tf,core/terraform/live/zitadel/outputs.tf— exactly the two files the brief allows, +99/-0.main.tfin context (locals block,zitadel_project.homelab, the radarr/sonarr/lab_director OIDC blocks, Project Roles section),outputs.tf,versions.tf.GET /repos/Homelab/infra-core/pulls/904.terraform fmt -check -diff .→ clean;terraform validate→ Success! The configuration is valid. (provider lock pinszitadel 3.8.7, constraint>= 2.10.0, soproject_role_check/has_project_check/private_labeling_settingare all first-class at the version actually in use — independently confirmed, not taken from the PR body.)Correctness vs acceptance criteria
zitadel_project.aaron_siteis a new resource withname = "aaron-site",project_role_check = false,project_role_assertion = false. The diff contains no hunk insidezitadel_project.homelabor any of its roles/grants; the app points atzitadel_project.aaron_site.id, not.homelab.id. Criterion met.OIDC_APP_TYPE_WEB,OIDC_RESPONSE_TYPE_CODE,OIDC_GRANT_TYPE_AUTHORIZATION_CODE,OIDC_AUTH_METHOD_TYPE_POST, redirect"${local.aaron_site_url}/oauth2/callback"=https://aaron.reynoza.org/oauth2/callback,post_logout_redirect_uris = [local.aaron_site_url],dev_mode = false, noid_token_role_assertion. Criterion met; the POST choice matches the radarr/sonarr/prowlarr/bazarr oauth2-proxy precedent already in this file (verified in the file, not just quoted in the comment).random_password.aaron_site_oauth2_cookie_secretislength = 32,special = false— byte-identical shape to the four existing*_oauth2_cookie_secretresources. Three outputs, allsensitive = true, each description naming its seed property exactly (aaron_site_oidc_client_id,aaron_site_oidc_client_secret,aaron_site_cookie_secret), matching thelab_director_*/pangolin_*description convention in the same file. Criterion met.kubernetes_secret_v1, nonull_resource, nolocal-exec; the new resources are one project, one app, onerandom_password. The seeds are documented as an out-of-band step. Criterion met.variables.tfuntouched;base_domainandzitadel_org_idboth already exist. Criterion met.aaron_site_urlis the last assignment in the block, directly afterlab_director_url, respecting the block's ownterraform fmtre-padding note;fmt -checkconfirms no re-padding of neighbours. Criterion met.fmt/validatemyself rather than trusting the "Verification done" section, and both reproduce. The wildcard-cert claim (aaron.reynoza.orgis adnsNamesentry ofcore/manifests/cert-manager/wildcard-certificate.yaml) is also true in this repo.sensitive; the cookie secret is stored in state exactly like the four existing cookie secrets — no new class of state exposure, no plaintext committed, no token or credential in the diff.Concerns (non-blocking)
C1 —
has_project_check = trueis copied from the HOMELAB project without justification, and it narrows the stated "any registered Zitadel user" intent. In Zitadel, Check for Project on Authentication means the authenticating user's organization must own or be granted the project. Same-org users pass, so today's single-org instance is unaffected; but a user registered into a different organization would be denied at login with no role check involved — the opposite of the operator's "Guests and anyone with a role" decision. The block's comment carefully justifiesproject_role_check = falseandproject_role_assertion = falseand then says "any authenticated user passes", which is only accurate for the owning org while this flag is on. Eitherhas_project_check = false(the literal reading of the decision) or one sentence stating that all admitted users live invar.zitadel_org_idwould close this. Worth settling before PR #2 makes the site reachable, because it fails at the IdP rather than in the app.C2 — post-logout URI is registered without the trailing slash while the PR body writes it with one.
local.aaron_site_urlis"https://aaron.reynoza.org", so the registered post-logout URI ishttps://aaron.reynoza.org; the PR body and the follow-up scope sayhttps://aaron.reynoza.org/. Zitadel compares redirect URIs exactly, so if the oauth2-proxy config in PR #2 ever sends the slash variant, logout will be rejected withinvalid redirect uriwhile login keeps working — a confusing, app-side-only symptom. Recording which form is canonical here keeps PR #2 honest; no change needed if PR #2 adopts the slash-less value verbatim.Suggestions
S1 — the OpenBao path deviation (shared
kv/app-credentialsproperties instead ofkv/homelab/aaron-site-oidc) is argued fromprod terraform/openbao.tfpolicyagent-pi, which is not in this repository, so it is unverifiable from this diff. The reasoning is consistent with the cockpit/OmniRoute precedent already documented in this file and with "ESO reads anykv/*object, so a new object buys nothing", so I treat it as sound; it belongs on HOMELAB-2479 (or PR #2's ExternalSecret review) as a verifiable citation rather than only in a code comment.S2 —
aaron_site_client_idis markedsensitivealthough a client ID is public. This matches every sibling output in this file, so it is convention rather than a defect; noting it only so a future reader does not "fix" one output and break symmetry.No blocker findings. The change is additive, applies on no pipeline event (manual
Infrastructure Applyonly), touches nothing outside the two allowed files, formats and validates clean at the pinned provider, and does what the PR body says it does. Approve.Verdict of record — ld-reviewer — head
049e16aabb: APPROVEmodel: omniroute/deepseek-flash
session: 01a1c227-6fbb-76de-ae4b-78137ce46b84
Blockers: None.
Review — seat 2 (independent pass), infra-core PR #1 of 3 for HOMELAB-2479
Reviewed from primary sources only:
git diff origin/live...HEADat049e16aa, the fullmain.tf/outputs.tf/versions.tfat that head, theprovider schema of the pinned provider, and the companion prod manifests.
Acceptance criteria — all met
zitadel_project.aaron_siteis new,name = "aaron-site",project_role_check = false,project_role_assertion = false.zitadel_project.homelabis untouched (the diff adds no line insideit) and
zitadel_application_oidc.aaron_site.project_idpoints at the newproject, so HOMELAB is neither reused nor modified.
OIDC_APP_TYPE_WEB,OIDC_RESPONSE_TYPE_CODE,OIDC_GRANT_TYPE_AUTHORIZATION_CODE,OIDC_AUTH_METHOD_TYPE_POST,redirect_uris = ["${local.aaron_site_url}/oauth2/callback"]→https://aaron.reynoza.org/oauth2/callback,post_logout_redirect_uris = [local.aaron_site_url],dev_mode = false, noid_token_role_assertion/access_token_role_assertion.random_password.aaron_site_oauth2_cookie_secret, length32,
special = false. Three sensitive outputs, each description naming itsseed property exactly.
kubernetes_secret_v1, nonull_resource/local-exec, novault_*/baoreference anywhere in the diff. Aterraform-owned Secret would also have failed to apply (namespace is created
by ArgoCD), which is the cockpit precedent quoted in the file.
main.tf+outputs.tf, 99 insertions, 0 deletions.variables.tfuntouched (no new variable is needed).aaron_site_urlis thelast entry in
locals, afterlab_director_url, exactly as the block's owncomment requires.
Seat-2 checks (the ones an independent pass should re-derive)
Provider attribute validity — verified against the schema, not the prose.
versions.tfpinszitadel >= 2.10.0;.terraform.lock.hclresolves it to3.8.7. I dumped that provider's schema (
terraform providers schema -jsonfrom a temp root using the checked-out plugin, filesystem mirror, no network
write) and every attribute and enum used here exists:
zitadel_project:has_project_check,private_labeling_setting(
PRIVATE_LABELING_SETTING_UNSPECIFIEDis one of the three documentedvalues),
project_role_assertion,project_role_check.zitadel_application_oidc:app_type(OIDC_APP_TYPE_WEB),auth_method_type(OIDC_AUTH_METHOD_TYPE_POST),response_types(
OIDC_RESPONSE_TYPE_CODE),grant_types(
OIDC_GRANT_TYPE_AUTHORIZATION_CODE),redirect_uris,post_logout_redirect_uris,dev_mode.terraform validateat the head: Success! The configuration is valid.(local, plugin cache) and CI run #3431
terraform-validateis green;terraform fmt -check -diffon both files exits 0 — the diff isfmt-clean.
Output property names vs the ExternalSecret contract — exact match. The
companion prod file (
manifests/aaron-site/externalsecret.yaml,plane/HOMELAB-2479-aaron-site-app) readskey: app-credentialswithproperties
aaron_site_oidc_client_id,aaron_site_oidc_client_secret,aaron_site_cookie_secret, mapped toclient-id/client-secret/cookie-secret. The three output descriptions here name those three stringscharacter-for-character, and the seed snippet in the PR body uses exactly the
output names that now exist (
aaron_site_client_id,aaron_site_client_secret,aaron_site_cookie_secret). A mismatch here would have silently put theoauth2-proxy pod in
CreateContainerConfigError; there is none.State sensitivity — consistent, not a regression. The
client_secretinstate question: this root already stores client secrets for outline, radarr,
sonarr, prowlarr, bazarr, omniroute, pangolin and lab_director, and already
stores four
random_passwordcookie secrets (radarr/sonarr/prowlarr/bazarr) instate. Adding
zitadel_application_oidc.aaron_site.client_secretandrandom_password.aaron_site_oauth2_cookie_secret.resultsits inside that sameexisting trust boundary (S3 state backend, sensitive outputs, no new writer),
so it is the house pattern rather than a widening. Nothing new is written
outside state, and the client secret stays out of the apply log because all
three outputs are
sensitive = true.PR-body claims vs the code. Checked, because a wrong sequencing claim is
how a merge gets treated as "done":
workflow (
lint.yamlonly,terraform-validate), and the apply path is theprod
Infrastructure Applydispatch. Merging this PR changes no live object.baostep in the apply workflow / terraform writes nothing to OpenBao" —consistent with the diff (no OpenBao resource, no
local-exec).bao kv patch, notkv put, and the inline file commentcalls out that
putwould replace the shared object. Correct, and the samewarning as the cockpit/jellyfin precedents.
Concerns (non-blocking — no action required before merge)
C1 —
has_project_check = trueis copied from the HOMELAB project and isorg-scoped, not user-scoped. This is the one setting left that can still
refuse a legitimate login. The pinned provider documents it as "ZITADEL checks
if the org of the user has permission to this project" — unlike
project_role_check("checks if the user has at least one role on thisproject"), which is correctly off here. In a single-org estate every user is
in
var.zitadel_org_idand the check passes, and it cannot affect theoperator's own login, so the "every registered user can log in" decision is
honoured in practice. The residual risk is a user from a different Zitadel
org: they would need an org-level project grant, and the site's intent says
they should get in. Two cheap responses, either fine: leave it (matches
HOMELAB) and make the post-merge live proof include a login from a second,
non-operator account rather than only the operator's; or set
has_project_check = falseif a second org is ever introduced. Recording it sothe live proof tests the actual claim instead of assuming it.
C2 —
post_logout_redirect_uris = ["https://aaron.reynoza.org"]carries notrailing slash, while the PR body writes the post-logout target as
https://aaron.reynoza.org/. oauth2-proxy derives its end-sessionpost_logout_redirect_urifrom the--redirect-url'sscheme://host/, i.e.with the slash. If Zitadel matches that value exactly, logout would land on a
Zitadel "redirect URI not registered" page rather than back on the site. Note
this is pre-existing house pattern, not introduced here: sonarr, bazarr,
argocd, grafana and the rest all register the bare
https://<host>with no trailing slash, so this PR is consistent with thefile. Sign-in (the acceptance criterion) is unaffected. Worth one line in the
post-merge proof — sign out and confirm where you land — rather than a change
here that would make aaron-site the odd one out.
What I did not do
I did not run
terraform planorapply(no backend credentials, and a planwould need live Zitadel). Provider-attribute validity was established from the
pinned provider's own schema plus
terraform validate, which is the strongestread-only evidence available for this lens.
Verdict: approve at
049e16aabb.Verdict: APPROVED
model: omniroute/deepseek-flash
head:
049e16aabbsession: 01a1c3f1-0ba2-745c-9310-59c9ab365970
tier: worker
APPROVE — exact head
049e16aabb95844c20e387539cb7becf7d56fb37(short049e16aa), merge-base with live63f21898.model: omniroute/deepseek-flash
What I reviewed
I read the diff myself at the pinned head, from an anonymous clone of the repo fetching
refs/pull/904/head(no credential involved). The PR's own commit049e16aachanges exactly two files, both inside the only root the ticket allows:core/terraform/live/zitadel/main.tf(+73)core/terraform/live/zitadel/outputs.tf(+26)99 insertions, 0 deletions. No other file is touched by the commit.
Diff-scope note (measured, not assumed). The branch's parent
cf8ad32is behind live's tip63f21898, so a plain tree difflive..headreports 23 files, 21 of them unrelated (plane chart, forgejo-runner, sbx-*, ghostty fixture, scripts, workflows). Those are live commits this branch simply predates — they are reverts shown by the comparison, not changes in this PR. The true three-dot / merge-base diffcf8ad32..049e16aais the two files above. Landing worker: measure the diff against the merge base, not against live's tip.Acceptance criteria (HOMELAB-2479 aaron-site v1)
aaron-site, role check off —zitadel_project.aaron_sitewithproject_role_check = false,project_role_assertion = false, and nozitadel_project_roleadded, so the token request can never demand a project role. PASShttps://aaron.reynoza.org/oauth2/callback—zitadel_application_oidc.aaron_sitesetsredirect_uris = ["${local.aaron_site_url}/oauth2/callback"]withlocal.aaron_site_url = "https://${var.base_domain}"(apex, no label prepended), which yields exactly the required URI forbase_domain = aaron.reynoza.org. PASSaaron_site_client_id,aaron_site_client_secret,aaron_site_cookie_secretall carrysensitive = true. PASSkubernetes_secretresource — none added. (The 14kubernetes_secret_v1resources in the file are pre-existing for other apps.) PASScore/terraform/live/zitadel/. PASSSolution quality and codebase fit
OIDC_APP_TYPE_WEB,OIDC_RESPONSE_TYPE_CODE,OIDC_GRANT_TYPE_AUTHORIZATION_CODE,OIDC_AUTH_METHOD_TYPE_POST,dev_mode = false,post_logout_redirect_uris = [site url].random_password.aaron_site_oauth2_cookie_secret(length 32,special = false) matches the radarr/sonarr/prowlarr/bazarr cookie secrets; 32 bytes is a valid oauth2-proxy cookie-secret length.zitadel_project.homelab's shape (org_id,private_labeling_setting,lifecycle { ignore_changes = [org_id] }) and the HCL is aligned consistently with the surrounding blocks.project_id = zitadel_project.aaron_site.id, not the HOMELAB project, so the site cannot inherit the HOMELAB role model — the stated reason for the separate project holds in the code. The client secret is not materialised as a terraform-managed Secret, so it stays out of cluster state owned by terraform, and no secret literal appears in the diff.Claim honesty (I checked the load-bearing comments)
core/manifests/cert-manager/wildcard-certificate.yamldnsNamescontainsaaron.reynoza.orgexactly. TRUE.kubernetes_secret_v1HERE, same as the cockpit block above": the block above (zitadel_application_oidc.lab_director) has nokubernetes_secret_v1either. TRUE.dev_mode = falsebecause Zitadel rejects http redirect URIs on WEB apps: consistent with every other WEB app in the file.No blocker findings.
Verification limits
No
terraform/tofubinary in the review sandbox, soterraform validateandfmt -checkwere not run; the HCL was read line by line and its alignment matches the neighbouring blocks. The OpenBao policy claim (that the pi-agent AppRole may write onlykv/data/app-credentials) lives in the prod repo and was not verifiable from this repo.APPROVE on exact head
049e16aabb95844c20e387539cb7becf7d56fb37.Verdict: APPROVED
model: omniroute/deepseek-flash
head:
049e16aabbsession: 01a1c405-259e-745c-9310-59cafb22c7a8
tier: worker
APPROVE — independent review seat B (aaron-site Zitadel OIDC, HOMELAB-2479)
model: omniroute/deepseek-flash
HEAD REVIEWED:
049e16aabb(verified equal to the request pin)MEASUREMENT: three-dot against the merge-base
cf8ad320(the branch predates the live tip63f21898, so origin/live..HEAD would have been wrong). Result: 2 files, 4 hunks, +99/-0, one commit.Scope of the diff is exactly the zitadel terraform root:
core/terraform/live/zitadel/main.tf (+73)
core/terraform/live/zitadel/outputs.tf (+26)
ACCEPTANCE CRITERIA, each checked in the diff itself:
aaron-sitewith the role check OFF — present, name = "aaron-site", org_id = var.zitadel_org_id, project_role_check = false, and NO zitadel_project_role is defined for it, so no role-gated token request can form. The separate-project choice is sound and the in-file comment gives the correct reason (the HOMELAB project sets project_role_check = true at main.tf:48, so reusing it would demand a role no user has).CODEBASE FIT
bao kv patch app-credentialsstep), omniroute and pangolin are wired. Thekv patch(neverkv put) warning in the block comments is the correct one for a shared object.SECURITY / TENANT SCOPING
No privilege widening: no role is asserted, no project grant is created, no IAM, RBAC or policy object is touched, and no secret value is introduced into the diff or into state beyond the existing pattern. The app admits authenticated users only, which is the stated intent.
BASIS AND LIMITS
Verified from this repo at the pinned head. Two claims in the block comments are cross-repo and cannot be read from infra-core anonymously (the prod tfvars base_domain value, and the agent-pi policy in prod terraform/openbao.tf); they are consistent with the in-repo evidence above and fall outside this PR's stated scope, which forbids touching prod. terraform fmt/validate was not run here (no terraform binary in this sandbox); the
=alignment in both new blocks is manually consistent with the surrounding blocks, and CI remains the authority.No blocking finding. Approving on this head.
Verdict: APPROVED
model: omniroute/kimi-pool
head:
049e16aabbsession: bridge-seatB2-904
tier: premium
Seat B2 (premium, independent) — APPROVE. Reviewed the three-dot diff at the pinned head
049e16aaagainst HOMELAB-2479 acceptance criteria, forming my own judgement from the diff and the file's precedents.Scope measured:
git diff $(git merge-base origin/live 049e16aa) 049e16aa= 2 files, 4 hunks, 99 insertions. Only the zitadel terraform root is touched (core/terraform/live/zitadel/main.tf,outputs.tf). Head verified == PR live head.Acceptance criteria, each verified in the diff itself:
zitadel_project.aaron_site(name "aaron-site"),project_role_check = false. The one subtle point —has_project_check = true— does NOT conflict with "every registered user can log in": per Zitadel's own semantics (zitadel/zitadel discussion #8397), the project check is ORG-level ("the organization of the user ... needs to have the project, either as owner or as granted"), not a per-user membership/role check. All registered users are in the owning org, so they pass; only out-of-org users are excluded. Correct and arguably preferable scoping. Withproject_role_checkoff and no roles defined, no user is asked for an ungrantable role.https://aaron.reynoza.org/oauth2/callback(derived fromvar.base_domain, matching the whole file). CODE response + AUTHORIZATION_CODE grant,auth_method_type = POST(sonarr/bazarr precedent — oauth2-proxy sends the secret in the token POST body). Correct for oauth2-proxy.post_logout_redirect_uris = [https://aaron.reynoza.org]→ apex. Correct.dev_mode = false. Correct (Zitadel rejects http redirects on WEB apps otherwise).aaron_site_client_id/_client_secret/_cookie_secret), allsensitive = true. Correct; descriptions name the exact OpenBaokv/app-credentialsseed properties.kubernetes_secret*resource — confirmed (the only k8s-secret mention in the diff is the explanatory comment). Matches the lab_director "cockpit" block, which also creates none (its namespace doesn't exist at apply time; secret reaches the pod via ExternalSecret). Honest "same as the cockpit" claim.vault/baoresource anywhere in the zitadel root, none added. Thebao kv patchseed is documented out-of-band operator action, and thepatch-not-putwarning (don't clobber the shared object) is a correct, valuable note.Codebase fit / solution quality:
random_password.aaron_site_oauth2_cookie_secret= length 32, special false — byte-for-byte the radarr/sonarr/bazarr cookie-secret pattern (32 = valid AES-256 key length for oauth2-proxy). random provider already declared in versions.tf.id_token_role_assertion(unlike cockpit) — correct: no project roles exist and oauth2-proxy reads none.project_role_assertion = falseand the separate-project rationale are well-justified in comments; consistent with the file's heavily-documented style.Security / tenant scoping: client secret stays out of any terraform-managed k8s Secret and out of the state beyond the sensitive output; org-level project check scopes login to the owning org. Claim honesty is clean — every comment claim I checked (file/hunk counts, no-k8s-secret, no-openbao-resource, provider-version support for
project_role_check, "same as cockpit") holds against the diff.No blockers. Ship it.
Verdict: APPROVED
model: omniroute/deepseek-flash
head:
049e16aabbsession: sbx-lab-b3-infra-core-904
tier: worker
APPROVE — independent review seat B3, explicit model omniroute/deepseek-flash (no pools).
HEAD AND MEASUREMENT
Head reviewed:
049e16aabb("049e16aa"). I checked it first with ananonymous
ls-remote refs/pull/904/head; it equals the pinned head. I measured the PR with theTHREE-DOT diff against the merge-base with origin/live (merge-base
cf8ad320, live63f21898):2 files, +99/-0, 4 hunks, both files under core/terraform/live/zitadel/. Nothing else in the tree
is touched by this PR.
ACCEPTANCE CRITERION -> what the diff actually contains
zitadel_project.aaron_site, name "aaron-site",project_role_assertion=false, project_role_check=false. The comment's reasoning is the right way
round: a project that defines no roles, with role_check left on, fails the code-flow exchange.
local.aaron_site_url = "https://${var.base_domain}"andredirect_uris = ["${local.aaron_site_url}/oauth2/callback"]-> apex /oauth2/callback. The apexderivation is the same pattern radarr/sonarr/bazarr use from the same variable.
post_logout_redirect_uris = [local.aaron_site_url](apex),dev_mode = false.sensitive = true, each description naming the exact seed property.kubernetes_secret_v1for aaron-site — I confirmed it is absent, and the cockpit block citedas the precedent really does the same (I read that block, not just the comment).
documented out-of-band
bao kv patchstep, and the comment correctly warns that a barekv putwould replace the shared object.
TWO CLAIMS I VERIFIED RATHER THAN TRUSTED
wildcard-certificate.yaml lists dnsNames ["aaron.reynoza.org", "*.aaron.reynoza.org"]: the apex
is explicit. A wildcard by itself would NOT have covered the apex, so this claim mattered.
random_password.aaron_site_oauth2_cookie_secretlength=32 / special=false — identical to theradarr, sonarr, prowlarr and bazarr blocks in the same file.
CI AT THIS HEAD
11/11 statuses success, including "Lint & Validate / terraform-validate" (the repo runs
terraform init -backend=false+terraform validateacross every core/terraform dir). There isno terraform binary in my review sandbox, so I lean on that job plus a line-by-line read of the HCL;
the new block is attribute-for-attribute consistent with the homelab block above it.
NON-BLOCKING OBSERVATIONS (I am not asking for a change on these)
has_project_check = trueis carried over from the HOMELAB project. Its meaning isorganization-level, not role-level: the authenticating user's org must own the project or hold a
grant of it. While every user lives in the org that owns it (org_id = var.zitadel_org_id)
everyone passes; a user from a second org would meet "the organization of the user must be
granted to the project". Given the block's comment claims "every registered Zitadel user", that
scope is worth one sentence beside the flag.
terraform/main.tf and no openbao.tf (secrets/app-credentials.yaml does exist, so the
shared-object reasoning holds); the citation looks stale rather than wrong.
cannot be checked from here. The three output descriptions must be exactly what that PR reads.
#904 applied from mgmt (targeted, 2026-11-10T12:44Z)
Applied from the mgmt VM, not from CI — throwaway docker container (
hashicorp/terraform:1.15.8,--network host --add-host=zitadel.aaron.reynoza.org:127.0.0.1) +kubectl -n zitadel port-forward svc/zitadel 8080:8080. Reason: Cilium's gRPC-trailers bug breaks the in-cluster Zitadel dial, and the Forgejo runner is in host mode (runs-on: infra) so it cannot take an--add-host(job-levelcontainer.optionsExtraHosts is filtered by act, runner-level has no job container). The durable CI fix (hostAliases on sts forgejo-runner, plus #911) is daylight work; #911 stays open and unmerged.Code applied: infra-core
liveat2b17d62f(the #904 merge itself; ancestry-verified, and it does not contain #911's heada6752c7f).Applied with
-target, and why: the untargeted saved plan was 3 add / 2 change / 0 destroy. The two changes were pre-existing user-profile drift in the code (zitadel_human_user.additional["enuila1989@gmail.com"],["william@reynoza.org"]: gender MALE→UNSPECIFIED,nick_name→ null, preferred_language → und, and last_nameNuilavs configUnuila). That is not #904's scope, so the plan was narrowed to exactly the three aaron-site resources:terraform showon the saved plan: 3 to add, 0 to change, 0 to destroy (only those 3 addresses).999115e0c07743c401f2639f0417a8aaafed9f3f6b05efa24416f2a33bd41482terraform apply <file>(the saved plan, one run):Apply complete! Resources: 3 added, 0 changed, 0 destroyed.zitadel_project.aaron_siteid398993465267980094,zitadel_application_oidc.aaron_siteid398993465335088958,random_password.aaron_site_oauth2_cookie_secret.zitadel_project.homelabunchanged, allzitadel_human_user.*untouched (users=unchanged).Auth route: a 15-minute access token minted from the Zitadel service-user key via the JWT-bearer grant (REST), kept in a 0600 file inside a 0700 tmp dir, never printed and never in argv. Provider auth via
provider_override.tfin the throwaway copy (no repo change), sha25615879203f777590bbaa4852fa69eda5d7525f305ac2772d632aa6b98595a331a(the earlier run recorded768e290b…a5b6; the only intended difference is the mount path of the token file, and the comments):Secrets seeded:
kv/app-credentialspatched once (merge, not put) withaaron_site_oidc_client_id,aaron_site_oidc_client_secret,aaron_site_cookie_secret— metadata version 29 → 30, property names 52 → 55, all pre-existing key names still present, stored values verified equal to the terraform outputs. Throwaway dir shredded; token revoked.Not done here: no user-profile changes of any kind. The profile drift goes to a daylight PR once Aaron answers which surname is correct for Manuel.