This repository has no description
7.4 kB
223 lines
1let
2 domain = "id.lassul.us";
3 port = 1411;
4in
5{
6 config,
7 lib,
8 pkgs,
9 ...
10}:
11let
12 cfg = config.services.zitadel;
13 vars = config.clan.core.vars.generators.zitadel.files;
14 yaml = pkgs.formats.yaml { };
15 configFile = yaml.generate "config.yaml" cfg.settings;
16 args = lib.cli.toCommandLineShellGNU { } {
17 config = [ configFile ];
18 steps = [
19 (yaml.generate "steps.yaml" cfg.steps)
20 vars."steps.yaml".path
21 ];
22 masterkeyFile = cfg.masterKeyFile;
23 inherit (cfg) tlsMode;
24 };
25 idpCredentials = name: url: {
26 prompts.client_id = {
27 description = ''
28 ${name} login on ${domain}: client id of an OAuth app (${url})
29 with the callback URL
30 https://${domain}/ui/login/login/externalidp/callback
31 Leave empty to not offer ${name} logins.
32 '';
33 persist = true;
34 };
35 prompts.client_secret = {
36 description = "${name} login on ${domain}: client secret of that OAuth app";
37 type = "hidden";
38 persist = true;
39 };
40 };
41in
42{
43 # Zitadel: identity provider for lassul.us services (OIDC). Anyone can
44 # register; users sign in with a passkey or an external login (GitHub,
45 # Google, their own OpenID provider via byoidc) linked to their account. The
46 # login policy, these logins and the services' projects and OIDC apps come from
47 # lass.zitadel (modules/zitadel); the zitadel-reconcile service applies
48 # them. Further IdPs and the grants of "managed" projects are done in the
49 # console at https://id.lassul.us/ui/console.
50 #
51 # Example projects for services on other hosts, once they speak OIDC (the
52 # client secret ends up in /var/lib/zitadel-apps/<project>-<app>.env here):
53 # lass.zitadel.projects.assist = {
54 # access = "declarative";
55 # users = [ "lass@lassul.us" ];
56 # apps.web.redirectUris = [ "https://assist.lassul.us/<callback>" ];
57 # };
58 # lass.zitadel.projects.flix = {
59 # access = "managed";
60 # # jellyfin-plugin-sso with a provider named id.lassul.us
61 # apps.jellyfin.redirectUris = [ "https://flix.lassul.us/sso/OID/redirect/id.lassul.us" ];
62 # };
63
64 clan.core.vars.generators.zitadel = {
65 files."masterkey".owner = "zitadel";
66 # initial password of the admin user lass, changed at first login
67 files."admin-password".deploy = false;
68 files."steps.yaml".owner = "zitadel";
69 runtimeInputs = [
70 pkgs.jq
71 pkgs.openssl
72 pkgs.pwgen
73 ];
74 script = ''
75 openssl rand -hex 16 | tr -d '\n' > "$out/masterkey"
76 pwgen -s -c -n -y 32 1 | tr -d '\n' > "$out/admin-password"
77 jq -n --rawfile pw "$out/admin-password" \
78 '{FirstInstance: {Org: {Human: {Password: $pw}}}}' > "$out/steps.yaml"
79 '';
80 };
81
82 services.zitadel = {
83 enable = true;
84 masterKeyFile = vars."masterkey".path;
85 settings = {
86 Port = port;
87 ExternalDomain = domain;
88 ExternalPort = 443;
89 ExternalSecure = true;
90 Machine.Identification = {
91 PrivateIp.Enabled = false;
92 Hostname.Enabled = true;
93 Webhook.Enabled = false;
94 };
95 Database.postgres = {
96 Host = "/run/postgresql";
97 Port = 5432;
98 Database = "zitadel";
99 User.Username = "zitadel";
100 Admin.Username = "zitadel";
101 Admin.ExistingDatabase = "zitadel";
102 };
103 };
104 steps.FirstInstance = {
105 InstanceName = "lassul.us";
106 Org = {
107 Name = "lassul.us";
108 Human = {
109 UserName = "lass";
110 FirstName = "lass";
111 LastName = "lassulus";
112 Email = {
113 Address = "lass@lassul.us";
114 Verified = true;
115 };
116 PasswordChangeRequired = true;
117 };
118 };
119 };
120 };
121
122 lass.zitadel.loginPolicy = {
123 allowRegister = true;
124 allowExternalIdp = true;
125 allowUsernamePassword = true;
126 passwordlessType = "PASSWORDLESS_TYPE_ALLOWED";
127 };
128 # Verification codes and password resets. Zitadel asks every user whose
129 # address is unverified (registration, byoidc) to verify it before the
130 # login finishes. Sent as the send-only account id@lassul.us of the
131 # mailserver here (configs/mailserver.nix), which DKIM-signs it.
132 lass.zitadel.smtp = {
133 senderAddress = "id@lassul.us";
134 senderName = "lassul.us ID";
135 host = "mail.lassul.us:465";
136 tls = true;
137 user = "id@lassul.us";
138 passwordFile = config.clan.core.vars.generators.mailserver-id.files."password".path;
139 };
140 lass.zitadel.idps = {
141 GitHub = {
142 type = "github";
143 clientIdFile = config.clan.core.vars.generators.zitadel-github.files.client_id.path;
144 clientSecretFile = config.clan.core.vars.generators.zitadel-github.files.client_secret.path;
145 scopes = [
146 "read:user"
147 "user:email"
148 ];
149 };
150 Google = {
151 type = "google";
152 clientIdFile = config.clan.core.vars.generators.zitadel-google.files.client_id.path;
153 clientSecretFile = config.clan.core.vars.generators.zitadel-google.files.client_secret.path;
154 scopes = [
155 "openid"
156 "profile"
157 "email"
158 ];
159 };
160 # byoidc (configs/byoidc.nix): sign in with any OpenID provider. Anyone
161 # can run one that claims any email address, so these accounts are never
162 # linked to existing ones.
163 byoidc = {
164 type = "oidc";
165 name = "Your own provider";
166 issuer = "https://byoidc.lassul.us";
167 clientId = "zitadel";
168 clientSecretFile = config.clan.core.vars.generators.byoidc.files."client-secret".path;
169 providerOptions = {
170 isLinkingAllowed = false;
171 isCreationAllowed = true;
172 isAutoCreation = true;
173 isAutoUpdate = true;
174 };
175 };
176 };
177 clan.core.vars.generators.zitadel-github =
178 idpCredentials "GitHub" "https://github.com/settings/applications/new";
179 clan.core.vars.generators.zitadel-google =
180 idpCredentials "Google" "https://console.cloud.google.com/apis/credentials";
181
182 # The database and its owner come from ensureDatabases/ensureUsers, so the
183 # zitadel role needs no CREATEROLE/CREATEDB. `zitadel init zitadel` creates
184 # only the schemas inside it; start-from-setup then skips the role and
185 # database creation that start-from-init would attempt.
186 systemd.services.zitadel = {
187 after = [ "postgresql.target" ];
188 requires = [ "postgresql.target" ];
189 serviceConfig = {
190 ExecStartPre = "${lib.getExe' cfg.package "zitadel"} init zitadel --config ${configFile}";
191 ExecStart = lib.mkForce "${lib.getExe' cfg.package "zitadel"} start-from-setup ${args}";
192 };
193 };
194
195 services.postgresql = {
196 enable = true;
197 ensureDatabases = [ "zitadel" ];
198 ensureUsers = [
199 {
200 name = "zitadel";
201 ensureDBOwnership = true;
202 }
203 ];
204 };
205
206 # users, passkeys, IdP links, OIDC apps; its secrets are encrypted with the
207 # masterkey from clan vars
208 lass.backup.state.zitadel.preBackup = ''
209 mkdir -p /var/backup/postgresql
210 ${pkgs.sudo}/bin/sudo -u postgres ${config.services.postgresql.package}/bin/pg_dump \
211 zitadel | ${pkgs.gzip}/bin/gzip > /var/backup/postgresql/zitadel.sql.gz
212 '';
213
214 # Zitadel serves the console, OIDC and its gRPC APIs on one port over h2c.
215 services.nginx.virtualHosts.${domain} = {
216 enableACME = true;
217 forceSSL = true;
218 locations."/".extraConfig = ''
219 grpc_pass grpc://127.0.0.1:${toString port};
220 grpc_set_header Host $host;
221 '';
222 };
223}