The identity provider is deployable on either cloud, defaulting to AWS
Status: Accepted Date: 2026-08-27 Deciders: Platform Team Supersedes: ADR-0022
Context
ADR-0022 made ZITADEL a
singleton on aws-0 serving both clusters, and made its host configurable
through an identity_provider_url variable. The reasoning was sound and still
is: two instances are two user directories, two session stores, and two sets of
OIDC clients with nothing federating them.
What it did not weigh is what the singleton costs a GCP-only platform.
Everything that authenticates through the IdP — Grafana SSO, Headlamp,
OpenWebUI, the Flux UI — depends on an aws-0 that is up. On a repository whose
whole point is that either cloud can stand on its own, that is a hard dependency
from GCP to AWS for the most basic function a platform has.
It showed up the moment a gcp-0 validation run was scoped: nothing on that
cluster could authenticate a user without also building an EKS cluster, an
OpenBao, and an AWS network — roughly doubling both the cost and the time of a
run whose subject was GCP.
The singleton was also never quite as enforced as it read. ADR-0022 says moving the IdP means “changing THREE things together”, with nothing able to check that they agree.
Decision
ZITADEL is a per-cloud deployable component. AWS remains the default host.
gcp-0 gains security/gcp-0/zitadel and its own Flux Kustomization, both off
unless deliberately turned on. Turning it on takes two gates that must agree:
| Gate | Where | Effect |
|---|---|---|
deploy_identity_provider = true | opentofu/gcp/gke/configure | Derives identity_provider_url to auth.<this cluster's public domain> |
spec.suspend removed / resumed | clusters/gcp-0/security/zitadel.yaml | Flux actually deploys it |
Gate 1 alone points every consumer at a hostname the cluster does not serve.
Gate 2 alone runs an instance nothing is configured to use. Neither can enforce
the other, so identity_provider_url is derived from gate 1 rather than
typed a second time — the literal is what let “which cloud hosts the IdP”
become unanswerable from configuration in the first place.
Public DNS stays AWS-owned. auth.<public domain> on gcp-0 resolves
through the same Route53 cross-cloud federation as every other public hostname
there (ADR-0019). This decision moves
where the IdP runs, not where DNS lives.
What this accepts
One user directory per cloud, when both host one. This is the cost ADR-0022 correctly identified, and it is accepted here rather than solved.
It is acceptable because of what these platforms are: throwaway clusters torn
down after every run, whose ZITADEL bootstraps empty each time — the GCP
claim deliberately drops objectStoreRecovery, so there is no long-lived
directory to federate and nothing accumulates across rebuilds.
It would not be acceptable on a production two-cloud deployment. There, one
instance remains right, and deploy_identity_provider = false is the setting
that gives it. The default is unchanged precisely so that nobody gets a second
directory by accident.
Alternatives considered
Keep the singleton and require aws-0 for GCP work. The status quo. Honest
about federation, but it makes the cheaper cloud’s platform untestable on its
own and couples every GCP validation run to an entire second cloud’s
infrastructure. The dependency is one-directional and always in the same
direction, which is what made it worth removing.
Federate two instances. ZITADEL can act as an external IdP to another ZITADEL, which would give per-cloud instances and one identity. It is a real option and the right one if these clusters ever stop being disposable — but it is a standing cross-cloud trust relationship to configure, monitor and rotate, which is a large amount of machinery for platforms that are deleted daily.
Deploy the IdP on GCP and consume it from AWS. Symmetric to the status quo and no better: it moves the dependency rather than removing it, and points it at the cloud with less of the platform on it today.
Consequences
- A GCP-only platform authenticates without any AWS cluster running. That is the point.
security/base/zitadelhad to become genuinely cloud-neutral to be shared. Its Gateway carried fourservice.beta.kubernetes.io/aws-load-balancer-*annotations, meaningless on GKE where Cilium’s GatewayClass provisions the LoadBalancer; they moved tosecurity/aws-0/zitadel/gateway-patch.yaml. The eighth instance of a cloud-specific value living in a directory namedbase/— see ADR-0023 for the seventh.gcp-0’s database claim differs fromaws-0’s in four ways, three of them consequences of the fourth: the GCP Composition, noobjectStoreRecovery, nobackup(blocked by the barman plugin’s S3-only CiliumNetworkPolicy), and thereforeinstances: 1— the XRD refuses a multi-instance cluster with no backup, correctly, since a replica without a backup is a false sense of one.- Two ZITADEL instances mean two sets of OIDC clients. Creating them by hand twice is how they drift, so client creation is scripted against the ZITADEL API rather than done in the console.
- ADR-0022’s
identity_provider_urlvariable survives intact; it is now the consume half of a two-way choice rather than the only half.