Central, self-hosted OIDC/IAM provider for BAUER GROUP, built on Zitadel. It federates to Microsoft Entra ID, keeps its own accounts, syncs profiles/avatars/groups, and provisions itself as code — so it can eventually run fully autonomous.
| Capability | How |
|---|---|
| Federates to Entra ID (and later Google/GitHub/…) | upstream IdP, provisioned via Terraform |
| Own local accounts (need not exist in Entra) | login policy allows local + IdP; federated users are password-less (IdP-only); ForceMFALocalOnly |
| Avatar + extended attributes kept current | directory-sync delta job → native phone + metadata (fax/mobile/office/dept) + avatar |
| Roles/groups drive app login | Entra groups → namespaced project roles + grants; project authorization-on-auth gate |
| Self-service UI | Zitadel Console + Login v2 |
| Everything as code | native config → Terraform (IaC init container) → Python sync; non-destructive |
| Stable migration | subject-preservation (sub = Entra OID) → zero app changes |
| Self-healing, backups, CI/CD | restart + healthchecks, pg_dump sidecar, reusable-workflow pipeline |
┌──────────────┐ OIDC ┌──────────────┐ upstream
apps ───▶ │ Zitadel │ ───SSO───▶│ Entra ID │ (+ Google/GitHub later)
(Outline, │ v4 (h2c) │ └──────────────┘
Documenso) │ sub = OID │
└──────┬───────┘
│ Postgres 18
┌──────▼───────┐ provisioner (OpenTofu, one-shot, non-destructive)
│ database-server │ directory-sync (Graph delta → profiles/avatar/groups)
└──────────────┘ database-backup (pg_dump, --profile backup)
python scripts/generate-env.py # generate secrets into .env
# edit .env: set AZURE_* (optional for standalone) — IAM_DEV_HOSTNAME defaults to zitadel.localhost
docker compose -f docker-compose.development.yml up -d --build
# console: http://zitadel.localhost:8080 (login admin@example.com)
curl http://zitadel.localhost:8080/.well-known/openid-configurationProduction (Traefik): set IAM_HOSTNAME, point DNS at the host, then
docker compose -f docker-compose.traefik.yml up -d. See
docs/installation.md.
config/zitadel/ defaults.yaml + steps.yaml (native Zitadel config — policies, org, admin, SA)
terraform/ IdPs, project, OIDC apps, role catalog, grants (IaC)
src/zitadel/ thin-wrapper image
src/provisioner/ OpenTofu init container (guarded, non-destructive)
src/directory-sync/ directory-sync (Python)
src/database-backup/ pg_dump sidecar (Python)
scripts/generate-env.py cross-platform secret generator
docker-compose.{development,traefik,coolify}.yml
docs/ setup / management / operation / migration guides
- Native config (
config/zitadel/*.yaml) — policies, branding, SMTP, token lifetimes, org + admin + machine SA. - Terraform (
terraform/, run by theprovisionerinit container) — IdPs, project, OIDC apps, role catalog, grants. Non-destructive: never prunes UI-made resources; aborts on any destroy. - Directory-sync (
directory-sync) — Graph delta → extended attributes, avatar, Entra groups → namespaced roles/grants. Login-time JIT refresh is native to Zitadel.
| Guide | Topic |
|---|---|
| architecture.md | the big picture: one instance/host, three orgs + user populations, domain discovery, access model |
| installation.md | deploy (dev / Traefik / Coolify), first login |
| endpoints-and-portals.md | every URL/portal: Console, Login v2, OIDC endpoints, APIs, dev vs prod |
| configuration.md | .env + defaults.yaml/steps.yaml reference |
| provisioning-terraform.md | IaC, state, non-destructive guard, adding apps/IdPs |
| applications-oidc.md | connect your apps as OIDC clients (internal/external tiers, scopes, claims) |
| identity-providers.md | per-org IdPs: Entra single/multi-tenant, Google + social catalog |
| branding-and-login.md | brand colors/logo/font, Login v2 theming, translations, IdP-logo plan |
| directory-sync.md | freshness model (JIT + delta), namespacing |
| users-roles-groups.md | local vs federated, role/grant app-gating, grants-vs-scopes-vs-roles, self-service |
| console-guide.md | view/manage each thing in the Console + how code (TF/branding) re-applies on deploy |
| migration.md | Entra → Zitadel cutover (avatar + data preserved) |
| operations.md | health, upgrades, HA path, decoupling |
| decoupling-from-entra.md | step-by-step runbook to go MS-free (credential migration, clean IdP removal) |
| sizing-and-tuning.md | PG18 presets (SSD/HDD + RAM size) |
| backup-and-restore.md | pg_dump snapshots, off-site, restore |
| security-hardening.md | security posture |
| troubleshooting.md | h2c/502, PG18, duplicate users, … |
| Component | Pin | Note |
|---|---|---|
| Zitadel | v4.15.0 |
PostgreSQL 18 needs ≥ v4.11.0 |
| PostgreSQL | 18-alpine |
volume path /var/lib/postgresql (no /data) |
| OpenTofu | 1.12.0 |
provisioner image base |
| Python | 3.14-alpine |
directory-sync + database-backup sidecars |
This stack (config, compose, Terraform, sidecars) is MIT — see LICENSE. It is not a fork of Zitadel: Zitadel is consumed only as a pre-built container image, so its copyleft does not attach to this repository.
Zitadel is AGPL-3.0. The images this stack pulls (ghcr.io/zitadel/zitadel
and the EP-Zitadel login base) are AGPL-3.0. Running them self-hosted is fine;
if you modify Zitadel or the login and offer it over a network, AGPL §13
requires you to make your modified source available. This repository ships no
Zitadel source — the modified login lives in the separate ep-zitadel repository,
which carries that obligation for its image.
Trademarks. "Zitadel" is a trademark of its respective owner. This project is an independent deployment stack and is not affiliated with, endorsed by, or sponsored by Zitadel; the name is used only nominatively to identify the upstream software it deploys.
See SECURITY.md for how to report a vulnerability, and docs/security-hardening.md for the stack's security posture.