HOMELAB-2479: feat(zitadel): aaron-site OIDC app (separate project, role_check=false) #904

Merged
pi-landing merged 1 commit from plane/HOMELAB-2479-aaron-site-zitadel into live 2026-11-10 10:39:36 +00:00
Collaborator

HOMELAB-2479 PR #1 of 3 — Zitadel half of the OIDC gate for Aaron's personal
site (aaron.reynoza.org apex) behind oauth2-proxy. This PR touches ONLY the
zitadel terraform root (core/terraform/live/zitadel/); no cluster manifests,
no OpenBao writes, nothing applies on merge (the zitadel root runs only by
manual Infrastructure Apply dispatch).

What

  • zitadel_project.aaron_site — a new SEPARATE project named aaron-site
    with project_role_check = false AND project_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_check is already used at provider >= 2.10.0 in this
    file (zitadel_project.homelab, line ~66), so the attribute is first-class
    at 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 token
    request, the sonarr/bazarr precedent), redirect
    https://aaron.reynoza.org/oauth2/callback, post-logout
    https://aaron.reynoza.org/, dev_mode = false (Zitadel rejects http
    redirects on WEB apps). No id_token_role_assertion — the site reads no
    roles (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 the
    cookie secret has a named source in the seed contract.
  • aaron_site_url local at the END of the locals block (the block's own
    comment documents why: terraform fmt re-padding). Apex, no subdomain.
  • Sensitive outputs aaron_site_client_id, aaron_site_client_secret,
    aaron_site_cookie_secret, each description naming its seed property
    exactly — the description IS the contract (cockpit/OmniRoute precedent).
  • NO kubernetes_secret_v1 (cockpit precedent, quoted in the file): the
    aaron-site namespace does not exist until ArgoCD creates it, and the
    client 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 cannot
work as written, on measured evidence:

  1. There is no kv/homelab path. The KV v2 mount is named kv (prod
    terraform/openbao.tf, path = "kv"); objects are kv/<object-name>.
  2. The pi-agent AppRole can write ONLY kv/data/app-credentials (prod
    terraform/openbao.tf, policy agent-pi, verbatim: scoped RW on exactly
    ONE kv object, no delete, no other paths). No agent identity can create or
    write a new kv/aaron-site-oidc object — doing it properly would mean
    widening 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.
  3. This is exactly what the brief meant by "the same way the cockpit app does
    it": properties of the shared kv/app-credentials object.

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 Apply
workflow (prod .forgejo/workflows/infra-apply.yaml, target: zitadel) has
no bao step — there is no automation for the seed and there cannot be in
the current design. After this PR merges:

  1. Dispatch Infrastructure Apply with target: zitadel. This creates
    the aaron-site project (role check off) and the OIDC app. Client secrets
    are sensitive and will not appear in the apply log.
  2. One-off seed step (performed by the orchestrator/operator on the mgmt
    VM, as the pi-agent AppRole — bao login -method=approle, env at
    ~/.config/openbao/approle.env; HOMELAB-1485 pattern, same as
    jellyfin/cockpit):
    bao kv patch app-credentials \
      aaron_site_oidc_client_id="$(terraform output -raw aaron_site_client_id)" \
      aaron_site_oidc_client_secret="$(terraform output -raw aaron_site_client_secret)" \
      aaron_site_cookie_secret="$(terraform output -raw aaron_site_cookie_secret)"
    
    kv patch, NOT kv put — a bare put REPLACES the shared object and drops
    every other app's credentials.
  3. ESO materialises the Secret: once the prod app PR (PR #2) lands, the
    ExternalSecret (refreshInterval: 1h) reads the three properties from
    kv/app-credentials and creates aaron-site-oidc-secrets in the
    aaron-site namespace; the oauth2-proxy pod then leaves
    CreateContainerConfigError (expected, loud and harmless until then).

Verification done

  • terraform fmt clean (fmt -check passes).
  • terraform init -backend=false + terraform validate: Success! The
    configuration is valid.
    (Full backend init needs the prod S3 backend +
    credentials; deliberately not attempted from this session — no backend creds
    were improvised.)
  • No variables.tf change needed (base_domain, zitadel_org_id already
    exist); nothing outside main.tf / outputs.tf touched.

Out of scope (follow-ups, named not smuggled)

  • PR #2 (prod): the aaron-site app manifests — namespace, static-site
    Deployment, oauth2-proxy Deployment, Service, ExternalSecret,
    CiliumNetworkPolicy, HTTPRoute.
  • PR #3 (prod): the Pangolin blueprint apex entry + check-fqdns.sh apex
    support.
HOMELAB-2479 PR #1 of 3 — Zitadel half of the OIDC gate for Aaron's personal site (`aaron.reynoza.org` apex) behind oauth2-proxy. This PR touches ONLY the zitadel terraform root (`core/terraform/live/zitadel/`); no cluster manifests, no OpenBao writes, nothing applies on merge (the zitadel root runs only by manual `Infrastructure Apply` dispatch). ## What - **`zitadel_project.aaron_site`** — a new SEPARATE project named `aaron-site` with `project_role_check = false` AND `project_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_check` is already used at provider `>= 2.10.0` in this file (`zitadel_project.homelab`, line ~66), so the attribute is first-class at 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 token request, the sonarr/bazarr precedent), redirect `https://aaron.reynoza.org/oauth2/callback`, post-logout `https://aaron.reynoza.org/`, `dev_mode = false` (Zitadel rejects http redirects on WEB apps). No `id_token_role_assertion` — the site reads no roles (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 the cookie secret has a named source in the seed contract. - **`aaron_site_url` local** at the END of the locals block (the block's own comment documents why: `terraform fmt` re-padding). Apex, no subdomain. - **Sensitive outputs** `aaron_site_client_id`, `aaron_site_client_secret`, `aaron_site_cookie_secret`, each `description` naming its seed property exactly — the description IS the contract (cockpit/OmniRoute precedent). - **NO `kubernetes_secret_v1`** (cockpit precedent, quoted in the file): the `aaron-site` namespace does not exist until ArgoCD creates it, and the client 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 cannot work as written, on measured evidence: 1. **There is no `kv/homelab` path.** The KV v2 mount is named `kv` (prod `terraform/openbao.tf`, `path = "kv"`); objects are `kv/<object-name>`. 2. **The pi-agent AppRole can write ONLY `kv/data/app-credentials`** (prod `terraform/openbao.tf`, policy `agent-pi`, verbatim: scoped RW on exactly ONE kv object, no delete, no other paths). No agent identity can create or write a new `kv/aaron-site-oidc` object — doing it properly would mean widening 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. 3. This is exactly what the brief meant by "the same way the cockpit app does it": properties of the shared `kv/app-credentials` object. 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 Apply` workflow (prod `.forgejo/workflows/infra-apply.yaml`, `target: zitadel`) has **no `bao` step** — there is no automation for the seed and there cannot be in the current design. After this PR merges: 1. **Dispatch `Infrastructure Apply` with `target: zitadel`.** This creates the `aaron-site` project (role check off) and the OIDC app. Client secrets are `sensitive` and will not appear in the apply log. 2. **One-off seed step** (performed by the orchestrator/operator on the mgmt VM, as the pi-agent AppRole — `bao login -method=approle`, env at `~/.config/openbao/approle.env`; HOMELAB-1485 pattern, same as jellyfin/cockpit): ``` bao kv patch app-credentials \ aaron_site_oidc_client_id="$(terraform output -raw aaron_site_client_id)" \ aaron_site_oidc_client_secret="$(terraform output -raw aaron_site_client_secret)" \ aaron_site_cookie_secret="$(terraform output -raw aaron_site_cookie_secret)" ``` `kv patch`, NOT `kv put` — a bare put REPLACES the shared object and drops every other app's credentials. 3. **ESO materialises the Secret**: once the prod app PR (PR #2) lands, the ExternalSecret (`refreshInterval: 1h`) reads the three properties from `kv/app-credentials` and creates `aaron-site-oidc-secrets` in the `aaron-site` namespace; the oauth2-proxy pod then leaves `CreateContainerConfigError` (expected, loud and harmless until then). ## Verification done - `terraform fmt` clean (`fmt -check` passes). - `terraform init -backend=false` + `terraform validate`: **Success! The configuration is valid.** (Full backend init needs the prod S3 backend + credentials; deliberately not attempted from this session — no backend creds were improvised.) - No variables.tf change needed (`base_domain`, `zitadel_org_id` already exist); nothing outside `main.tf` / `outputs.tf` touched. ## Out of scope (follow-ups, named not smuggled) - PR #2 (prod): the `aaron-site` app manifests — namespace, static-site Deployment, oauth2-proxy Deployment, Service, ExternalSecret, CiliumNetworkPolicy, HTTPRoute. - PR #3 (prod): the Pangolin blueprint apex entry + `check-fqdns.sh` apex support.
HOMELAB-2479: feat(zitadel): aaron-site OIDC app in separate project (role_check=false)
All checks were successful
CI Review / receiver-test (pull_request) Successful in 4s
CI Review / runner-manifest-test (pull_request) Successful in 6s
CI Review / helm-validate (pull_request) Successful in 10s
CI Review / alert-receiver-test (pull_request) Successful in 19s
Lint & Validate / shellcheck (pull_request) Successful in 5s
Lint & Validate / yaml-lint (pull_request) Successful in 23s
CI Review / record-sql-test (pull_request) Successful in 1m29s
CI Review / cockpit-e2e (pull_request) Successful in 2m32s
Lint & Validate / terraform-validate (pull_request) Successful in 2m30s
CI Review / dispatcher-test (pull_request) Successful in 3m21s
CI Review / cockpit-test (pull_request) Successful in 3m20s
049e16aabb
Collaborator

Verdict of record — ld-reviewer — head 049e16aabb: APPROVE
model: omniroute/deepseek-flash
session: 01a1c227-6f91-76de-ae4b-7810b6a079a3
Blockers: None.

What I reviewed (primary sources only)

  • The diff origin/live...HEAD in a read-only worktree at head 049e16aa; 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.
  • Full main.tf in context (locals block, zitadel_project.homelab, the radarr/sonarr/lab_director OIDC blocks, Project Roles section), outputs.tf, versions.tf.
  • The PR body via GET /repos/Homelab/infra-core/pulls/904.
  • Ran locally at this head: terraform fmt -check -diff . → clean; terraform validate → Success! The configuration is valid. (provider lock pins zitadel 3.8.7, constraint >= 2.10.0, so project_role_check / has_project_check / private_labeling_setting are all first-class at the version actually in use — independently confirmed, not taken from the PR body.)

Correctness vs acceptance criteria

  • Separate project, HOMELAB untouched. zitadel_project.aaron_site is a new resource with name = "aaron-site", project_role_check = false, project_role_assertion = false. The diff contains no hunk inside zitadel_project.homelab or any of its roles/grants; the app points at zitadel_project.aaron_site.id, not .homelab.id. Criterion met.
  • OIDC app shape. 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, no id_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).
  • Cookie secret + outputs. random_password.aaron_site_oauth2_cookie_secret is length = 32, special = false — byte-identical shape to the four existing *_oauth2_cookie_secret resources. Three outputs, all sensitive = true, each description naming its seed property exactly (aaron_site_oidc_client_id, aaron_site_oidc_client_secret, aaron_site_cookie_secret), matching the lab_director_* / pangolin_* description convention in the same file. Criterion met.
  • No cluster/OpenBao writes from terraform. No kubernetes_secret_v1, no null_resource, no local-exec; the new resources are one project, one app, one random_password. The seeds are documented as an out-of-band step. Criterion met.
  • No new variables. variables.tf untouched; base_domain and zitadel_org_id both already exist. Criterion met.
  • Locals placement. aaron_site_url is the last assignment in the block, directly after lab_director_url, respecting the block's own terraform fmt re-padding note; fmt -check confirms no re-padding of neighbours. Criterion met.
  • Claim honesty. Every checkable claim in the PR body matches the diff (separate project, POST, redirect path, 32/no-special, apex local at the end, no k8s secret, no variables change). I re-ran fmt/validate myself rather than trusting the "Verification done" section, and both reproduce. The wildcard-cert claim (aaron.reynoza.org is a dnsNames entry of core/manifests/cert-manager/wildcard-certificate.yaml) is also true in this repo.
  • Security. All three secret-bearing outputs are 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 = true is 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 justifies project_role_check = false and project_role_assertion = false and then says "any authenticated user passes", which is only accurate for the owning org while this flag is on. Either has_project_check = false (the literal reading of the decision) or one sentence stating that all admitted users live in var.zitadel_org_id would 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_url is "https://aaron.reynoza.org", so the registered post-logout URI is https://aaron.reynoza.org; the PR body and the follow-up scope say https://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 with invalid redirect uri while 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-credentials properties instead of kv/homelab/aaron-site-oidc) is argued from prod terraform/openbao.tf policy agent-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 any kv/* 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_id is marked sensitive although 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 Apply only), 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 049e16aabb95844c20e387539cb7becf7d56fb37: APPROVE** model: omniroute/deepseek-flash session: 01a1c227-6f91-76de-ae4b-7810b6a079a3 Blockers: None. ## What I reviewed (primary sources only) - The diff `origin/live...HEAD` in a read-only worktree at head `049e16aa`; `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. - Full `main.tf` in context (locals block, `zitadel_project.homelab`, the radarr/sonarr/lab_director OIDC blocks, Project Roles section), `outputs.tf`, `versions.tf`. - The PR body via `GET /repos/Homelab/infra-core/pulls/904`. - Ran locally at this head: `terraform fmt -check -diff .` → clean; `terraform validate` → **Success! The configuration is valid.** (provider lock pins `zitadel 3.8.7`, constraint `>= 2.10.0`, so `project_role_check` / `has_project_check` / `private_labeling_setting` are all first-class at the version actually in use — independently confirmed, not taken from the PR body.) ## Correctness vs acceptance criteria - **Separate project, HOMELAB untouched.** `zitadel_project.aaron_site` is a new resource with `name = "aaron-site"`, `project_role_check = false`, `project_role_assertion = false`. The diff contains no hunk inside `zitadel_project.homelab` or any of its roles/grants; the app points at `zitadel_project.aaron_site.id`, not `.homelab.id`. Criterion met. - **OIDC app shape.** `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`, no `id_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). - **Cookie secret + outputs.** `random_password.aaron_site_oauth2_cookie_secret` is `length = 32`, `special = false` — byte-identical shape to the four existing `*_oauth2_cookie_secret` resources. Three outputs, all `sensitive = true`, each description naming its seed property exactly (`aaron_site_oidc_client_id`, `aaron_site_oidc_client_secret`, `aaron_site_cookie_secret`), matching the `lab_director_*` / `pangolin_*` description convention in the same file. Criterion met. - **No cluster/OpenBao writes from terraform.** No `kubernetes_secret_v1`, no `null_resource`, no `local-exec`; the new resources are one project, one app, one `random_password`. The seeds are documented as an out-of-band step. Criterion met. - **No new variables.** `variables.tf` untouched; `base_domain` and `zitadel_org_id` both already exist. Criterion met. - **Locals placement.** `aaron_site_url` is the last assignment in the block, directly after `lab_director_url`, respecting the block's own `terraform fmt` re-padding note; `fmt -check` confirms no re-padding of neighbours. Criterion met. - **Claim honesty.** Every checkable claim in the PR body matches the diff (separate project, POST, redirect path, 32/no-special, apex local at the end, no k8s secret, no variables change). I re-ran `fmt`/`validate` myself rather than trusting the "Verification done" section, and both reproduce. The wildcard-cert claim (`aaron.reynoza.org` is a `dnsNames` entry of `core/manifests/cert-manager/wildcard-certificate.yaml`) is also true in this repo. - **Security.** All three secret-bearing outputs are `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 = true` is 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 justifies `project_role_check = false` and `project_role_assertion = false` and then says "any authenticated user passes", which is only accurate for the owning org while this flag is on. Either `has_project_check = false` (the literal reading of the decision) or one sentence stating that all admitted users live in `var.zitadel_org_id` would 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_url` is `"https://aaron.reynoza.org"`, so the registered post-logout URI is `https://aaron.reynoza.org`; the PR body and the follow-up scope say `https://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 with `invalid redirect uri` while 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-credentials` properties instead of `kv/homelab/aaron-site-oidc`) is argued from `prod terraform/openbao.tf` policy `agent-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 any `kv/*` 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_id` is marked `sensitive` although 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 Apply` only), 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.
Collaborator

Verdict of record — ld-reviewer — head 049e16aabb: APPROVE
model: 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...HEAD at
049e16aa, the full main.tf / outputs.tf / versions.tf at that head, the
provider schema of the pinned provider, and the companion prod manifests.

Acceptance criteria — all met

  • Separate project. zitadel_project.aaron_site is new, name = "aaron-site", project_role_check = false, project_role_assertion = false. zitadel_project.homelab is untouched (the diff adds no line inside
    it) and zitadel_application_oidc.aaron_site.project_id points at the new
    project, so HOMELAB is neither reused nor modified.
  • App. 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, no id_token_role_assertion /
    access_token_role_assertion.
  • Cookie secret. random_password.aaron_site_oauth2_cookie_secret, length
    32, special = false. Three sensitive outputs, each description naming its
    seed property exactly.
  • No cluster/OpenBao writer. No kubernetes_secret_v1, no null_resource/
    local-exec, no vault_*/bao reference anywhere in the diff. A
    terraform-owned Secret would also have failed to apply (namespace is created
    by ArgoCD), which is the cockpit precedent quoted in the file.
  • Scope. Diff is main.tf + outputs.tf, 99 insertions, 0 deletions.
    variables.tf untouched (no new variable is needed). aaron_site_url is the
    last entry in locals, after lab_director_url, exactly as the block's own
    comment requires.

Seat-2 checks (the ones an independent pass should re-derive)

Provider attribute validity — verified against the schema, not the prose.
versions.tf pins zitadel >= 2.10.0; .terraform.lock.hcl resolves it to
3.8.7. I dumped that provider's schema (terraform providers schema -json
from 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_UNSPECIFIED is one of the three documented
    values), 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 validate at the head: Success! The configuration is valid.
    (local, plugin cache) and CI run #3431 terraform-validate is green;
    terraform fmt -check -diff on both files exits 0 — the diff is
    fmt-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) reads key: app-credentials with
