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