ZITADEL Helm
Overview
ZITADEL deployed via HelmRelease (Flux) with external PostgreSQL (CNPG), Gateway API routing (Cilium Gateway), memory caching. Two containers: main ZITADEL API (Go) and Login UI (Next.js). Init + setup jobs bootstrap the FirstInstance on install.
Latest stable: chart 10.0.4 → ZITADEL v4.15.3. See artifacthub for latest version.
Architecture
Gateway API (e.g., Cilium, Istio, Envoy Gateway)
├── HTTPRoute "zitadel" → zitadel:8080 (h2c)
├── HTTPRoute "zitadel-login" → zitadel-login:3000 (/ui/v2/login)
└── GRPCRoute (optional) → zitadel:8080 (gRPC)
ZITADEL Deployment (zitadel:8080)
├── Init job (schema bootstrap)
├── Setup job (FirstInstance creation)
└── Env vars → DB credentials from K8s secrets
ZITADEL Login Deployment (login:3000)
└── Authenticates to ZITADEL via X.509 RSA keypair (auto-generated by Helm chart)
CNPG Cluster (zitadel-db, PostgreSQL 18)
└── zitadel-db-rw:5432 → ZITADEL connects via DSN
Key Configuration
Database (CNPG — External)
Disabled bundled PostgreSQL subchart. CNPG cluster created separately:
# HelmRelease values
postgresql:
enabled: false
zitadel:
configmapConfig:
Database:
Postgres:
Host: zitadel-db-rw.zitadel.svc.cluster.local
Port: 5432
Database: zitadel
User:
Username: zitadel
SSL:
Mode: disable # or require/verify-full with dbSslCaCrt
Admin:
Username: zitadel
SSL:
Mode: disable
env:
- name: ZITADEL_DATABASE_POSTGRES_USER_PASSWORD
valueFrom:
secretKeyRef:
name: zitadel-db-app
key: password
- name: ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: zitadel-db-app
key: password
Alternative DSN-based config (replaces configmapConfig.Database.Postgres):
zitadel:
env:
- name: ZITADEL_DATABASE_POSTGRES_DSN
valueFrom:
secretKeyRef:
name: zitadel-db-credentials
key: dsn
Secrets Management
Sensitive values via valuesFrom in HelmRelease (not plaintext):
# release.yaml
valuesFrom:
- kind: Secret
name: zitadel-secrets
valuesKey: masterkey
targetPath: zitadel.masterkey
- kind: Secret
name: zitadel-db-credentials
valuesKey: password
targetPath: zitadel.configmapConfig.Database.Postgres.User.Password
- kind: Secret
name: zitadel-db-credentials
valuesKey: password
targetPath: zitadel.configmapConfig.Database.Postgres.Admin.Password
- kind: Secret
name: zitadel-secrets
valuesKey: smtpPassword
targetPath: zitadel.configmapConfig.Notifications.SMTP.Password
Chart 10.x change:
global.sharedSecretwas removed. Login UI auth switched from PAT (chart 9.x) to X.509 RSA keypair. Helm auto-generates the keypair on install and reuses it across upgrades. Setlogin.loginServiceKeySecretNameto use an existing secret instead.
Masterkey: Must be exactly 32 bytes printable ASCII. Generate:
tr -dc A-Za-z0-9 </dev/urandom | head -c 32
Set via zitadel.masterkey or zitadel.masterkeySecretName. Loss = data loss.
Gateway API Routing
ingress:
enabled: false # Use Gateway API instead
gateway:
grpcRoute:
enabled: true
parentRefs:
- kind: Gateway
name: my-gateway
namespace: gateway-ns
sectionName: https
Separate HTTPRoute resources for main API and login:
| Route | Host | Backend | Purpose |
|---|---|---|---|
zitadel (HTTPRoute) |
auth.example.com | zitadel:8080 | Main API, console |
zitadel-login (HTTPRoute) |
auth.example.com | zitadel-login:3000 | Login UI at /ui/v2/login |
zitadel-grpc (GRPCRoute, auto) |
auth.example.com | zitadel:8080 | gRPC (if gateway.grpcRoute.enabled) |
Both HTTPRoutes match the same hostname with path-based splitting.
Important: ZITADEL requires end-to-end HTTP/2 (h2c). The Gateway must support h2c backend connections (Cilium does natively).
Caching
Memory cache for small deployments:
zitadel:
configmapConfig:
Caches:
Connectors:
Memory:
Enabled: true
Instance:
Connector: memory
MaxAge: 1h
Organization:
Connector: memory
MaxAge: 1h
Production (Redis/Valkey):
Caches:
Connectors:
Redis:
Enabled: true
Addr: redis-cluster:6379
Instance:
Connector: redis
MaxAge: 10m
FirstInstance / Bootstrapping
Configured in configmapConfig.FirstInstance. Helm chart auto-generates Machine Keys + PAT secrets:
zitadel:
configmapConfig:
FirstInstance:
Org:
Human:
UserName: admin
Password: "Admin123!"
FirstName: Admin
LastName: User
Email: admin@example.com
PasswordChangeRequired: false
Machine:
Machine:
Username: iam-admin
Pat:
ExpirationDate: "2029-01-01T00:00:00Z"
After install, secrets created:
iam-admin— JWT machine keyiam-admin-pat— Personal Access Token (for admin API access){release}-login-service-key— X.509 RSA keypair for Login UI auth (chart 10.x+)login-client— Login UI credential (chart 9.x: PAT; removed in 10.x, replaced by X.509 keypair)
Node Selection
nodeSelector:
kubernetes.io/hostname: dedicated-node
login:
nodeSelector:
kubernetes.io/hostname: dedicated-node
SMTP / Notifications
zitadel:
configmapConfig:
Notifications:
SMTP:
Host: smtp.example.com
Port: 587
User: noreply@example.com
Password: "" # via valuesFrom secret
SenderAddress: noreply@example.com
SenderName: Zitadel Auth
Upgrade
General
- Update
version:in HelmRelease - ZITADEL handles its own DB migrations via init + setup jobs
- Helm hooks orchestrate order: init → setup → deployment
- No manual migration steps needed for patch/minor versions
- Check release notes for breaking changes between major chart versions
- ZITADEL supports PG 14–18. PG 18 requires ZITADEL v4.11.0+
- Masterkey never changes after initial install
- After upgrade, verify console login, OIDC app auth, and DB migration logs
Chart 9.x → 10.x Migration
Breaking changes:
| Change | Impact |
|---|---|
| Login UI auth: PAT → X.509 RSA keypair | Helm auto-generates keypair. Set login.loginServiceKeySecretName to use an existing secret |
global.domain removed |
Set zitadel.configmapConfig.ExternalDomain instead |
initJob.command removed |
Template handles the command internally |
global.sharedSecret removed |
No longer needed — X.509 cert replaces cookie-based login auth |
FirstInstance.MachineKeyPath/PatPath auto-managed |
Helm manages these internally; setting manually causes failures |
Required CNPG changes: The init job in chart 10.x tries to CREATE DATABASE on upgrade. Your PostgreSQL user needs both LOGIN and CREATEDB privileges. For CNPG clusters, add a managed.roles section:
# In your CNPG Cluster spec
managed:
roles:
- name: zitadel # or your DB user
login: true # required — CNPG defaults to login: false
createdb: true # required for init job
ensure: present
Without login: true, the init job fails with: FATAL: role "zitadel" is not permitted to log in (SQLSTATE 28000).
Without createdb: true, the init job fails with: permission denied to create database (SQLSTATE 42501).
Upgrade procedure:
- Apply CNPG
managed.rolesfirst (before chart upgrade) - Update chart version from 9.x to 10.x
- Delete these deprecated values (if present):
global.domain,initJob.command,global.sharedSecretvaluesFrom - Let Helm run init + setup jobs (DB migration)
- Verify console login and OIDC app auth
Quick Reference
| Need | Config path |
|---|---|
| External domain | zitadel.configmapConfig.ExternalDomain |
| TLS termination | At Gateway, set TLS.Enabled: false |
| Database host | zitadel.configmapConfig.Database.Postgres.Host |
| DB SSL mode | Database.Postgres.User.SSL.Mode (disable/require/verify-full) |
| Masterkey | zitadel.masterkey or zitadel.masterkeySecretName |
| Admin bootstrap | zitadel.configmapConfig.FirstInstance.Org.Human |
| SMTP | zitadel.configmapConfig.Notifications.SMTP.* |
| Memory cache | zitadel.configmapConfig.Caches.Connectors.Memory.Enabled: true |
| Node pinning | nodeSelector, login.nodeSelector |
| Disable bundled PG | postgresql.enabled: false |
| Disable Ingress | ingress.enabled: false (use Gateway API instead) |
| Enable gRPC route | gateway.grpcRoute.enabled: true |
Common Mistakes
- Masterkey loss = permanent data loss. Cannot recover. Store in SealedSecret or external vault.
- Bundled PostgreSQL subchart NOT for production. Always disable (
postgresql.enabled: false) and use external CNPG cluster. - Gateway must support h2c backend. Without h2c, ZITADEL console fails. Cilium Gateway supports h2c natively.
- Login route must precede main route on the same hostname, otherwise /ui/v2/login matches the catch-all
/rule and hits the main API instead of the login UI. - env vars take precedence over configmapConfig. If both
ZITADEL_DATABASE_POSTGRES_DSNenv var andconfigmapConfig.Database.Postgresare set, the env var wins. - PasswordChangeRequired: true on FirstInstance.Human will block initial console login until admin changes password.
- SMTP password must be via secret, never in values.yaml. Use
valuesFromin HelmRelease. - FirstInstance fields are install-only. Changing them post-install has no effect. Modify via API or DB directly.
- Chart 10.x init job needs CREATEDB. If using CNPG, add
managed.roleswithlogin: trueandcreatedb: true. Withoutlogin: true, init job fails withFATAL: role "x" is not permitted to log in. - Chart 10.x removed
global.domain. Usezitadel.configmapConfig.ExternalDomaininstead. Also removed:initJob.command,global.sharedSecret. - Chart 10.x login auth changed. Login UI now uses X.509 RSA keypair (auto-generated) instead of PAT. Chart preserves existing keypair across upgrades via
lookup. - Login UI key rotation. To force new keypair, delete the
{release}-login-service-keysecret and trigger Helm upgrade. New keypair is auto-generated.