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 (lib.mdDoc "the Kanidm client");
69 enableServer = lib.mkEnableOption (lib.mdDoc "the Kanidm server");
70 enablePam = lib.mkEnableOption (lib.mdDoc "the Kanidm PAM and NSS integration");
71
72 package = lib.mkPackageOptionMD 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 = lib.mdDoc "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 = lib.mdDoc ''
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 = lib.mdDoc "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 = lib.mdDoc ''
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 = lib.mdDoc "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 = lib.mdDoc "TLS chain in pem format.";
119 type = lib.types.path;
120 };
121 tls_key = lib.mkOption {
122 description = lib.mdDoc "TLS key in pem format.";
123 type = lib.types.path;
124 };
125 log_level = lib.mkOption {
126 description = lib.mdDoc "Log level of the server.";
127 default = "info";
128 type = lib.types.enum [ "info" "debug" "trace" ];
129 };
130 role = lib.mkOption {
131 description = lib.mdDoc "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 };
136 };
137 default = { };
138 description = lib.mdDoc ''
139 Settings for Kanidm, see
140 [the documentation](https://kanidm.github.io/kanidm/stable/server_configuration.html)
141 and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/server.toml)
142 for possible values.
143 '';
144 };
145
146 clientSettings = lib.mkOption {
147 type = lib.types.submodule {
148 freeformType = settingsFormat.type;
149
150 options.uri = lib.mkOption {
151 description = lib.mdDoc "Address of the Kanidm server.";
152 example = "http://127.0.0.1:8080";
153 type = lib.types.str;
154 };
155 };
156 description = lib.mdDoc ''
157 Configure Kanidm clients, needed for the PAM daemon. See
158 [the documentation](https://kanidm.github.io/kanidm/stable/client_tools.html#kanidm-configuration)
159 and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/config)
160 for possible values.
161 '';
162 };
163
164 unixSettings = lib.mkOption {
165 type = lib.types.submodule {
166 freeformType = settingsFormat.type;
167
168 options.pam_allowed_login_groups = lib.mkOption {
169 description = lib.mdDoc "Kanidm groups that are allowed to login using PAM.";
170 example = "my_pam_group";
171 type = lib.types.listOf lib.types.str;
172 };
173 };
174 description = lib.mdDoc ''
175 Configure Kanidm unix daemon.
176 See [the documentation](https://kanidm.github.io/kanidm/stable/integrations/pam_and_nsswitch.html#the-unix-daemon)
177 and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/unixd)
178 for possible values.
179 '';
180 };
181 };
182
183 config = lib.mkIf (cfg.enableClient || cfg.enableServer || cfg.enablePam) {
184 assertions =
185 [
186 {
187 assertion = !cfg.enableServer || ((cfg.serverSettings.tls_chain or null) == null) || (!lib.isStorePath cfg.serverSettings.tls_chain);
188 message = ''
189 <option>services.kanidm.serverSettings.tls_chain</option> points to
190 a file in the Nix store. You should use a quoted absolute path to
191 prevent this.
192 '';
193 }
194 {
195 assertion = !cfg.enableServer || ((cfg.serverSettings.tls_key or null) == null) || (!lib.isStorePath cfg.serverSettings.tls_key);
196 message = ''
197 <option>services.kanidm.serverSettings.tls_key</option> points to
198 a file in the Nix store. You should use a quoted absolute path to
199 prevent this.
200 '';
201 }
202 {
203 assertion = !cfg.enableClient || options.services.kanidm.clientSettings.isDefined;
204 message = ''
205 <option>services.kanidm.clientSettings</option> needs to be configured
206 if the client is enabled.
207 '';
208 }
209 {
210 assertion = !cfg.enablePam || options.services.kanidm.clientSettings.isDefined;
211 message = ''
212 <option>services.kanidm.clientSettings</option> needs to be configured
213 for the PAM daemon to connect to the Kanidm server.
214 '';
215 }
216 {
217 assertion = !cfg.enableServer || (cfg.serverSettings.domain == null
218 -> cfg.serverSettings.role == "WriteReplica" || cfg.serverSettings.role == "WriteReplicaNoUI");
219 message = ''
220 <option>services.kanidm.serverSettings.domain</option> can only be set if this instance
221 is not a ReadOnlyReplica. Otherwise the db would inherit it from
222 the instance it follows.
223 '';
224 }
225 ];
226
227 environment.systemPackages = lib.mkIf cfg.enableClient [ cfg.package ];
228
229 systemd.services.kanidm = lib.mkIf cfg.enableServer {
230 description = "kanidm identity management daemon";
231 wantedBy = [ "multi-user.target" ];
232 after = [ "network.target" ];
233 serviceConfig = lib.mkMerge [
234 # Merge paths and ignore existing prefixes needs to sidestep mkMerge
235 (defaultServiceConfig // {
236 BindReadOnlyPaths = mergePaths (defaultServiceConfig.BindReadOnlyPaths ++ certPaths);
237 })
238 {
239 StateDirectory = "kanidm";
240 StateDirectoryMode = "0700";
241 RuntimeDirectory = "kanidmd";
242 ExecStart = "${cfg.package}/bin/kanidmd server -c ${serverConfigFile}";
243 User = "kanidm";
244 Group = "kanidm";
245
246 BindPaths = [
247 # To create the socket
248 "/run/kanidmd:/run/kanidmd"
249 ];
250
251 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
252 CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
253 # This would otherwise override the CAP_NET_BIND_SERVICE capability.
254 PrivateUsers = lib.mkForce false;
255 # Port needs to be exposed to the host network
256 PrivateNetwork = lib.mkForce false;
257 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
258 TemporaryFileSystem = "/:ro";
259 }
260 ];
261 environment.RUST_LOG = "info";
262 };
263
264 systemd.services.kanidm-unixd = lib.mkIf cfg.enablePam {
265 description = "Kanidm PAM daemon";
266 wantedBy = [ "multi-user.target" ];
267 after = [ "network.target" ];
268 restartTriggers = [ unixConfigFile clientConfigFile ];
269 serviceConfig = lib.mkMerge [
270 defaultServiceConfig
271 {
272 CacheDirectory = "kanidm-unixd";
273 CacheDirectoryMode = "0700";
274 RuntimeDirectory = "kanidm-unixd";
275 ExecStart = "${cfg.package}/bin/kanidm_unixd";
276 User = "kanidm-unixd";
277 Group = "kanidm-unixd";
278
279 BindReadOnlyPaths = [
280 "-/etc/kanidm"
281 "-/etc/static/kanidm"
282 "-/etc/ssl"
283 "-/etc/static/ssl"
284 "-/etc/passwd"
285 "-/etc/group"
286 ];
287 BindPaths = [
288 # To create the socket
289 "/run/kanidm-unixd:/var/run/kanidm-unixd"
290 ];
291 # Needs to connect to kanidmd
292 PrivateNetwork = lib.mkForce false;
293 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
294 TemporaryFileSystem = "/:ro";
295 }
296 ];
297 environment.RUST_LOG = "info";
298 };
299
300 systemd.services.kanidm-unixd-tasks = lib.mkIf cfg.enablePam {
301 description = "Kanidm PAM home management daemon";
302 wantedBy = [ "multi-user.target" ];
303 after = [ "network.target" "kanidm-unixd.service" ];
304 partOf = [ "kanidm-unixd.service" ];
305 restartTriggers = [ unixConfigFile clientConfigFile ];
306 serviceConfig = {
307 ExecStart = "${cfg.package}/bin/kanidm_unixd_tasks";
308
309 BindReadOnlyPaths = [
310 "/nix/store"
311 "-/etc/resolv.conf"
312 "-/etc/nsswitch.conf"
313 "-/etc/hosts"
314 "-/etc/localtime"
315 "-/etc/kanidm"
316 "-/etc/static/kanidm"
317 ];
318 BindPaths = [
319 # To manage home directories
320 "/home"
321 # To connect to kanidm-unixd
322 "/run/kanidm-unixd:/var/run/kanidm-unixd"
323 ];
324 # CAP_DAC_OVERRIDE is needed to ignore ownership of unixd socket
325 CapabilityBoundingSet = [ "CAP_CHOWN" "CAP_FOWNER" "CAP_DAC_OVERRIDE" "CAP_DAC_READ_SEARCH" ];
326 IPAddressDeny = "any";
327 # Need access to users
328 PrivateUsers = false;
329 # Need access to home directories
330 ProtectHome = false;
331 RestrictAddressFamilies = [ "AF_UNIX" ];
332 TemporaryFileSystem = "/:ro";
333 Restart = "on-failure";
334 };
335 environment.RUST_LOG = "info";
336 };
337
338 # These paths are hardcoded
339 environment.etc = lib.mkMerge [
340 (lib.mkIf cfg.enableServer {
341 "kanidm/server.toml".source = serverConfigFile;
342 })
343 (lib.mkIf options.services.kanidm.clientSettings.isDefined {
344 "kanidm/config".source = clientConfigFile;
345 })
346 (lib.mkIf cfg.enablePam {
347 "kanidm/unixd".source = unixConfigFile;
348 })
349 ];
350
351 system.nssModules = lib.mkIf cfg.enablePam [ cfg.package ];
352
353 system.nssDatabases.group = lib.optional cfg.enablePam "kanidm";
354 system.nssDatabases.passwd = lib.optional cfg.enablePam "kanidm";
355
356 users.groups = lib.mkMerge [
357 (lib.mkIf cfg.enableServer {
358 kanidm = { };
359 })
360 (lib.mkIf cfg.enablePam {
361 kanidm-unixd = { };
362 })
363 ];
364 users.users = lib.mkMerge [
365 (lib.mkIf cfg.enableServer {
366 kanidm = {
367 description = "Kanidm server";
368 isSystemUser = true;
369 group = "kanidm";
370 packages = [ cfg.package ];
371 };
372 })
373 (lib.mkIf cfg.enablePam {
374 kanidm-unixd = {
375 description = "Kanidm PAM daemon";
376 isSystemUser = true;
377 group = "kanidm-unixd";
378 };
379 })
380 ];
381 };
382
383 meta.maintainers = with lib.maintainers; [ erictapen Flakebi ];
384 meta.buildDocsInSandbox = false;
385}