# k8s/infra/iam/zitadel-values.yaml # ZITADEL — Authentik replacement. Coexistence is deliberate: Authentik keeps # running, read-only, in the same `iam` namespace until the last consumer moves. # Migration plan: ~/workplace/homelab-doc/ZITADEL_MIGRATION.html (outside git). # # Chart: zitadel/zitadel 10.4.0, app v4.19.2. The plan cites v4.19.4; the chart # has not published it yet. Pin the chart, never `*`, until the v4.19.4 chart # exists — a silent app bump invalidates the verified masterkey/passwap facts. # # URL: https://zitadel.riotpiao.com. TLS terminates at ingress-nginx, which # serves the wildcard riotpiao-com-tls as its default-ssl-certificate, so # ExternalSecure=true and TLS.Enabled=false (no per-rule tls: block needed). # # Database: CNPG cluster zitadel-db (wave 2). The DSN comes from CNPG's # generated `zitadel-db-app` secret. When ZITADEL_DATABASE_POSTGRES_DSN is set, # ZITADEL ignores the discrete Database.Postgres.* fields, so neither a # password nor a host is written into this file. # # Masterkey: Secret `zitadel-masterkey` (k8s/argocd/secrets/ # zitadel-masterkey.enc.yaml), key `masterkey`, exactly 32 bytes. It wraps the # rows in system.encryption_keys, which encrypt every client secret and private # key. Losing it makes those values unreadable: keep the offline sealed copy. # # Ingress uses the chart's own objects (controller: nginx adds the gRPC backend # annotation ZITADEL needs for its HTTP/2 API). Do not hand-write a second # Ingress for this host. zitadel: configmapConfig: ExternalSecure: true ExternalDomain: zitadel.riotpiao.com TLS: Enabled: false # Rendered into a Secret by the chart; read by pods and init jobs alike. masterkeySecretName: zitadel-masterkey resources: requests: { memory: "512Mi", cpu: "250m" } limits: { memory: "2Gi", cpu: "1" } # FirstInstance defaults stay for the FIRST sync: the chart creates org # `zitadel` plus the `iam-admin` machine user and its machine-key Secret. # Terraform consumes that key to declare projects, roles, OIDC apps and the # groups action. # # SECOND-SYNC HAZARD — read before enabling automated sync. # The chart marks eight objects as `pre-install,pre-upgrade` hooks (two # ServiceAccounts, two ConfigMaps, Role, RoleBinding, zitadel-init, # zitadel-setup), and one cleanup Job as `post-delete`. ArgoCD maps the first # eight to PreSync with `before-hook-creation`: it deletes and re-creates them # on EVERY sync, not only on a chart change. # - zitadel-init (`init zitadel`) — schema step # - zitadel-setup (`setup`) — first instance + iam-admin machine key # On an initialized instance the setup step can fail, which aborts the sync # before the Deployment is applied. If it succeeds instead, its # machinekeyWriter container re-applies the iam-admin key Secret that # Terraform reads, which silently rotates a credential in use. # After the first sync reports healthy: # 1. set configmapConfig.FirstInstance.Skip: true here — permanently: the # setup steps are schema-only once the instance exists, and Skip alone # stops the machinekeyWriter rotation # 2. set initJob.enabled: false below # 3. commit, sync once, then enable automated+selfHeal on the Application # For a later schema upgrade, re-enable initJob.enabled only — never set # FirstInstance.Skip back to false. # ArgoCD never prunes a hook Job, so a Job that failed before step 2 stays and # keeps ZitadelSetupJobFailed firing until a human deletes it. # DSN mode (chart renders this through the zitadel.dbEnv helper). CNPG's `uri` # key is a postgresql:// URL, which ZITADEL accepts as a DSN. env: - name: ZITADEL_DATABASE_POSTGRES_DSN valueFrom: secretKeyRef: name: zitadel-db-app key: uri # Bundled Bitnami Postgres is disabled: the CNPG cluster above is the system of # record. Enabling it would create a second, unmanaged database. postgresql: enabled: false # CNPG already created the database, the `app` user and its grants, so the init # job runs only the ZITADEL-internal step. Running the full default command # would try to create a database and a superuser it does not own. initJob: command: "zitadel" activeDeadlineSeconds: 600 # Two API replicas: the read path survives one node drain, and both share the # same masterkey and database. The login UI stays at one replica — a session # cookie is not shared, so a second pod buys nothing. replicaCount: 2 revisionHistoryLimit: 3 ingress: enabled: true className: nginx controller: nginx login: enabled: true replicaCount: 1 ingress: enabled: true className: nginx # Chart-owned ServiceMonitor. The cluster's Prometheus runs with # serviceMonitorSelectorNilUsesHelmValues=false, so the release label is not # required for discovery. Endpoint is /debug/metrics on port http2-server. metrics: enabled: true serviceMonitor: enabled: true scrapeInterval: 30s scrapeTimeout: 10s # The 3-CP / 2-worker cluster mixes control-plane taints with worker capacity. # Without the toleration a rolling upgrade can leave both replicas Pending. tolerations: - key: node-role.kubernetes.io/control-plane operator: Exists effect: NoSchedule affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: topologyKey: kubernetes.io/hostname labelSelector: matchLabels: app.kubernetes.io/name: zitadel