Skip to content
0024 · IdP per cloud

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:

GateWhereEffect
deploy_identity_provider = trueopentofu/gcp/gke/configureDerives identity_provider_url to auth.<this cluster's public domain>
spec.suspend removed / resumedclusters/gcp-0/security/zitadel.yamlFlux 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/zitadel had to become genuinely cloud-neutral to be shared. Its Gateway carried four service.beta.kubernetes.io/aws-load-balancer-* annotations, meaningless on GKE where Cilium’s GatewayClass provisions the LoadBalancer; they moved to security/aws-0/zitadel/gateway-patch.yaml. The eighth instance of a cloud-specific value living in a directory named base/ — see ADR-0023 for the seventh.
  • gcp-0’s database claim differs from aws-0’s in four ways, three of them consequences of the fourth: the GCP Composition, no objectStoreRecovery, no backup (blocked by the barman plugin’s S3-only CiliumNetworkPolicy), and therefore instances: 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_url variable survives intact; it is now the consume half of a two-way choice rather than the only half.

Related

  • ADR-0022 — superseded by this
  • ADR-0019 — why public DNS stays on Route53
  • ADR-0023 — the secret-store naming this depends on