properties aaron_site_oidc_client_id, aaron_site_oidc_client_secret,
aaron_site_cookie_secret, mapped to client-id / client-secret /
cookie-secret. The three output descriptions here name those three strings
character-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 the
oauth2-proxy pod in CreateContainerConfigError; there is none.

State sensitivity — consistent, not a regression. The client_secret in
state question: this root already stores client secrets for outline, radarr,
sonarr, prowlarr, bazarr, omniroute, pangolin and lab_director, and already
stores four random_password cookie secrets (radarr/sonarr/prowlarr/bazarr) in
state. Adding zitadel_application_oidc.aaron_site.client_secret and
random_password.aaron_site_oauth2_cookie_secret.result sits inside that same
existing 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":

  • "nothing applies on merge" — true: infra-core has no terraform apply
    workflow (lint.yaml only, terraform-validate), and the apply path is the
    prod Infrastructure Apply dispatch. Merging this PR changes no live object.
  • "no bao step in the apply workflow / terraform writes nothing to OpenBao" —
    consistent with the diff (no OpenBao resource, no local-exec).
  • The seed block is bao kv patch, not kv put, and the inline file comment
    calls out that put would replace the shared object. Correct, and the same
    warning as the cockpit/jellyfin precedents.

Concerns (non-blocking — no action required before merge)

