↓ Skip to main content
  1. Posts/
  2. IAM and IdP Solutions/

ZITADEL - A Modern Open-Source IAM Solution

Stilicho2011
Author
Stilicho2011
Writing about homelab, self-hosting, automation and open-source solutions
Table of Contents
IAM Solutions - This article is part of a series.
Part : This Article

What ZITADEL is, and why I even looked at it
#

Having gone through pretty much every notable self-hosted SSO/IAM option one by one - Authentik, Authelia, Keycloak - it would’ve been odd to skip ZITADEL. It keeps coming up as “the thing to look at once you’ve outgrown Authelia but Keycloak feels like overkill”, and I wanted to see for myself whether that’s actually true.

It was built by the Swiss company CAOS AG (St. Gallen), and the project now lives under its own ZITADEL Inc. brand, with CAOS AG staying on as a local operating subsidiary - a detail more for trivia than practice. In short, it’s an open-source IAM platform: user management, authentication, authorization, but unlike Authelia it doesn’t need you to bring an already-configured SSO provider - it is the provider, with its own user base, MFA, and admin console.

If you want the short version of where it sits next to the other three, I’ve got a separate comparison of Authelia/Authentik/Keycloak/ZITADEL that breaks it down by use case.

If this article or the video were useful, you can support the channel on Boosty - link in the contacts.

What’s inside
#

There’s no point retyping the docs, but a few things are worth calling out.

Protocols - OIDC, OAuth 2.0, and SAML 2.0 out of the box, so hooking it up to a modern web app is almost never a problem.

Multi-tenancy out of the box - you can run several organizations inside one instance, each with its own users and permissions. For a one-person homelab that’s honestly overkill - but if, like me, you’re running ZITADEL for more than just yourself (family, a small team), splitting things into organizations stops being a checkbox feature and starts being genuinely useful.

MFA - Email/OTP, TOTP (the usual Google Authenticator), SMS, WebAuthn (so YubiKeys work too). Configurable globally or per organization.

API and CLI - REST, gRPC, its own zitadel CLI. If you’re the kind of person who automates access management through GitOps/CI, there’s plenty to work with here - though for a typical homelab setup you probably won’t touch any of it, and that’s fine.

RBAC/ABAC - roles at the project, application, and user level, plus custom claims and scopes in tokens if you need to pass something non-standard through.

Deployment - either the developers’ hosted SaaS, or self-hosted via Docker, Kubernetes, or Podman. The database is PostgreSQL or CockroachDB, no third option.

How it stacks up against the neighbors
#

FeatureZITADELKeycloakAuth0Authelia / TinyAuth
Open Source✅✅❌✅
OIDC/SAML✅✅✅❌/partial
MFA✅✅✅❌/partial
Multi-tenant✅Partial✅❌
Self-service UI✅❌✅❌
API/CLI✅Partial✅❌

Roughly speaking: where Authelia just opens or closes a door in front of an already-configured app, ZITADEL (like Keycloak) is a full-blown identity provider in its own right - its own user base, roles, tokens. The price for that is that it’s heavier and nowhere near as “install and forget” as Authelia.

Deploying it
#

Here’s the same docker-compose I used in the video:

