This repository has no description
6.5 kB
203 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 lass.zitadel.idps = {
129 GitHub = {
130 type = "github";
131 clientIdFile = config.clan.core.vars.generators.zitadel-github.files.client_id.path;
132 clientSecretFile = config.clan.core.vars.generators.zitadel-github.files.client_secret.path;
133 scopes = [
134 "read:user"
135 "user:email"
136 ];
137 };
138 Google = {
139 type = "google";
140 clientIdFile = config.clan.core.vars.generators.zitadel-google.files.client_id.path;
141 clientSecretFile = config.clan.core.vars.generators.zitadel-google.files.client_secret.path;
142 scopes = [
143 "openid"
144 "profile"
145 "email"
146 ];
147 };
148 # byoidc (configs/byoidc.nix): sign in with any OpenID provider. Anyone
149 # can run one that claims any email address, so these accounts are never
150 # linked to existing ones.
151 byoidc = {
152 type = "oidc";
153 name = "Your own provider";
154 issuer = "https://byoidc.lassul.us";
155 clientId = "zitadel";
156 clientSecretFile = config.clan.core.vars.generators.byoidc.files."client-secret".path;
157 providerOptions = {
158 isLinkingAllowed = false;
159 isCreationAllowed = true;
160 isAutoCreation = true;
161 isAutoUpdate = true;
162 };
163 };
164 };
165 clan.core.vars.generators.zitadel-github =
166 idpCredentials "GitHub" "https://github.com/settings/applications/new";
167 clan.core.vars.generators.zitadel-google =
168 idpCredentials "Google" "https://console.cloud.google.com/apis/credentials";
169
170 # The database and its owner come from ensureDatabases/ensureUsers, so the
171 # zitadel role needs no CREATEROLE/CREATEDB. `zitadel init zitadel` creates
172 # only the schemas inside it; start-from-setup then skips the role and
173 # database creation that start-from-init would attempt.
174 systemd.services.zitadel = {
175 after = [ "postgresql.target" ];
176 requires = [ "postgresql.target" ];
177 serviceConfig = {
178 ExecStartPre = "${lib.getExe' cfg.package "zitadel"} init zitadel --config ${configFile}";
179 ExecStart = lib.mkForce "${lib.getExe' cfg.package "zitadel"} start-from-setup ${args}";
180 };
181 };
182
183 services.postgresql = {
184 enable = true;
185 ensureDatabases = [ "zitadel" ];
186 ensureUsers = [
187 {
188 name = "zitadel";
189 ensureDBOwnership = true;
190 }
191 ];
192 };
193
194 # Zitadel serves the console, OIDC and its gRPC APIs on one port over h2c.
195 services.nginx.virtualHosts.${domain} = {
196 enableACME = true;
197 forceSSL = true;
198 locations."/".extraConfig = ''
199 grpc_pass grpc://127.0.0.1:${toString port};
200 grpc_set_header Host $host;
201 '';
202 };
203}