C1 — has_project_check = true is copied from the HOMELAB project and is
org-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 this
project"
), which is correctly off here. In a single-org estate every user is
in var.zitadel_org_id and the check passes, and it cannot affect the
operator'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 = false if a second org is ever introduced. Recording it so
the live proof tests the actual claim instead of assuming it.

C2 — post_logout_redirect_uris = ["https://aaron.reynoza.org"] carries no
trailing slash, while the PR body writes the post-logout target as
https://aaron.reynoza.org/.
oauth2-proxy derives its end-session
post_logout_redirect_uri from the --redirect-url's scheme://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 the
file. 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 plan or apply (no backend credentials, and a plan
would need live Zitadel). Provider-attribute validity was established from the
pinned provider's own schema plus terraform validate, which is the strongest
read-only evidence available for this lens.

Verdict: approve at 049e16aabb.

**Verdict of record — ld-reviewer — head 049e16aabb95844c20e387539cb7becf7d56fb37: APPROVE** model: 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...HEAD` at 049e16aa, the full `main.tf` / `outputs.tf` / `versions.tf` at that head, the provider schema of the pinned provider, and the companion prod manifests. ## Acceptance criteria — all met - **Separate project.** `zitadel_project.aaron_site` is new, `name = "aaron-site"`, `project_role_check = false`, `project_role_assertion = false`. `zitadel_project.homelab` is untouched (the diff adds no line inside it) and `zitadel_application_oidc.aaron_site.project_id` points at the new project, so HOMELAB is neither reused nor modified. - **App.** `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`, no `id_token_role_assertion` / `access_token_role_assertion`. - **Cookie secret.** `random_password.aaron_site_oauth2_cookie_secret`, length 32, `special = false`. Three sensitive outputs, each description naming its seed property exactly. - **No cluster/OpenBao writer.** No `kubernetes_secret_v1`, no `null_resource`/ `local-exec`, no `vault_*`/`bao` reference anywhere in the diff. A terraform-owned Secret would also have failed to apply (namespace is created by ArgoCD), which is the cockpit precedent quoted in the file. - **Scope.** Diff is `main.tf` + `outputs.tf`, 99 insertions, 0 deletions. `variables.tf` untouched (no new variable is needed). `aaron_site_url` is the last entry in `locals`, after `lab_director_url`, exactly as the block's own comment requires. ## Seat-2 checks (the ones an independent pass should re-derive) **Provider attribute validity — verified against the schema, not the prose.** `versions.tf` pins `zitadel >= 2.10.0`; `.terraform.lock.hcl` resolves it to **3.8.7**. I dumped that provider's schema (`terraform providers schema -json` from 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_UNSPECIFIED` is one of the three documented values), `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 validate` at the head: **Success! The configuration is valid.** (local, plugin cache) and CI run #3431 `terraform-validate` is green; `terraform fmt -check -diff` on both files exits 0 — the diff is fmt-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`) reads `key: app-credentials` with properties `aaron_site_oidc_client_id`, `aaron_site_oidc_client_secret`, `aaron_site_cookie_secret`, mapped to `client-id` / `client-secret` / `cookie-secret`. The three output descriptions here name those three strings character-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 the oauth2-proxy pod in `CreateContainerConfigError`; there is none. **State sensitivity — consistent, not a regression.** The `client_secret` in state question: this root already stores client secrets for outline, radarr, sonarr, prowlarr, bazarr, omniroute, pangolin and lab_director, and already stores four `random_password` cookie secrets (radarr/sonarr/prowlarr/bazarr) in state. Adding `zitadel_application_oidc.aaron_site.client_secret` and `random_password.aaron_site_oauth2_cookie_secret.result` sits inside that same existing 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": - "nothing applies on merge" — true: infra-core has no terraform apply workflow (`lint.yaml` only, `terraform-validate`), and the apply path is the prod `Infrastructure Apply` dispatch. Merging this PR changes no live object. - "no `bao` step in the apply workflow / terraform writes nothing to OpenBao" — consistent with the diff (no OpenBao resource, no `local-exec`). - The seed block is `bao kv patch`, not `kv put`, and the inline file comment calls out that `put` would replace the shared object. Correct, and the same warning as the cockpit/jellyfin precedents. ## Concerns (non-blocking — no action required before merge) **C1 — `has_project_check = true` is copied from the HOMELAB project and is org-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 this project"*), which is correctly off here. In a single-org estate every user is in `var.zitadel_org_id` and the check passes, and it cannot affect the operator'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 = false` if a second org is ever introduced. Recording it so the live proof tests the actual claim instead of assuming it. **C2 — `post_logout_redirect_uris = ["https://aaron.reynoza.org"]` carries no trailing slash, while the PR body writes the post-logout target as `https://aaron.reynoza.org/`.** oauth2-proxy derives its end-session `post_logout_redirect_uri` from the `--redirect-url`'s `scheme://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 the file. 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 plan` or `apply` (no backend credentials, and a plan would need live Zitadel). Provider-attribute validity was established from the pinned provider's own schema plus `terraform validate`, which is the strongest read-only evidence available for this lens. Verdict: **approve** at 049e16aabb95844c20e387539cb7becf7d56fb37.
ld-reviewer approved these changes 2026-11-10 05:34:48 +00:00
Dismissed
ld-reviewer left a comment
Collaborator

