This repository has no description
5.9 kB
187 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, other OIDC) linked to their account. The login policy, the GitHub
46 # and Google 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 };
149 clan.core.vars.generators.zitadel-github =
150 idpCredentials "GitHub" "https://github.com/settings/applications/new";
151 clan.core.vars.generators.zitadel-google =
152 idpCredentials "Google" "https://console.cloud.google.com/apis/credentials";
153
154 # The database and its owner come from ensureDatabases/ensureUsers, so the
155 # zitadel role needs no CREATEROLE/CREATEDB. `zitadel init zitadel` creates
156 # only the schemas inside it; start-from-setup then skips the role and
157 # database creation that start-from-init would attempt.
158 systemd.services.zitadel = {
159 after = [ "postgresql.target" ];
160 requires = [ "postgresql.target" ];
161 serviceConfig = {
162 ExecStartPre = "${lib.getExe' cfg.package "zitadel"} init zitadel --config ${configFile}";
163 ExecStart = lib.mkForce "${lib.getExe' cfg.package "zitadel"} start-from-setup ${args}";
164 };
165 };
166
167 services.postgresql = {
168 enable = true;
169 ensureDatabases = [ "zitadel" ];
170 ensureUsers = [
171 {
172 name = "zitadel";
173 ensureDBOwnership = true;
174 }
175 ];
176 };
177
178 # Zitadel serves the console, OIDC and its gRPC APIs on one port over h2c.
179 services.nginx.virtualHosts.${domain} = {
180 enableACME = true;
181 forceSSL = true;
182 locations."/".extraConfig = ''
183 grpc_pass grpc://127.0.0.1:${toString port};
184 grpc_set_header Host $host;
185 '';
186 };
187}