Repository navigation
fix(gcp): a GCP-hosted ZITADEL keeps its consumers in step - #2126
Merged
Merged
Conversation
Contributor
🔍 Rendered manifest diff — this PR vs
|
…owns, and fails loudly
Called under ||, store_write runs with errexit off. A cat cut short by a full disk, or a failed mktemp or jq, fell through to versions add / put-secret-value, which stored a truncated secret and returned 0. Every step before the cloud call now exits 1 explicitly, on both clouds.
…e project id is written only on change resolve_zitadel_pat takes an explicit hosting|consuming role. A consuming sync's kube context is its own cluster, so a chart Secret left there from when it hosted is never read and never overwrites the IdP cloud's only copy. A failed overwrite now warns and still returns the token. IDP_URL is checked before the resolve, which can write. zitadel-project-id is compared before it is written, and a failed publish accumulates like a failed mirror: the OpenBao reconcile and the summary still run, then the sync exits 1. data.tf documents the version-less-secret recovery.
…overy, and halt on OIDC drift - The mirror writes the whole store payload when OpenBao has nothing at the path. migrate skips an existing path, so an owned-fields-only write left grafana-envvars without its admin credentials for good. The ownership filter still protects an existing value. - Stage 3's flags live in IDP_SYNC_ARGS and CLIENT_SYNC_ARGS, expanded by both the real calls and the ZITADEL-not-ready recovery, which now prints the IdP sync and the clients sync with every OpenBao flag. - gcp/gke/init gains stage5-verify-openbao-oidc: on a hosting gcp-0 it runs openbao-oidc-check.sh as its last statement, so exit 1 and exit 2 halt the deploy. A consuming gcp-0 skips it and says why. - After a mirror, the ExternalSecrets reading the mirrored paths are force-synced (warn-only, --apply only). - store_write adds the version when secrets create reports ALREADY_EXISTS. - sso.md gives gcp-0's hosting and consuming cases, and states GP-20.
Smana
force-pushed
the
fix/gcp-hosted-idp
branch
from
September 29, 2026 11:23
71c5639 to
ac96645
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A GCP-hosted ZITADEL now keeps its consumers in step. On a fresh directory, gcp/gke/init's hosting stage 3 does four things:
auth/oidc.A new stage 5 then halts the deploy if OpenBao's OIDC client has drifted. This closes the known G-1 gap: a ZITADEL rotation left GCP OpenBao OIDC stale.
It fixes three bugs:
zitadel_project_idwas committed per directory. A GCP-hosted project id now reachesgke/configurethrough Secret Managerzitadel-project-id.The stage-3 and stage-5 changes are inert while AWS is primary.
mainkeepsprimary_cloud = "aws", so the hosting branch does not run and stage 5 prints its skip. GP-20 is live on both clouds. For a seed-restored aws-0 it is a no-op.1fc5c619refactor(secrets): one managed-store to OpenBao map, sharedscripts/lib/bao-map.sh, read bysecret-store.sh migrateand the new mirrorb91e295cfeat(zitadel): mirror consumer secrets into the OpenBao path they are read from--mirror-openbao(requires--openbao-url, runs only under--apply)ef915a1bfix(zitadel): the OpenBao mirror repairs itself, writes only what it owns, and fails loudlyMIRRORED_FIELDSonly onto an existing value; a failed mirror makes the run exit 16ba74878fix(secrets): store_write refuses a short payload writemktemp,catorjqno longer ships a truncated payload to the cloud5917a4b6fix(gcp): a hosted ZITADEL's project id reaches gke/configure through Secret Managerd8d39323fix(zitadel): the chart's fresh admin PAT wins over a stored one21c2788efix(zitadel): a consumer never overwrites the IdP cloud's PAT, and the project id is written only on changeresolve_zitadel_pat hosting|consuming;publish_project_idcompares before it writes95fdd664fix(gcp): a hosting stage 3 registers the IdP, then mirrors every client into OpenBaoac96645bfix(gcp): mirror an absent OpenBao path in full, print a complete recovery, and halt on OIDC driftFinal-review round (
ac96645b)migrateskips a path that exists. So an owned-fields-only write leftgrafana-envvarswithoutGF_SECURITY_ADMIN_USER/PASSWORD, and grafana-operator could never authenticate. The ownership filter still protects an existing value.IDP_SYNC_ARGSandCLIENT_SYNC_ARGS. The real calls and the ZITADEL-not-ready recovery both expand them, so the printed recovery is the IdP sync, then the clients sync with every OpenBao flag.sso.mdnow gives gcp-0's hosting and consuming cases, and states GP-20.stage5-verify-openbao-oidc:openbao-oidc-check.sh --cloud gcp --project …is its last statement, so exit 1 and exit 2 both halt. It keeps aws-0's job name, so feat(openbao): reconcile the OIDC client after ZITADEL rotates it #2078's contract suite guards both clouds (flags known to the parser, the call is the last statement). A consuming gcp-0 skips it and says why. The stale "There is no stage 3" header is rewritten.store_writeadds the version whensecrets createreportsALREADY_EXISTS.--apply.Stage 3 and stage 5, hosting branch
flowchart LR S2["stage 2<br/>writes gke/configure/.tls/ca.pem"] --> W["wait for ZITADEL"] W -- "not ready" --> R["print recovery:<br/>IdP sync + clients sync<br/>from the same arg arrays"] W --> IDP["zitadel-idp.sh sync IDP_SYNC_ARGS<br/>Google IdP + groups Action<br/>(PAT resolved as hosting)"] IDP --> CL["zitadel-oidc-clients.sh sync CLIENT_SYNC_ARGS<br/>--openbao-* / --mirror-openbao"] CL --> SM[("Secret Manager<br/>client secrets, zitadel-project-id")] CL --> MIR["mirror: full payload if absent,<br/>else MIRRORED_FIELDS"] CL --> OIDC["reconcile_openbao_oidc<br/>auth/oidc config + role"] MIR --> BAO[("OpenBao gcp-0<br/>bao.priv.gcp.ogenki.io")] OIDC --> BAO MIR --> FS["force-sync ExternalSecrets<br/>on mirrored paths"] BAO --> S5{"stage 5<br/>openbao-oidc-check.sh"} S5 -- "exit 1 / 2" --> HALT["deploy halts"]The consumer branch (
--idp-cloud) is unchanged, and a guard keeps it that way: it passes no--openbao-*flag and no--mirror-openbao.When stage 5 halts (e.g. ZITADEL not ready within 45 min, or stage 3 exiting early on a failed
get-credentials), the deploy ends red, as on aws-0, and the standalonegke/configureapply aftergke/initdoes not run. After the printed recovery, re-run the deploy (or at leastgke/configure) so the vars ConfigMap takes the newly publishedzitadel_project_id. On a first-ever build, also runsecret-store.sh grantforheadlamp-oauth2-proxy, which the recovery text does not print.GP-20: the fresh PAT wins only on the hosting cloud
IDP_CLOUD == CLOUD, and always forzitadel-idp.sh):security/iam-admin-patis read first. It overwrites the storedzitadel-iam-admin-patwhen they differ.security/iam-admin-patelsewhere would overwrite that cloud's stored PAT.Owner pre-flight (P-11)
Before the first GCP deploy, delete the stale Secret Manager
zitadel-iam-admin-pat. GP-20 only adds a new version, so the stale one stays enabled and readable by version number.Live objects
--applymutatesresolve_zitadel_pat hosting(both scripts)zitadel-iam-admin-pat/ AWSzitadel/iam-admin-patsecurity/iam-admin-patzitadel-idp.sh synczitadel-oidc-clients.sh syncpublish_project_id(GCP hosting)zitadel-project-idmanaged-by=zitadel-oidc-clients; new version only on changegke/configurezitadel_project_id--mirror-openbaoplatform/*,apps/*(perbao-map.sh)MIRRORED_FIELDSpatched onto an existing oneforce_sync_mirroredopenbao-<mount>store and key match a mirrored pathforce-sync=<epoch>annotation (--overwrite); a failure only warnsreconcile_openbao_oidcauth/oidc/config,auth/oidc/role/<role>bound_audiencesrotated to what ZITADEL issuedMerge class
Platform fix: merged when green and reviewed (owner, 2026-09-29; GP-21).
No ADR: fixes, not a technology choice.
Gates
The branch was rebased onto
origin/main5dc3e23e(#2124, a victoria-metrics chart bump) before this push, and every gate was re-run after the rebase.test-zitadel-oidc-clients-mirror.sh71c56398(404 wrote onlyGF_AUTH_GENERIC_OAUTH_CLIENT_ID;force_sync_mirroredmissing). Now exit 0, 22 oktest-cloud-secret-store.shtest-gcp-gke-init-workflow.pyPASS. Five workflow mutants are all caughttest-zitadel-oidc-clients-openbao.shtest-no-secret-argv.shtest-openbao-oidc-check.shwith 91 oktest-terramate-script-refs.sh./scripts/ci/validate-manifests.sh(viatask check)Valid: 2096, Invalid: 0, Skipped: 0validate-links.sh/verify-doc-paths.sh/validate-doc-claims.shvalidate-idp-topology.shaws hosts, all other clouds suspendedshellcheck -x -S warning(7 touched scripts)terramate listTM-OK;script info deploylistsstage5-verify-openbao-oidcfor both cloudstask checkci:test39 passed, 1 skipped (vectornot installed), 0 failedLive evidence
Placeholder for Phase 8.3–8.4 (gcp-0 hosting deploy from
integration/agent-factory):[warn];[synced ]lines for the mirrored ExternalSecretsopenbao-oidc-check.shexit 0grafana-envvarsin OpenBao carriesGF_SECURITY_ADMIN_USER/PASSWORD, and grafana-operator authenticatesplatform/*path isSecretSyncedhttps://auth.gcp.cloud.ogenki.io