services:
    zitadel:
        restart: "always"
        container_name: zitadel
        networks:
            - zitadel
            - proxy
        image: "ghcr.io/zitadel/zitadel:v4.15.0" # don't use :latest - pin the version explicitly, check the current one at https://github.com/zitadel/zitadel/releases
        command: 'start-from-init --masterkey "${MASTERKEY_32}" --tlsMode external'
        env_file: .env
        environment:
            ZITADEL_DATABASE_POSTGRES_HOST: zitadel-db
            ZITADEL_DATABASE_POSTGRES_PORT: 5432
            ZITADEL_DATABASE_POSTGRES_DATABASE: zitadel
            ZITADEL_DATABASE_POSTGRES_USER_USERNAME: ${DB_USER}
            ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: ${DB_PASSWORD}
            ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: disable
            ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: ${DB_ADMIN_USER}
            ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: ${DB_ADMIN_PASSWORD}
            ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: disable
            ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME: ${ZITADEL_LOGIN_USER} 
            ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD: ${ZITADEL_LOGIN_PASSWORD}
            ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED: false
            ZITADEL_FIRSTINSTANCE_ORG_NAME: HomeLab
            ZITADEL_PORT: 8080 #For External TLS (Also use h2c)
            ZITADEL_EXTERNALPORT: 443
            ZITADEL_EXTERNALDOMAIN: ${ZITADEL_SUBDOMAIN}.${DOMAIN_NAME} #eg. auth.DOMAIN_NAME
            ZITADEL_EXTERNALSECURE: true
        labels:
            - "traefik.enable=true"
            - "traefik.http.routers.zitadel.entrypoints=http"
            - "traefik.http.routers.zitadel.rule=Host(`${ZITADEL_SUBDOMAIN}.${DOMAIN_NAME}`)"
            - "traefik.http.middlewares.https-redirect.redirectscheme.scheme=https"
            - "traefik.http.routers.zitadel.middlewares=https-redirect"
            - "traefik.http.routers.zitadel-secure.entrypoints=https"
            - "traefik.http.routers.zitadel-secure.rule=Host(`${ZITADEL_SUBDOMAIN}.${DOMAIN_NAME}`)"
            - "traefik.http.routers.zitadel-secure.tls=true"
            - "traefik.http.routers.zitadel-secure.service=zitadel"
            - "traefik.http.services.zitadel.loadbalancer.server.scheme=h2c"
            - "traefik.http.services.zitadel.loadbalancer.passHostHeader=true"
            - "traefik.http.services.zitadel.loadbalancer.server.port=8080"
            - "traefik.docker.network=proxy"
        depends_on:
            zitadel-db:
                condition: "service_healthy"

    zitadel-db:
        restart: "always"
        container_name: zitadel-db
        image: postgres:17-alpine
        env_file: .env
        environment:
            POSTGRES_USER: ${DB_ADMIN_USER}
            POSTGRES_PASSWORD: ${DB_ADMIN_PASSWORD}
        networks:
            - zitadel
        healthcheck:
            test:
                [
                    "CMD-SHELL",
                    "pg_isready",
                    "-d",
                    "zitadel",
                    "-U",
                    "${DB_ADMIN_USER}",
                ]
            interval: "10s"
            timeout: "30s"
            retries: 5
            start_period: "20s"
        volumes:
            - ${DB_LOCATION}:/var/lib/postgresql/data

networks:
    zitadel:
        name: zitadel
    proxy:
        external: true
        name: proxy

And the .env next to it:

MASTERKEY_32=GENERATE_RANDOM_KEY_LENGTH_32_ChangeMe  #Replace Master Key (tr -dc A-Za-z0-9 </dev/urandom | head -c 32)
DB_USER=Enter_DB_User_ChangeMe
DB_PASSWORD=Enter_DB_Password_ChangeMe
DB_ADMIN_USER=Enter_DB_Root_User_ChangeMe
DB_ADMIN_PASSWORD=Enter_DB_Root_Password_ChangeMe
ZITADEL_LOGIN_USER=Loginusername@Your_Domain_ChangeMe.com
ZITADEL_LOGIN_PASSWORD=Zitadel_Password_ChangeMe
ZITADEL_SUBDOMAIN=zitadel
DOMAIN_NAME=Your_Domain_ChangeMe.com
DB_LOCATION=/path/to/directory/zitadel/zitadel-db

A couple of things worth knowing before you actually get stuck on them:

  • MASTERKEY_32 - has to be exactly 32 characters, no more, no less. The comment next to it generates one for you (tr -dc A-Za-z0-9 </dev/urandom | head -c 32), but if you’d rather pick your own, count the length carefully - otherwise ZITADEL just refuses to start with a not-very-helpful error about the master key.
  • command: start-from-init - this is specifically the first-run command, and it’s what creates the initial organization and user from the ZITADEL_FIRSTINSTANCE_* variables. Once the instance is up and you’ve logged in, some guides suggest switching to the plain start command instead. I left start-from-init in place - it’s safe to keep using on subsequent runs too (it just finds nothing left to initialize and moves on) - but it’s worth knowing the plain start exists.
  • loadbalancer.server.scheme=h2c in the Traefik labels - not a typo, not decoration. ZITADEL talks gRPC, and gRPC doesn’t work over plain HTTP/1.1 - it needs HTTP/2 without TLS between Traefik and the container (h2c, “HTTP/2 Cleartext”). Drop this line to “simplify things” and you’ll get cryptic auth failures on the way in that give no hint about the real cause.

If you’re plugging in your own ZITADEL_LOGIN_USER/PASSWORD with real values - remember this is the first admin account, not just a “test user to poke around with.” That’s the login you’ll actually use to get into the console and configure everything else.

Bottom line
#

There’s no clean-cut “use ZITADEL instead of X” verdict here - it really depends on your situation. If all you need is a login form in front of a couple of web services, ZITADEL is overkill - grab Authelia or TinyAuth instead. But if you want your own identity provider with roles, MFA, and an API - without the administrative weight of Keycloak - ZITADEL is worth trying properly, not just reading about in yet another comparison list.

IAM Solutions - This article is part of a series.
Part : This Article

Related