Set up single sign-on
A freshly deployed cluster has ZITADEL running and nothing configured in it. These are the steps that turn that into a working Google login for Grafana, Harbor, the Flux UI and Headlamp — the same steps on both clouds, with different values.
For what the resulting stack actually does — and why ZITADEL sits in the middle of it — see Authentication.
The values for your cluster
Everything below is the same on both clouds except these:
aws-0 | gcp-0 | |
|---|---|---|
--cluster | aws-0 | gcp-0 |
--cloud | aws | gcp |
| account flag | --region eu-west-3 | --project ogenki-435905 |
IDP_URL | https://auth.cloud.ogenki.io | https://auth.gcp.cloud.ogenki.io |
PRIVATE_DOMAIN | priv.aws.ogenki.io | priv.gcp.ogenki.io |
| step 2 | not needed — External Secrets uses EKS Pod Identity, granted by OpenTofu | required |
Set them once and the rest of the page copies straight into a shell:
On aws-0:
CL="--cluster aws-0 --cloud aws --region eu-west-3"
IDP_URL=https://auth.cloud.ogenki.io
PRIVATE_DOMAIN=priv.aws.ogenki.ioOn gcp-0:
CL="--cluster gcp-0 --cloud gcp --project ogenki-435905"
IDP_URL=https://auth.gcp.cloud.ogenki.io
PRIVATE_DOMAIN=priv.gcp.ogenki.ioThe steps
The order is load-bearing: each one creates what the next reads. Running only the first leaves you with OIDC clients and no way to log in through Google.
Every step is idempotent — re-running prints [skip …] and changes nothing.
# 1. Register the OIDC clients, the project, its roles, and
# projectRoleAssertion. That last one is not optional: with it off ZITADEL
# puts NO roles in any token AND leaves ctx.v1.user.grants empty inside the
# groups action, so every consumer authenticates and then has no groups.
./scripts/zitadel-oidc-clients.sh sync $CL --apply# 2. GCP ONLY. Let External Secrets read what step 1 just created. Those secrets
# did not exist when the OpenTofu stack applied, so nothing granted access to
# them. On AWS this step does not exist — External Secrets authenticates with
# EKS Pod Identity, which OpenTofu already granted.
./scripts/secret-store.sh grant --cloud gcp --project ogenki-435905 --apply# 3. The Google identity provider, the LOGIN POLICY entry that actually enables
# it, and the action that flattens project roles into a `groups` claim.
# Creating the provider without the policy entry gives "User not found".
IDP_URL=$IDP_URL ./scripts/zitadel-idp.sh sync $CL --apply
# 4. Harbor's auth mode. Harbor stores this in its DATABASE, not in the chart,
# so no manifest can express it and a fresh cluster has no SSO button.
PRIVATE_DOMAIN=$PRIVATE_DOMAIN ./scripts/harbor-oidc.sh sync $CL --applyThen log in once through Google, at any consumer. That first login is what CREATES your ZITADEL user — the IdP auto-registers it — so there is nobody to authorise before it. Afterwards:
# 5. Give yourself the admin role. Group-based RBAC (cluster-admin via the
# `admin` group) does nothing until a user actually holds it.
./scripts/zitadel-oidc-clients.sh sync $CL --grant-admin you@example.com --applyThe one step no script can do
The OAuth client’s authorized redirect URI, on the Google side, must list:
https://auth.<public domain>/ui/login/login/externalidp/callbackGoogle accepts many redirect URIs on one client, so a single client serves every
cluster — add both clusters’ URIs once and you are done.
zitadel-idp.sh prints the exact URI on every run and cannot add it for you.
Then check it worked
Open Grafana, Harbor, the Flux UI or Headlamp. You should be offered a Google button rather than a username field, and after logging in your ZITADEL user exists.
The per-cloud verify pages cover the rest of the post-deploy checks: aws-0 · gcp-0.
aws-0 bootstraps its
database from a frozen backup, so a cluster rebuilt today comes back with
whatever OIDC clients and roles existed when that seed was taken — not the ones
this page creates. Re-run steps 1, 3 and 4 after a restore; they are idempotent
and will fill in whatever the seed is missing. See
Restore a database.Related
- Authentication — what the stack does and where the two clouds genuinely differ
- ADR-0024 — why each cloud runs its own ZITADEL rather than sharing one