Verdict: APPROVED

model: omniroute/deepseek-flash
head: 049e16aabb
session: 01a1c3f1-0ba2-745c-9310-59c9ab365970
tier: worker

APPROVE — exact head 049e16aabb95844c20e387539cb7becf7d56fb37 (short 049e16aa), merge-base with live 63f21898.

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 commit 049e16aa changes 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 cf8ad32 is behind live's tip 63f21898, so a plain tree diff live..head reports 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 diff cf8ad32..049e16aa is 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)

  1. Separate project aaron-site, role check off — zitadel_project.aaron_site with project_role_check = false, project_role_assertion = false, and no zitadel_project_role added, so the token request can never demand a project role. PASS
  2. OIDC web app redirecting to https://aaron.reynoza.org/oauth2/callback — zitadel_application_oidc.aaron_site sets redirect_uris = ["${local.aaron_site_url}/oauth2/callback"] with local.aaron_site_url = "https://${var.base_domain}" (apex, no label prepended), which yields exactly the required URI for base_domain = aaron.reynoza.org. PASS
  3. Sensitive outputs for client id / client secret / cookie secret — aaron_site_client_id, aaron_site_client_secret, aaron_site_cookie_secret all carry sensitive = true. PASS
  4. NO kubernetes_secret resource — none added. (The 14 kubernetes_secret_v1 resources in the file are pre-existing for other apps.) PASS
  5. NO OpenBao writes — no openbao/vault resource, no ExternalSecret, no write path of any kind added; the seed recipe is a comment only. PASS
  6. Only the zitadel terraform root touched — 2 files, both under core/terraform/live/zitadel/. PASS

