Zitadel - Hardened Identity and Access Management
by Lynxroute
Zitadel 4.19.4 - CIS Level 1 hardened identity provider on Ubuntu 24.04 LTS, SBOM + CIS
What is Zitadel
Zitadel is an open-source identity and access management platform. It issues and validates identities for your own applications over the standards they already speak: OpenID Connect 1.0, OAuth 2.0 (including device and token-exchange flows), SAML 2.0 and SCIM 2.0. Users authenticate with passwords, passkeys/WebAuthn or one-time codes, and can be split across multiple organizations in one instance for multi-tenant products. Zitadel is event-sourced: every change is appended to an immutable log, so the audit trail is the storage model rather than a feature on top. Administration happens in a web Console, and everything it does is also on the API.
Why self-host Zitadel
Identity is the one system that sees every login, every session and every user attribute you hold. Self-hosting keeps that data in your own subscription and region, which turns GDPR residency and retention into a configuration choice, and cost scales with the VM rather than per monthly active user.
What this VM image adds
Security hardening:
- Unique admin credential per instance - the administrator password, both database passwords and the encryption master key are generated on first boot into /root/zitadel-credentials.txt (0600). Nothing is baked into the image
- Self-registration disabled - anonymous visitors cannot create accounts; enable it in the Console when you want public sign-up
- TLS perimeter - nginx terminates HTTPS on 443 with a per-instance certificate generated at first boot; Certbot is pre-installed for a CA-signed one
- Bundled PostgreSQL 17 on loopback only - the event store is unreachable from the network, uses scram-sha-256 and has no trust rule
- Consent screens cannot be framed - X-Frame-Options DENY
- Telemetry off - no usage reporting leaves the instance
- CVE scan - every image is scanned with Trivy before release
- UFW firewall - the API, the login service and PostgreSQL are blocked externally; only 22, 80 and 443 open
- fail2ban - SSH brute-force protection
- AppArmor - mandatory access control
OS hardening (CIS Level 1):
- CIS Level 1 hardened - CIS Ubuntu 24.04 LTS Level 1 Benchmark via ansible-lockdown
- auditd - system call auditing for critical paths
- SSH hardening - PasswordAuthentication disabled, key-only access
- Kernel hardening - SYN cookies, ASLR, rp_filter, TCP BBR
- Azure IMDS egress - pre-configured (169.254.169.254, 168.63.129.16)
Compliance artifacts (inside the VM):
- SBOM - CycloneDX 1.6 at /etc/lynxroute/sbom.json
- CIS Conformance Report - OpenSCAP HTML at /etc/lynxroute/cis-report.html
- Tailored CIS profile - /usr/share/doc/lynxroute/CIS_TAILORED_PROFILE.md
- Licence and source offer - /usr/share/doc/lynxroute/LICENSES.txt carries the AGPL-3.0 text and the exact upstream revision built from
Quick Start
- Deploy the VM (Standard_D2s_v3 or larger recommended)
- SSH: ssh -i key.pem <username>@<PUBLIC_IP> (username set during VM creation, default: azureuser)
- Read the generated credentials: sudo cat /root/zitadel-credentials.txt
- Open https://<PUBLIC_IP>/ui/console, sign in, change the password and add a second factor
- Point your application at https://<PUBLIC_IP>/.well-known/openid-configuration
Creating your first users
This image ships one administrator and no mail provider, so plan the first accounts before inviting anyone.
- Self-registration is disabled by default. Enable it under Console -> Settings -> Login Behaviour -> "Register allowed" if you want public sign-up.
- No mail provider is configured, so invitation and password-reset mails cannot be sent yet. Create each user under Console -> Users -> New with an initial password you set, leave "Email verified" on, and pass it on out of band.
- For real invitation mails, configure SMTP under Console -> Settings -> Notification providers first.
- To move onto your own hostname, run sudo zitadel-set-domain idp.example.com so the OIDC issuer follows. Do this before registering applications.
Restrict port 443 to the networks that need it until the instance is fully configured.