1{ config, lib, options, pkgs, ... }:
2let
3 cfg = config.services.kanidm;
4 settingsFormat = pkgs.formats.toml { };
5 # Remove null values, so we can document optional values that don't end up in the generated TOML file.
6 filterConfig = lib.converge (lib.filterAttrsRecursive (_: v: v != null));
7 serverConfigFile = settingsFormat.generate "server.toml" (filterConfig cfg.serverSettings);
8 clientConfigFile = settingsFormat.generate "kanidm-config.toml" (filterConfig cfg.clientSettings);
9 unixConfigFile = settingsFormat.generate "kanidm-unixd.toml" (filterConfig cfg.unixSettings);
10 certPaths = builtins.map builtins.dirOf [ cfg.serverSettings.tls_chain cfg.serverSettings.tls_key ];
11
12 # Merge bind mount paths and remove paths where a prefix is already mounted.
13 # This makes sure that if e.g. the tls_chain is in the nix store and /nix/store is already in the mount
14 # paths, no new bind mount is added. Adding subpaths caused problems on ofborg.
15 hasPrefixInList = list: newPath: lib.any (path: lib.hasPrefix (builtins.toString path) (builtins.toString newPath)) list;
16 mergePaths = lib.foldl' (merged: newPath: let
17 # If the new path is a prefix to some existing path, we need to filter it out
18 filteredPaths = lib.filter (p: !lib.hasPrefix (builtins.toString newPath) (builtins.toString p)) merged;
19 # If a prefix of the new path is already in the list, do not add it
20 filteredNew = lib.optional (!hasPrefixInList filteredPaths newPath) newPath;
21 in filteredPaths ++ filteredNew) [];
22
23 defaultServiceConfig = {
24 BindReadOnlyPaths = [
25 "/nix/store"
26 "-/etc/resolv.conf"
27 "-/etc/nsswitch.conf"
28 "-/etc/hosts"
29 "-/etc/localtime"
30 ];
31 CapabilityBoundingSet = [];
32 # ProtectClock= adds DeviceAllow=char-rtc r
33 DeviceAllow = "";
34 # Implies ProtectSystem=strict, which re-mounts all paths
35 # DynamicUser = true;
36 LockPersonality = true;
37 MemoryDenyWriteExecute = true;
38 NoNewPrivileges = true;
39 PrivateDevices = true;
40 PrivateMounts = true;
41 PrivateNetwork = true;
42 PrivateTmp = true;
43 PrivateUsers = true;
44 ProcSubset = "pid";
45 ProtectClock = true;
46 ProtectHome = true;
47 ProtectHostname = true;
48 # Would re-mount paths ignored by temporary root
49 #ProtectSystem = "strict";
50 ProtectControlGroups = true;
51 ProtectKernelLogs = true;
52 ProtectKernelModules = true;
53 ProtectKernelTunables = true;
54 ProtectProc = "invisible";
55 RestrictAddressFamilies = [ ];
56 RestrictNamespaces = true;
57 RestrictRealtime = true;
58 RestrictSUIDSGID = true;
59 SystemCallArchitectures = "native";
60 SystemCallFilter = [ "@system-service" "~@privileged @resources @setuid @keyring" ];
61 # Does not work well with the temporary root
62 #UMask = "0066";
63 };
64
65in
66{
67 options.services.kanidm = {
68 enableClient = lib.mkEnableOption "the Kanidm client";
69 enableServer = lib.mkEnableOption "the Kanidm server";
70 enablePam = lib.mkEnableOption "the Kanidm PAM and NSS integration";
71
72 package = lib.mkPackageOption pkgs "kanidm" {};
73
74 serverSettings = lib.mkOption {
75 type = lib.types.submodule {
76 freeformType = settingsFormat.type;
77
78 options = {
79 bindaddress = lib.mkOption {
80 description = "Address/port combination the webserver binds to.";
81 example = "[::1]:8443";
82 type = lib.types.str;
83 };
84 # Should be optional but toml does not accept null
85 ldapbindaddress = lib.mkOption {
86 description = ''
87 Address and port the LDAP server is bound to. Setting this to `null` disables the LDAP interface.
88 '';
89 example = "[::1]:636";
90 default = null;
91 type = lib.types.nullOr lib.types.str;
92 };
93 origin = lib.mkOption {
94 description = "The origin of your Kanidm instance. Must have https as protocol.";
95 example = "https://idm.example.org";
96 type = lib.types.strMatching "^https://.*";
97 };
98 domain = lib.mkOption {
99 description = ''
100 The `domain` that Kanidm manages. Must be below or equal to the domain
101 specified in `serverSettings.origin`.
102 This can be left at `null`, only if your instance has the role `ReadOnlyReplica`.
103 While it is possible to change the domain later on, it requires extra steps!
104 Please consider the warnings and execute the steps described
105 [in the documentation](https://kanidm.github.io/kanidm/stable/administrivia.html#rename-the-domain).
106 '';
107 example = "example.org";
108 default = null;
109 type = lib.types.nullOr lib.types.str;
110 };
111 db_path = lib.mkOption {
112 description = "Path to Kanidm database.";
113 default = "/var/lib/kanidm/kanidm.db";
114 readOnly = true;
115 type = lib.types.path;
116 };
117 tls_chain = lib.mkOption {
118 description = "TLS chain in pem format.";
119 type = lib.types.path;
120 };
121 tls_key = lib.mkOption {
122 description = "TLS key in pem format.";
123 type = lib.types.path;
124 };
125 log_level = lib.mkOption {
126 description = "Log level of the server.";
127 default = "info";
128 type = lib.types.enum [ "info" "debug" "trace" ];
129 };
130 role = lib.mkOption {
131 description = "The role of this server. This affects the replication relationship and thereby available features.";
132 default = "WriteReplica";
133 type = lib.types.enum [ "WriteReplica" "WriteReplicaNoUI" "ReadOnlyReplica" ];
134 };
135 online_backup = {
136 path = lib.mkOption {
137 description = "Path to the output directory for backups.";
138 type = lib.types.path;
139 default = "/var/lib/kanidm/backups";
140 };
141 schedule = lib.mkOption {
142 description = "The schedule for backups in cron format.";
143 type = lib.types.str;
144 default = "00 22 * * *";
145 };
146 versions = lib.mkOption {
147 description = ''
148 Number of backups to keep.
149
150 The default is set to `0`, in order to disable backups by default.
151 '';
152 type = lib.types.ints.unsigned;
153 default = 0;
154 example = 7;
155 };
156 };
157 };
158 };
159 default = { };
160 description = ''
161 Settings for Kanidm, see
162 [the documentation](https://kanidm.github.io/kanidm/stable/server_configuration.html)
163 and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/server.toml)
164 for possible values.
165 '';
166 };
167
168 clientSettings = lib.mkOption {
169 type = lib.types.submodule {
170 freeformType = settingsFormat.type;
171
172 options.uri = lib.mkOption {
173 description = "Address of the Kanidm server.";
174 example = "http://127.0.0.1:8080";
175 type = lib.types.str;
176 };
177 };
178 description = ''
179 Configure Kanidm clients, needed for the PAM daemon. See
180 [the documentation](https://kanidm.github.io/kanidm/stable/client_tools.html#kanidm-configuration)
181 and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/config)
182 for possible values.
183 '';
184 };
185
186 unixSettings = lib.mkOption {
187 type = lib.types.submodule {
188 freeformType = settingsFormat.type;
189
190 options = {
191 pam_allowed_login_groups = lib.mkOption {
192 description = "Kanidm groups that are allowed to login using PAM.";
193 example = "my_pam_group";
194 type = lib.types.listOf lib.types.str;
195 };
196 hsm_pin_path = lib.mkOption {
197 description = "Path to a HSM pin.";
198 default = "/var/cache/kanidm-unixd/hsm-pin";
199 type = lib.types.path;
200 };
201 };
202 };
203 description = ''
204 Configure Kanidm unix daemon.
205 See [the documentation](https://kanidm.github.io/kanidm/stable/integrations/pam_and_nsswitch.html#the-unix-daemon)
206 and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/unixd)
207 for possible values.
208 '';
209 };
210 };
211
212 config = lib.mkIf (cfg.enableClient || cfg.enableServer || cfg.enablePam) {
213 assertions =
214 [
215 {
216 assertion = !cfg.enableServer || ((cfg.serverSettings.tls_chain or null) == null) || (!lib.isStorePath cfg.serverSettings.tls_chain);
217 message = ''
218 <option>services.kanidm.serverSettings.tls_chain</option> points to
219 a file in the Nix store. You should use a quoted absolute path to
220 prevent this.
221 '';
222 }
223 {
224 assertion = !cfg.enableServer || ((cfg.serverSettings.tls_key or null) == null) || (!lib.isStorePath cfg.serverSettings.tls_key);
225 message = ''
226 <option>services.kanidm.serverSettings.tls_key</option> points to
227 a file in the Nix store. You should use a quoted absolute path to
228 prevent this.
229 '';
230 }
231 {
232 assertion = !cfg.enableClient || options.services.kanidm.clientSettings.isDefined;
233 message = ''
234 <option>services.kanidm.clientSettings</option> needs to be configured
235 if the client is enabled.
236 '';
237 }
238 {
239 assertion = !cfg.enablePam || options.services.kanidm.clientSettings.isDefined;
240 message = ''
241 <option>services.kanidm.clientSettings</option> needs to be configured
242 for the PAM daemon to connect to the Kanidm server.
243 '';
244 }
245 {
246 assertion = !cfg.enableServer || (cfg.serverSettings.domain == null
247 -> cfg.serverSettings.role == "WriteReplica" || cfg.serverSettings.role == "WriteReplicaNoUI");
248 message = ''
249 <option>services.kanidm.serverSettings.domain</option> can only be set if this instance
250 is not a ReadOnlyReplica. Otherwise the db would inherit it from
251 the instance it follows.
252 '';
253 }
254 ];
255
256 environment.systemPackages = lib.mkIf cfg.enableClient [ cfg.package ];
257
258 systemd.tmpfiles.settings."10-kanidm" = {
259 ${cfg.serverSettings.online_backup.path}.d = {
260 mode = "0700";
261 user = "kanidm";
262 group = "kanidm";
263 };
264 };
265
266 systemd.services.kanidm = lib.mkIf cfg.enableServer {
267 description = "kanidm identity management daemon";
268 wantedBy = [ "multi-user.target" ];
269 after = [ "network.target" ];
270 serviceConfig = lib.mkMerge [
271 # Merge paths and ignore existing prefixes needs to sidestep mkMerge
272 (defaultServiceConfig // {
273 BindReadOnlyPaths = mergePaths (defaultServiceConfig.BindReadOnlyPaths ++ certPaths);
274 })
275 {
276 StateDirectory = "kanidm";
277 StateDirectoryMode = "0700";
278 RuntimeDirectory = "kanidmd";
279 ExecStart = "${cfg.package}/bin/kanidmd server -c ${serverConfigFile}";
280 User = "kanidm";
281 Group = "kanidm";
282
283 BindPaths = [
284 # To create the socket
285 "/run/kanidmd:/run/kanidmd"
286 # To store backups
287 cfg.serverSettings.online_backup.path
288 ];
289
290 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
291 CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
292 # This would otherwise override the CAP_NET_BIND_SERVICE capability.
293 PrivateUsers = lib.mkForce false;
294 # Port needs to be exposed to the host network
295 PrivateNetwork = lib.mkForce false;
296 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
297 TemporaryFileSystem = "/:ro";
298 }
299 ];
300 environment.RUST_LOG = "info";
301 };
302
303 systemd.services.kanidm-unixd = lib.mkIf cfg.enablePam {
304 description = "Kanidm PAM daemon";
305 wantedBy = [ "multi-user.target" ];
306 after = [ "network.target" ];
307 restartTriggers = [ unixConfigFile clientConfigFile ];
308 serviceConfig = lib.mkMerge [
309 defaultServiceConfig
310 {
311 CacheDirectory = "kanidm-unixd";
312 CacheDirectoryMode = "0700";
313 RuntimeDirectory = "kanidm-unixd";
314 ExecStart = "${cfg.package}/bin/kanidm_unixd";
315 User = "kanidm-unixd";
316 Group = "kanidm-unixd";
317
318 BindReadOnlyPaths = [
319 "-/etc/kanidm"
320 "-/etc/static/kanidm"
321 "-/etc/ssl"
322 "-/etc/static/ssl"
323 "-/etc/passwd"
324 "-/etc/group"
325 ];
326 BindPaths = [
327 # To create the socket
328 "/run/kanidm-unixd:/var/run/kanidm-unixd"
329 ];
330 # Needs to connect to kanidmd
331 PrivateNetwork = lib.mkForce false;
332 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
333 TemporaryFileSystem = "/:ro";
334 }
335 ];
336 environment.RUST_LOG = "info";
337 };
338
339 systemd.services.kanidm-unixd-tasks = lib.mkIf cfg.enablePam {
340 description = "Kanidm PAM home management daemon";
341 wantedBy = [ "multi-user.target" ];
342 after = [ "network.target" "kanidm-unixd.service" ];
343 partOf = [ "kanidm-unixd.service" ];
344 restartTriggers = [ unixConfigFile clientConfigFile ];
345 serviceConfig = {
346 ExecStart = "${cfg.package}/bin/kanidm_unixd_tasks";
347
348 BindReadOnlyPaths = [
349 "/nix/store"
350 "-/etc/resolv.conf"
351 "-/etc/nsswitch.conf"
352 "-/etc/hosts"
353 "-/etc/localtime"
354 "-/etc/kanidm"
355 "-/etc/static/kanidm"
356 ];
357 BindPaths = [
358 # To manage home directories
359 "/home"
360 # To connect to kanidm-unixd
361 "/run/kanidm-unixd:/var/run/kanidm-unixd"
362 ];
363 # CAP_DAC_OVERRIDE is needed to ignore ownership of unixd socket
364 CapabilityBoundingSet = [ "CAP_CHOWN" "CAP_FOWNER" "CAP_DAC_OVERRIDE" "CAP_DAC_READ_SEARCH" ];
365 IPAddressDeny = "any";
366 # Need access to users
367 PrivateUsers = false;
368 # Need access to home directories
369 ProtectHome = false;
370 RestrictAddressFamilies = [ "AF_UNIX" ];
371 TemporaryFileSystem = "/:ro";
372 Restart = "on-failure";
373 };
374 environment.RUST_LOG = "info";
375 };
376
377 # These paths are hardcoded
378 environment.etc = lib.mkMerge [
379 (lib.mkIf cfg.enableServer {
380 "kanidm/server.toml".source = serverConfigFile;
381 })
382 (lib.mkIf options.services.kanidm.clientSettings.isDefined {
383 "kanidm/config".source = clientConfigFile;
384 })
385 (lib.mkIf cfg.enablePam {
386 "kanidm/unixd".source = unixConfigFile;
387 })
388 ];
389
390 system.nssModules = lib.mkIf cfg.enablePam [ cfg.package ];
391
392 system.nssDatabases.group = lib.optional cfg.enablePam "kanidm";
393 system.nssDatabases.passwd = lib.optional cfg.enablePam "kanidm";
394
395 users.groups = lib.mkMerge [
396 (lib.mkIf cfg.enableServer {
397 kanidm = { };
398 })
399 (lib.mkIf cfg.enablePam {
400 kanidm-unixd = { };
401 })
402 ];
403 users.users = lib.mkMerge [
404 (lib.mkIf cfg.enableServer {
405 kanidm = {
406 description = "Kanidm server";
407 isSystemUser = true;
408 group = "kanidm";
409 packages = [ cfg.package ];
410 };
411 })
412 (lib.mkIf cfg.enablePam {
413 kanidm-unixd = {
414 description = "Kanidm PAM daemon";
415 isSystemUser = true;
416 group = "kanidm-unixd";
417 };
418 })
419 ];
420 };
421
422 meta.maintainers = with lib.maintainers; [ erictapen Flakebi ];
423 meta.buildDocsInSandbox = false;
424}