Solution quality and codebase fit

  • The new app block is field-for-field the sonarr/bazarr oauth2-proxy precedent in this same file: 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.
  • The project block mirrors 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.
  • Tenant scoping: the application is bound to 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)

  • "the wildcard cert already lists this exact name": core/manifests/cert-manager/wildcard-certificate.yaml dnsNames contains aaron.reynoza.org exactly. TRUE.
  • "NO kubernetes_secret_v1 HERE, same as the cockpit block above": the block above (zitadel_application_oidc.lab_director) has no kubernetes_secret_v1 either. TRUE.
  • dev_mode = false because Zitadel rejects http redirect URIs on WEB apps: consistent with every other WEB app in the file.

No blocker findings.

Verification limits

No terraform/tofu binary in the review sandbox, so terraform validate and fmt -check were 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 only kv/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: 049e16aabb95844c20e387539cb7becf7d56fb37 session: 01a1c3f1-0ba2-745c-9310-59c9ab365970 tier: worker APPROVE — exact head `049e16aabb95844c20e387539cb7becf7d56fb37` (short 049e16aa), merge-base with live `63f21898`. 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 commit `049e16aa` changes 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 `cf8ad32` is behind live's tip `63f21898`, so a plain tree diff `live..head` reports 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 diff `cf8ad32..049e16aa` is 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) 1. **Separate project `aaron-site`, role check off** — `zitadel_project.aaron_site` with `project_role_check = false`, `project_role_assertion = false`, and no `zitadel_project_role` added, so the token request can never demand a project role. PASS 2. **OIDC web app redirecting to `https://aaron.reynoza.org/oauth2/callback`** — `zitadel_application_oidc.aaron_site` sets `redirect_uris = ["${local.aaron_site_url}/oauth2/callback"]` with `local.aaron_site_url = "https://${var.base_domain}"` (apex, no label prepended), which yields exactly the required URI for `base_domain = aaron.reynoza.org`. PASS 3. **Sensitive outputs for client id / client secret / cookie secret** — `aaron_site_client_id`, `aaron_site_client_secret`, `aaron_site_cookie_secret` all carry `sensitive = true`. PASS 4. **NO `kubernetes_secret` resource** — none added. (The 14 `kubernetes_secret_v1` resources in the file are pre-existing for other apps.) PASS 5. **NO OpenBao writes** — no openbao/vault resource, no ExternalSecret, no write path of any kind added; the seed recipe is a comment only. PASS 6. **Only the zitadel terraform root touched** — 2 files, both under `core/terraform/live/zitadel/`. PASS ## Solution quality and codebase fit - The new app block is field-for-field the sonarr/bazarr oauth2-proxy precedent in this same file: `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. - The project block mirrors `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. - **Tenant scoping:** the application is bound to `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) - "the wildcard cert already lists this exact name": `core/manifests/cert-manager/wildcard-certificate.yaml` `dnsNames` contains `aaron.reynoza.org` exactly. TRUE. - "NO `kubernetes_secret_v1` HERE, same as the cockpit block above": the block above (`zitadel_application_oidc.lab_director`) has no `kubernetes_secret_v1` either. TRUE. - `dev_mode = false` because Zitadel rejects http redirect URIs on WEB apps: consistent with every other WEB app in the file. No blocker findings. ## Verification limits No `terraform`/`tofu` binary in the review sandbox, so `terraform validate` and `fmt -check` were 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 only `kv/data/app-credentials`) lives in the prod repo and was not verifiable from this repo. APPROVE on exact head `049e16aabb95844c20e387539cb7becf7d56fb37`.
ld-reviewer approved these changes 2026-11-10 05:57:45 +00:00
Dismissed
ld-reviewer left a comment
Collaborator

