From c8d35df580f2af1eef1586501775ea820c666c6d Mon Sep 17 00:00:00 2001 From: Quinn Unger Date: Tue, 11 Aug 2026 17:46:58 +0100 Subject: [PATCH] zitadel bootstrapped --- hosts/lala/default.nix | 1 + hosts/lala/traefik.nix | 23 ++++++ hosts/lala/zitadel.nix | 158 +++++++++++++++++++++++++++++++++++++++++ secrets/secrets.yaml | 8 ++- 4 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 hosts/lala/zitadel.nix diff --git a/hosts/lala/default.nix b/hosts/lala/default.nix index bff5094..28c1b02 100644 --- a/hosts/lala/default.nix +++ b/hosts/lala/default.nix @@ -14,6 +14,7 @@ ./proxies.nix ./tangled.nix ./traefik.nix + ./zitadel.nix ]; catppuccin = { enable = true; diff --git a/hosts/lala/traefik.nix b/hosts/lala/traefik.nix index 194a047..c677e11 100644 --- a/hosts/lala/traefik.nix +++ b/hosts/lala/traefik.nix @@ -7,6 +7,7 @@ }: let netbirdDomain = config.services.netbird.server.domain; + zitadelDomain = config.services.zitadel.settings.ExternalDomain; dashboardPort = (builtins.head config.services.nginx.virtualHosts.${netbirdDomain}.listen).port; in { @@ -49,6 +50,20 @@ in }; http = { routers = { + zitadel-grpc = { + rule = "Host(`${zitadelDomain}`)"; + entryPoints = "websecure"; + tls.certResolver = "letsencrypt"; + service = "zitadel-grpc"; + priority = 10; + }; + zitadel = { + rule = "Host(`${zitadelDomain}`) && PathPrefix(`/ui/v2/login`)"; + entryPoints = "websecure"; + tls.certResolver = "letsencrypt"; + service = "zitadel"; + priority = 100; + }; netbird-dashboard = { rule = "Host(`${netbirdDomain}`)"; entryPoints = "websecure"; @@ -86,6 +101,14 @@ in }; }; services = { + zitadel-grpc.loadBalancer = { + passHostHeader = true; + servers = [ { url = "h2c://localhost:${toString config.services.zitadel.settings.Port}"; } ]; + }; + zitadel.loadBalancer = { + passHostHeader = true; + servers = [ { url = "http://localhost:${toString config.services.zitadel.settings.Port}"; } ]; + }; netbird-dash.loadBalancer.servers = [ { url = "http://localhost:${toString dashboardPort}"; } ]; diff --git a/hosts/lala/zitadel.nix b/hosts/lala/zitadel.nix new file mode 100644 index 0000000..459baf0 --- /dev/null +++ b/hosts/lala/zitadel.nix @@ -0,0 +1,158 @@ +{ + inputs, + config, + pkgs, + lib, + ... +}: +let + dbPassword = config.sops.placeholder."lala/zitadel/dbPassword"; +in +{ + sops.secrets = { + "lala/zitadel/masterKey".owner = config.services.zitadel.user; + "lala/zitadel/dbPassword" = { }; + "lala/zitadel/adminPass" = { }; + }; + + # Initial database setup + sops.templates."zitadel-init.sql" = { + owner = config.users.users.postgres.name; + restartUnits = [ "postgresql.service" ]; + content = '' + CREATE ROLE zitadel WITH LOGIN PASSWORD '${dbPassword}' CREATEDB CREATEROLE; + GRANT ALL PRIVILEGES ON DATABASE zitadel TO zitadel; + ''; + }; + services.postgresql = { + enable = true; + package = pkgs.postgresql_17; + + ensureDatabases = [ "zitadel" ]; + + # Enable TCP/IP for localhost connections + enableTCPIP = true; + + # Authentication rules + authentication = pkgs.lib.mkOverride 10 '' + # Local Unix socket connections use peer authentication + local all all peer + + # TCP localhost: allow zitadel user to connect to any database + host all zitadel 127.0.0.1/32 trust + host all zitadel ::1/128 trust + ''; + + initialScript = config.sops.templates."zitadel-init.sql".path; + + settings = { + max_connections = 100; + shared_buffers = "256MB"; + }; + }; + + # Ensure the zitadel role always has CREATEROLE for migrations + sops.templates."zitadel-postgres-setup.sql" = { + owner = config.users.users.postgres.name; + restartUnits = [ "zitadel-postgres-setup.service" ]; + content = '' + DO $$ + BEGIN + IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'zitadel') THEN + CREATE ROLE zitadel WITH LOGIN PASSWORD '${dbPassword}' CREATEDB CREATEROLE; + ELSE + ALTER ROLE zitadel WITH PASSWORD '${dbPassword}' CREATEDB CREATEROLE; + END IF; + END + $$; + + GRANT ALL PRIVILEGES ON DATABASE zitadel TO zitadel; + ''; + }; + systemd.services = { + zitadel-postgres-setup = { + description = "Ensure Zitadel PostgreSQL user has required privileges"; + wantedBy = [ "multi-user.target" ]; + after = [ "postgresql.service" ]; + requires = [ "postgresql.service" ]; + + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + User = config.users.users.postgres.name; + }; + script = "${config.services.postgresql.package}/bin/psql -d postgres -f ${ + config.sops.templates."zitadel-postgres-setup.sql".path + }"; + }; + zitadel = { + after = [ + "postgresql.service" + "zitadel-postgres-setup.service" + ]; + requires = [ + "postgresql.service" + "zitadel-postgres-setup.service" + ]; + }; + }; + + sops.templates."zitadel-settings-db-password.yaml" = { + owner = config.services.zitadel.user; + restartUnits = [ "zitadel.service" ]; + content = '' + Database: + postgres: + User: + Password: ${dbPassword} + Admin: + Password: ${dbPassword} + ''; + }; + + services.zitadel = { + enable = true; + openFirewall = false; # Traefik handles this for us + masterKeyFile = config.sops.secrets."lala/zitadel/masterKey".path; + tlsMode = "external"; + extraSettingsPaths = [ config.sops.templates."zitadel-settings-db-password.yaml".path ]; + settings = { + Port = 9323; + ExternalPort = 443; + ExternalDomain = "auth.moppu.dev"; + ExternalSecure = true; + Database = { + postgres = { + Host = "localhost"; + Port = 5432; + Database = "zitadel"; + User = { + Username = "zitadel"; + SSL.Mode = "disable"; + }; + Admin = { + Username = "zitadel"; + SSL.Mode = "disable"; + }; + }; + }; + }; + steps = { + FirstInstance = { + InstanceName = "Zitadel"; + Org.Human = { + UserName = "admin"; + FirstName = "Admin"; + LastName = "User"; + Password = "Placeholder@393Password!"; + DisplayName = "Administrator"; + Email = { + Address = "admin@moppu.dev"; + Verified = true; + }; + PasswordChangeRequired = true; + }; + }; + }; + }; +} diff --git a/secrets/secrets.yaml b/secrets/secrets.yaml index 5ec5802..24b28ab 100644 --- a/secrets/secrets.yaml +++ b/secrets/secrets.yaml @@ -26,6 +26,10 @@ lala: idpKey: ENC[AES256_GCM,data:cRgySkw7V8E+DnAg7HecynO1PUeR+lth6rQQJ/85W6rCxD5KIuzZ9zcwmRA=,iv:/Hcm7xpALsMX6qceL6/hpRocQxB7JJ56A8wnh8QVNVs=,tag:JK5T8QsXU9rEb05sJAjG6w==,type:str] crowdsecLAPI: ENC[AES256_GCM,data:kaRIk/Y0eozW+jrfeqiqliE40cTIkhbremdk7Fj7UT5noCV1/iG61AWqkKy3vem6vf7l4HpwHFwSp3zDc/pS7RJnszMP95BySsPZ/ni1DsKYd6Ypq84Er2yIQ+1GOEuwH9Sb,iv:c50HL3b555eWPMzfPioBhSijsHSDZsZxiattRZQfSuQ=,tag:ETweA7ZaA54rjNyc9NyW6w==,type:str] crowdsecCAPI: ENC[AES256_GCM,data:lp2pedoGTB40ExtN8TI6My6uyU+oBHQHGF2DHdeycyYgYmAKHlLJ+2GSeMpbquRiOuWQnqnluy1z8xTXHbiCt1upPKX6VbSgsnLhJZgxSZvVA1UlLTALlzXkHysYNLkV1hSJnKZ8Xmi3lakRAY9TEakQDW/RtatC/D82ZGGcbX5lQNLT4IIX3RM5vYifaygkttKQYEUr1rVQOj12CUiM+xDz,iv:PMTJxiLj3RSGCzMF1unjrjWsrkmz9L+ZqzQcyfhEJdk=,tag:hXxv6NMkJivzCx8PyCSMPw==,type:str] + zitadel: + masterKey: ENC[AES256_GCM,data:pS0lkUd12p6lrHyGZlnBCVAx6v2TQGWS3Ccc8AfJ94M=,iv:lFp50mm6kW/jpI0Rln1EOQN5TrfRrVZi3alnyw9uYzo=,tag:InuJEz6n+ktNZGROd4N+tg==,type:str] + dbPassword: ENC[AES256_GCM,data:ISgvRpJlppU2yd0sn65sUyI443O+ZFZJVrE04XZ6bf8=,iv:DkHeqgNx050pHEUSgLcYkdHDeLRyrSL8o3Tnxls5GeI=,tag:V4jnl8o5K+vwt/xQ+xHJog==,type:str] + adminPass: ENC[AES256_GCM,data:/HHKMAVdRyh7usVu+ubsCdYWCpXMZlAeqXZDPF7g1xM=,iv:fwVEPYbrhQOAXraj+VHJxzodZlCF39CBG6JWsOUGGfk=,tag:G9zIsxHgnsEZnyCoEG8M3w==,type:str] sops: age: - enc: | @@ -73,7 +77,7 @@ sops: JFaroA1RPAmhCC0nSl4seBSIuRl8QrPjfcBJy4mFycll5Mp/0/WwLA== -----END AGE ENCRYPTED FILE----- recipient: age1cpqgsevvfgk0nnmxacsdcwsrkefd0xcx7wkdlyaqj8vgvfvnm4rq4npjv6 - lastmodified: "2026-08-07T14:45:44Z" - mac: ENC[AES256_GCM,data:atqw5sMkgL+59P4FIzpgW7MgnH4LBWC3+Ed7EHRrlX6+R7yapOIeIeei1L+GPFiYAzCdsTk9kksUL3ZmqsOw/t57Ssp8Q2yMpAyt8bQF/n8JP3zGIzuYM7LVPdKjA7B+YFuL8wjjXwh+km00OSqU6I90XoTPBnSqbAMZitevzAU=,iv:hZ2EdNW3oKXxL7bL+bP8V47GENfnR5PVpg78SxsdQho=,tag:hDrHtArXU/L9CrBr2rWKrg==,type:str] + lastmodified: "2026-08-11T15:10:29Z" + mac: ENC[AES256_GCM,data:bblWFd8qihBcpmKCBW8lQWoDuSrUjqa+6AZTc7C6MqwMbGHtuDK1Tk1kwETqvgi9Nu2XFkacQ70CzkcIHRgw/OspuYKOrrqEjcIonbBiESNpqiDQp6S2mzxQSonyLo6yaMN1HcGMf2m1EGBUQGeePPNbtb3ojAI+5IOJf5+058o=,iv:/NWWvR0DOnCEx9WT5B8Kp2JMf4Av9BIWwzW9p8CX2Bo=,tag:R1yiGf/Ob1udo0l9Xmk02g==,type:str] unencrypted_suffix: _unencrypted version: 3.13.3 -- 2.51.2