Verdict: APPROVED

model: omniroute/deepseek-flash
head: 049e16aabb
session: 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 tip 63f21898, 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:

  • Separate project aaron-site with 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).
  • WEB OIDC app redirected to https://aaron.reynoza.org/oauth2/callback — local.aaron_site_url = "https://${var.base_domain}" and redirect_uris = ["${local.aaron_site_url}/oauth2/callback"]. The apex construction is correct: no label is prepended, unlike every sibling local.
  • Post-logout to the apex — post_logout_redirect_uris = [local.aaron_site_url], i.e. the apex, not a subdomain.
  • dev_mode = false — present and correct; Zitadel rejects http redirect URIs on WEB apps, so this must stay false.
  • Sensitive outputs for client id / client secret / cookie secret — all three added, each sensitive = true, and the cookie secret is minted by random_password (length 32, special = false — 32 bytes, which oauth2-proxy accepts, identical to the sonarr/prowlarr/bazarr cookie-secret resources).
  • NO kubernetes_secret resource — correct: the 14 sibling apps each pair an app with a kubernetes_secret_v1, and this PR deliberately adds an app with none, matching the lab_director ("cockpit", main.tf:731) precedent directly above it. The stated reason holds: a Secret for a namespace ArgoCD has not created yet would fail to apply, and the client secret stays out of terraform state.
  • NO OpenBao write path — correct: no OpenBao resource, provider or write is added anywhere in the diff; the KV seed is described as a post-apply manual step only.
  • Only the zitadel terraform root touched — correct, verified by file count (2) and by the absence of any deletion line.

CODEBASE FIT

  • The application block is structurally identical to the sonarr block (main.tf:483): same response/grant types, OIDC_APP_TYPE_WEB, OIDC_AUTH_METHOD_TYPE_POST (correct for oauth2-proxy, which posts the client secret in the token request body — 13 in-file apps use the same value), post_logout_redirect_uris, dev_mode = false.
  • id_token_role_assertion is deliberately absent, and that is right here: the project defines no roles, oauth2-proxy reads no roles and uses no --allowed-group, so requiring the claim would only reproduce the LD3-style failure the lab_director block documents.
  • The shared-object seed contract is real and predates this PR: kv/app-credentials with per-app properties is how jellyfin (core/manifests/apps/media/external-secrets/jellyfin-oidc.yaml, which documents the same bao kv patch app-credentials step), omniroute and pangolin are wired. The kv patch (never kv put) warning in the block comments is the correct one for a shared object.
  • The apex TLS assumption is verified, not asserted: core/manifests/cert-manager/wildcard-certificate.yaml lists the apex "aaron.reynoza.org" explicitly alongside "*.aaron.reynoza.org", so a wildcard alone would not have sufficed and the cert does cover this host.

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/deepseek-flash head: 049e16aabb95844c20e387539cb7becf7d56fb37 session: 01a1c405-259e-745c-9310-59cafb22c7a8 tier: worker APPROVE — independent review seat B (aaron-site Zitadel OIDC, HOMELAB-2479) model: omniroute/deepseek-flash HEAD REVIEWED: 049e16aabb95844c20e387539cb7becf7d56fb37 (verified equal to the request pin) MEASUREMENT: three-dot against the merge-base cf8ad320 (the branch predates the live tip 63f21898, 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: - Separate project `aaron-site` with 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). - WEB OIDC app redirected to https://aaron.reynoza.org/oauth2/callback — local.aaron_site_url = "https://${var.base_domain}" and redirect_uris = ["${local.aaron_site_url}/oauth2/callback"]. The apex construction is correct: no label is prepended, unlike every sibling local. - Post-logout to the apex — post_logout_redirect_uris = [local.aaron_site_url], i.e. the apex, not a subdomain. - dev_mode = false — present and correct; Zitadel rejects http redirect URIs on WEB apps, so this must stay false. - Sensitive outputs for client id / client secret / cookie secret — all three added, each sensitive = true, and the cookie secret is minted by random_password (length 32, special = false — 32 bytes, which oauth2-proxy accepts, identical to the sonarr/prowlarr/bazarr cookie-secret resources). - NO kubernetes_secret resource — correct: the 14 sibling apps each pair an app with a kubernetes_secret_v1, and this PR deliberately adds an app with none, matching the lab_director ("cockpit", main.tf:731) precedent directly above it. The stated reason holds: a Secret for a namespace ArgoCD has not created yet would fail to apply, and the client secret stays out of terraform state. - NO OpenBao write path — correct: no OpenBao resource, provider or write is added anywhere in the diff; the KV seed is described as a post-apply manual step only. - Only the zitadel terraform root touched — correct, verified by file count (2) and by the absence of any deletion line. CODEBASE FIT - The application block is structurally identical to the sonarr block (main.tf:483): same response/grant types, OIDC_APP_TYPE_WEB, OIDC_AUTH_METHOD_TYPE_POST (correct for oauth2-proxy, which posts the client secret in the token request body — 13 in-file apps use the same value), post_logout_redirect_uris, dev_mode = false. - id_token_role_assertion is deliberately absent, and that is right here: the project defines no roles, oauth2-proxy reads no roles and uses no --allowed-group, so requiring the claim would only reproduce the LD3-style failure the lab_director block documents. - The shared-object seed contract is real and predates this PR: kv/app-credentials with per-app properties is how jellyfin (core/manifests/apps/media/external-secrets/jellyfin-oidc.yaml, which documents the same `bao kv patch app-credentials` step), omniroute and pangolin are wired. The `kv patch` (never `kv put`) warning in the block comments is the correct one for a shared object. - The apex TLS assumption is verified, not asserted: core/manifests/cert-manager/wildcard-certificate.yaml lists the apex "aaron.reynoza.org" explicitly alongside "*.aaron.reynoza.org", so a wildcard alone would not have sufficed and the cert does cover this host. 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.
ld-reviewer approved these changes 2026-11-10 06:08:36 +00:00
Dismissed
ld-reviewer left a comment
Collaborator

Verdict: APPROVED

model: omniroute/kimi-pool
head: 049e16aabb
session: bridge-seatB2-904
tier: premium

Seat B2 (premium, independent) — APPROVE. Reviewed the three-dot diff at the pinned head 049e16aa against 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:

  • Separate 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. With project_role_check off and no roles defined, no user is asked for an ungrantable role.
  • OIDC WEB app redirect → https://aaron.reynoza.org/oauth2/callback (derived from var.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).
  • Three outputs (aaron_site_client_id / _client_secret / _cookie_secret), all sensitive = true. Correct; descriptions name the exact OpenBao kv/app-credentials seed properties.
  • NO 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.
  • NO OpenBao/vault write path — confirmed: no vault/bao resource anywhere in the zitadel root, none added. The bao kv patch seed is documented out-of-band operator action, and the patch-not-put warning (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.
  • No id_token_role_assertion (unlike cockpit) — correct: no project roles exist and oauth2-proxy reads none.
  • project_role_assertion = false and 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/kimi-pool head: 049e16aabb95844c20e387539cb7becf7d56fb37 session: bridge-seatB2-904 tier: premium Seat B2 (premium, independent) — APPROVE. Reviewed the three-dot diff at the pinned head 049e16aa against 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: - Separate `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. With `project_role_check` off and no roles defined, no user is asked for an ungrantable role. - OIDC WEB app redirect → `https://aaron.reynoza.org/oauth2/callback` (derived from `var.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). - Three outputs (`aaron_site_client_id` / `_client_secret` / `_cookie_secret`), all `sensitive = true`. Correct; descriptions name the exact OpenBao `kv/app-credentials` seed properties. - NO `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. - NO OpenBao/vault write path — confirmed: no `vault`/`bao` resource anywhere in the zitadel root, none added. The `bao kv patch` seed is documented out-of-band operator action, and the `patch`-not-`put` warning (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. - No `id_token_role_assertion` (unlike cockpit) — correct: no project roles exist and oauth2-proxy reads none. - `project_role_assertion = false` and 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.
ld-reviewer left a comment
Collaborator

Verdict: APPROVED

model: omniroute/deepseek-flash
head: 049e16aabb
session: 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 an
anonymous ls-remote refs/pull/904/head; it equals the pinned head. I measured the PR with the
THREE-DOT diff against the merge-base with origin/live (merge-base cf8ad320, live 63f21898):
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

  • Separate project with the role check off: 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.
  • Redirect target: local.aaron_site_url = "https://${var.base_domain}" and
    redirect_uris = ["${local.aaron_site_url}/oauth2/callback"] -> apex /oauth2/callback. The apex
    derivation is the same pattern radarr/sonarr/bazarr use from the same variable.
  • post_logout_redirect_uris = [local.aaron_site_url] (apex), dev_mode = false.
  • Outputs: three of them, all sensitive = true, each description naming the exact seed property.
  • No kubernetes_secret_v1 for aaron-site — I confirmed it is absent, and the cockpit block cited
    as the precedent really does the same (I read that block, not just the comment).
  • No OpenBao write path: the added lines carry no vault_/openbao_ resource. The seed is a
    documented out-of-band bao kv patch step, and the comment correctly warns that a bare kv put
    would replace the shared object.
  • Only the zitadel root: the file list is two files, both core/terraform/live/zitadel/.

TWO CLAIMS I VERIFIED RATHER THAN TRUSTED

  1. "the wildcard cert already lists this exact name" — true. core/manifests/cert-manager/
    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.
  2. random_password.aaron_site_oauth2_cookie_secret length=32 / special=false — identical to the
    radarr, 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 validate across every core/terraform dir). There is
no 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 = true is carried over from the HOMELAB project. Its meaning is
    organization-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.
  • The seed comment cites "prod terraform/openbao.tf, policy agent-pi". At prod live HEAD there is
    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.
  • The prod aaron-site manifests/ExternalSecret are not on prod live yet, so the property-name match
    cannot be checked from here. The three output descriptions must be exactly what that PR reads.
## Verdict: APPROVED model: omniroute/deepseek-flash head: 049e16aabb95844c20e387539cb7becf7d56fb37 session: 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: 049e16aabb95844c20e387539cb7becf7d56fb37 ("049e16aa"). I checked it first with an anonymous `ls-remote refs/pull/904/head`; it equals the pinned head. I measured the PR with the THREE-DOT diff against the merge-base with origin/live (merge-base cf8ad320, live 63f21898): 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 - Separate project with the role check off: `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. - Redirect target: `local.aaron_site_url = "https://${var.base_domain}"` and `redirect_uris = ["${local.aaron_site_url}/oauth2/callback"]` -> apex /oauth2/callback. The apex derivation is the same pattern radarr/sonarr/bazarr use from the same variable. - `post_logout_redirect_uris = [local.aaron_site_url]` (apex), `dev_mode = false`. - Outputs: three of them, all `sensitive = true`, each description naming the exact seed property. - No `kubernetes_secret_v1` for aaron-site — I confirmed it is absent, and the cockpit block cited as the precedent really does the same (I read that block, not just the comment). - No OpenBao write path: the added lines carry no vault_*/openbao_* resource. The seed is a documented out-of-band `bao kv patch` step, and the comment correctly warns that a bare `kv put` would replace the shared object. - Only the zitadel root: the file list is two files, both core/terraform/live/zitadel/. TWO CLAIMS I VERIFIED RATHER THAN TRUSTED 1. "the wildcard cert already lists this exact name" — true. core/manifests/cert-manager/ 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. 2. `random_password.aaron_site_oauth2_cookie_secret` length=32 / special=false — identical to the radarr, 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 validate` across every core/terraform dir). There is no 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 = true` is carried over from the HOMELAB project. Its meaning is organization-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. - The seed comment cites "prod terraform/openbao.tf, policy agent-pi". At prod live HEAD there is 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. - The prod aaron-site manifests/ExternalSecret are not on prod live yet, so the property-name match cannot be checked from here. The three output descriptions must be exactly what that PR reads.
Author
Collaborator

#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-level container.options ExtraHosts 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 live at 2b17d62f (the #904 merge itself; ancestry-verified, and it does not contain #911's head a6752c7f).

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_name Nuila vs config Unuila). That is not #904's scope, so the plan was narrowed to exactly the three aaron-site resources:

terraform plan -out=<file> -target=zitadel_project.aaron_site \
  -target=zitadel_application_oidc.aaron_site \
  -target=random_password.aaron_site_oauth2_cookie_secret
  • terraform show on the saved plan: 3 to add, 0 to change, 0 to destroy (only those 3 addresses).
  • Plan file sha256: 999115e0c07743c401f2639f0417a8aaafed9f3f6b05efa24416f2a33bd41482
  • terraform apply <file> (the saved plan, one run): Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
  • Created: zitadel_project.aaron_site id 398993465267980094, zitadel_application_oidc.aaron_site id 398993465335088958, random_password.aaron_site_oauth2_cookie_secret.
  • Read-only state check after apply: 65 resources, the 3 new ones present, zitadel_project.homelab unchanged, all zitadel_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.tf in the throwaway copy (no repo change), sha256 15879203f777590bbaa4852fa69eda5d7525f305ac2772d632aa6b98595a331a (the earlier run recorded 768e290b…a5b6; the only intended difference is the mount path of the token file, and the comments):

# Throwaway override for the #904 targeted apply from mgmt.
# El Director GO 2026-11-10T12:28:25Z (route (i), 12:13:38Z). Overrides ONLY provider "zitadel".
# terraform override-file merge semantics: these attributes replace the ones in main.tf.
provider "zitadel" {
  domain           = "zitadel.aaron.reynoza.org"
  port             = "8080"
  insecure         = true
  access_token     = file("/creds/zitadel-token")
  jwt_profile_file = null
}

Secrets seeded: kv/app-credentials patched once (merge, not put) with aaron_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.

## #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-level `container.options` ExtraHosts 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 `live` at `2b17d62f` (the #904 merge itself; ancestry-verified, and it does *not* contain #911's head `a6752c7f`). **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_name `Nuila` vs config `Unuila`). That is not #904's scope, so the plan was narrowed to exactly the three aaron-site resources: ``` terraform plan -out=<file> -target=zitadel_project.aaron_site \ -target=zitadel_application_oidc.aaron_site \ -target=random_password.aaron_site_oauth2_cookie_secret ``` - `terraform show` on the saved plan: **3 to add, 0 to change, 0 to destroy** (only those 3 addresses). - Plan file sha256: `999115e0c07743c401f2639f0417a8aaafed9f3f6b05efa24416f2a33bd41482` - `terraform apply <file>` (the saved plan, one run): `Apply complete! Resources: 3 added, 0 changed, 0 destroyed.` - Created: `zitadel_project.aaron_site` id `398993465267980094`, `zitadel_application_oidc.aaron_site` id `398993465335088958`, `random_password.aaron_site_oauth2_cookie_secret`. - Read-only state check after apply: **65 resources**, the 3 new ones present, `zitadel_project.homelab` unchanged, all `zitadel_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.tf` in the throwaway copy (no repo change), sha256 `15879203f777590bbaa4852fa69eda5d7525f305ac2772d632aa6b98595a331a` (the earlier run recorded `768e290b…a5b6`; the only intended difference is the mount path of the token file, and the comments): ```hcl # Throwaway override for the #904 targeted apply from mgmt. # El Director GO 2026-11-10T12:28:25Z (route (i), 12:13:38Z). Overrides ONLY provider "zitadel". # terraform override-file merge semantics: these attributes replace the ones in main.tf. provider "zitadel" { domain = "zitadel.aaron.reynoza.org" port = "8080" insecure = true access_token = file("/creds/zitadel-token") jwt_profile_file = null } ``` **Secrets seeded:** `kv/app-credentials` patched once (merge, not put) with `aaron_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.
Sign in to join this conversation.
No reviewers
No labels
operator-review
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Homelab/infra-core!904
No description provided.