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 = if hasPrefixInList filteredPaths newPath then [] else [ 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 serverSettings = lib.mkOption {
73 type = lib.types.submodule {
74 freeformType = settingsFormat.type;
75
76 options = {
77 bindaddress = lib.mkOption {
78 description = lib.mdDoc "Address/port combination the webserver binds to.";
79 example = "[::1]:8443";
80 type = lib.types.str;
81 };
82 # Should be optional but toml does not accept null
83 ldapbindaddress = lib.mkOption {
84 description = lib.mdDoc ''
85 Address and port the LDAP server is bound to. Setting this to `null` disables the LDAP interface.
86 '';
87 example = "[::1]:636";
88 default = null;
89 type = lib.types.nullOr lib.types.str;
90 };
91 origin = lib.mkOption {
92 description = lib.mdDoc "The origin of your Kanidm instance. Must have https as protocol.";
93 example = "https://idm.example.org";
94 type = lib.types.strMatching "^https://.*";
95 };
96 domain = lib.mkOption {
97 description = lib.mdDoc ''
98 The `domain` that Kanidm manages. Must be below or equal to the domain
99 specified in `serverSettings.origin`.
100 This can be left at `null`, only if your instance has the role `ReadOnlyReplica`.
101 While it is possible to change the domain later on, it requires extra steps!
102 Please consider the warnings and execute the steps described
103 [in the documentation](https://kanidm.github.io/kanidm/stable/administrivia.html#rename-the-domain).
104 '';
105 example = "example.org";
106 default = null;
107 type = lib.types.nullOr lib.types.str;
108 };
109 db_path = lib.mkOption {
110 description = lib.mdDoc "Path to Kanidm database.";
111 default = "/var/lib/kanidm/kanidm.db";
112 readOnly = true;
113 type = lib.types.path;
114 };
115 tls_chain = lib.mkOption {
116 description = lib.mdDoc "TLS chain in pem format.";
117 type = lib.types.path;
118 };
119 tls_key = lib.mkOption {
120 description = lib.mdDoc "TLS key in pem format.";
121 type = lib.types.path;
122 };
123 log_level = lib.mkOption {
124 description = lib.mdDoc "Log level of the server.";
125 default = "default";
126 type = lib.types.enum [ "default" "verbose" "perfbasic" "perffull" ];
127 };
128 role = lib.mkOption {
129 description = lib.mdDoc "The role of this server. This affects the replication relationship and thereby available features.";
130 default = "WriteReplica";
131 type = lib.types.enum [ "WriteReplica" "WriteReplicaNoUI" "ReadOnlyReplica" ];
132 };
133 };
134 };
135 default = { };
136 description = lib.mdDoc ''
137 Settings for Kanidm, see
138 [the documentation](https://github.com/kanidm/kanidm/blob/master/kanidm_book/src/server_configuration.md)
139 and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/server.toml)
140 for possible values.
141 '';
142 };
143
144 clientSettings = lib.mkOption {
145 type = lib.types.submodule {
146 freeformType = settingsFormat.type;
147
148 options.uri = lib.mkOption {
149 description = lib.mdDoc "Address of the Kanidm server.";
150 example = "http://127.0.0.1:8080";
151 type = lib.types.str;
152 };
153 };
154 description = lib.mdDoc ''
155 Configure Kanidm clients, needed for the PAM daemon. See
156 [the documentation](https://github.com/kanidm/kanidm/blob/master/kanidm_book/src/client_tools.md#kanidm-configuration)
157 and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/config)
158 for possible values.
159 '';
160 };
161
162 unixSettings = lib.mkOption {
163 type = lib.types.submodule {
164 freeformType = settingsFormat.type;
165
166 options.pam_allowed_login_groups = lib.mkOption {
167 description = lib.mdDoc "Kanidm groups that are allowed to login using PAM.";
168 example = "my_pam_group";
169 type = lib.types.listOf lib.types.str;
170 };
171 };
172 description = lib.mdDoc ''
173 Configure Kanidm unix daemon.
174 See [the documentation](https://github.com/kanidm/kanidm/blob/master/kanidm_book/src/pam_and_nsswitch.md#the-unix-daemon)
175 and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/unixd)
176 for possible values.
177 '';
178 };
179 };
180
181 config = lib.mkIf (cfg.enableClient || cfg.enableServer || cfg.enablePam) {
182 assertions =
183 [
184 {
185 assertion = !cfg.enableServer || ((cfg.serverSettings.tls_chain or null) == null) || (!lib.isStorePath cfg.serverSettings.tls_chain);
186 message = ''
187 <option>services.kanidm.serverSettings.tls_chain</option> points to
188 a file in the Nix store. You should use a quoted absolute path to
189 prevent this.
190 '';
191 }
192 {
193 assertion = !cfg.enableServer || ((cfg.serverSettings.tls_key or null) == null) || (!lib.isStorePath cfg.serverSettings.tls_key);
194 message = ''
195 <option>services.kanidm.serverSettings.tls_key</option> points to
196 a file in the Nix store. You should use a quoted absolute path to
197 prevent this.
198 '';
199 }
200 {
201 assertion = !cfg.enableClient || options.services.kanidm.clientSettings.isDefined;
202 message = ''
203 <option>services.kanidm.clientSettings</option> needs to be configured
204 if the client is enabled.
205 '';
206 }
207 {
208 assertion = !cfg.enablePam || options.services.kanidm.clientSettings.isDefined;
209 message = ''
210 <option>services.kanidm.clientSettings</option> needs to be configured
211 for the PAM daemon to connect to the Kanidm server.
212 '';
213 }
214 {
215 assertion = !cfg.enableServer || (cfg.serverSettings.domain == null
216 -> cfg.serverSettings.role == "WriteReplica" || cfg.serverSettings.role == "WriteReplicaNoUI");
217 message = ''
218 <option>services.kanidm.serverSettings.domain</option> can only be set if this instance
219 is not a ReadOnlyReplica. Otherwise the db would inherit it from
220 the instance it follows.
221 '';
222 }
223 ];
224
225 environment.systemPackages = lib.mkIf cfg.enableClient [ pkgs.kanidm ];
226
227 systemd.services.kanidm = lib.mkIf cfg.enableServer {
228 description = "kanidm identity management daemon";
229 wantedBy = [ "multi-user.target" ];
230 after = [ "network.target" ];
231 serviceConfig = lib.mkMerge [
232 # Merge paths and ignore existing prefixes needs to sidestep mkMerge
233 (defaultServiceConfig // {
234 BindReadOnlyPaths = mergePaths (defaultServiceConfig.BindReadOnlyPaths ++ certPaths);
235 })
236 {
237 StateDirectory = "kanidm";
238 StateDirectoryMode = "0700";
239 ExecStart = "${pkgs.kanidm}/bin/kanidmd server -c ${serverConfigFile}";
240 User = "kanidm";
241 Group = "kanidm";
242
243 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
244 CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
245 # This would otherwise override the CAP_NET_BIND_SERVICE capability.
246 PrivateUsers = lib.mkForce false;
247 # Port needs to be exposed to the host network
248 PrivateNetwork = lib.mkForce false;
249 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
250 TemporaryFileSystem = "/:ro";
251 }
252 ];
253 environment.RUST_LOG = "info";
254 };
255
256 systemd.services.kanidm-unixd = lib.mkIf cfg.enablePam {
257 description = "Kanidm PAM daemon";
258 wantedBy = [ "multi-user.target" ];
259 after = [ "network.target" ];
260 restartTriggers = [ unixConfigFile clientConfigFile ];
261 serviceConfig = lib.mkMerge [
262 defaultServiceConfig
263 {
264 CacheDirectory = "kanidm-unixd";
265 CacheDirectoryMode = "0700";
266 RuntimeDirectory = "kanidm-unixd";
267 ExecStart = "${pkgs.kanidm}/bin/kanidm_unixd";
268 User = "kanidm-unixd";
269 Group = "kanidm-unixd";
270
271 BindReadOnlyPaths = [
272 "-/etc/kanidm"
273 "-/etc/static/kanidm"
274 "-/etc/ssl"
275 "-/etc/static/ssl"
276 ];
277 BindPaths = [
278 # To create the socket
279 "/run/kanidm-unixd:/var/run/kanidm-unixd"
280 ];
281 # Needs to connect to kanidmd
282 PrivateNetwork = lib.mkForce false;
283 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
284 TemporaryFileSystem = "/:ro";
285 }
286 ];
287 environment.RUST_LOG = "info";
288 };
289
290 systemd.services.kanidm-unixd-tasks = lib.mkIf cfg.enablePam {
291 description = "Kanidm PAM home management daemon";
292 wantedBy = [ "multi-user.target" ];
293 after = [ "network.target" "kanidm-unixd.service" ];
294 partOf = [ "kanidm-unixd.service" ];
295 restartTriggers = [ unixConfigFile clientConfigFile ];
296 serviceConfig = {
297 ExecStart = "${pkgs.kanidm}/bin/kanidm_unixd_tasks";
298
299 BindReadOnlyPaths = [
300 "/nix/store"
301 "-/etc/resolv.conf"
302 "-/etc/nsswitch.conf"
303 "-/etc/hosts"
304 "-/etc/localtime"
305 "-/etc/kanidm"
306 "-/etc/static/kanidm"
307 ];
308 BindPaths = [
309 # To manage home directories
310 "/home"
311 # To connect to kanidm-unixd
312 "/run/kanidm-unixd:/var/run/kanidm-unixd"
313 ];
314 # CAP_DAC_OVERRIDE is needed to ignore ownership of unixd socket
315 CapabilityBoundingSet = [ "CAP_CHOWN" "CAP_FOWNER" "CAP_DAC_OVERRIDE" "CAP_DAC_READ_SEARCH" ];
316 IPAddressDeny = "any";
317 # Need access to users
318 PrivateUsers = false;
319 # Need access to home directories
320 ProtectHome = false;
321 RestrictAddressFamilies = [ "AF_UNIX" ];
322 TemporaryFileSystem = "/:ro";
323 };
324 environment.RUST_LOG = "info";
325 };
326
327 # These paths are hardcoded
328 environment.etc = lib.mkMerge [
329 (lib.mkIf options.services.kanidm.clientSettings.isDefined {
330 "kanidm/config".source = clientConfigFile;
331 })
332 (lib.mkIf cfg.enablePam {
333 "kanidm/unixd".source = unixConfigFile;
334 })
335 ];
336
337 system.nssModules = lib.mkIf cfg.enablePam [ pkgs.kanidm ];
338
339 system.nssDatabases.group = lib.optional cfg.enablePam "kanidm";
340 system.nssDatabases.passwd = lib.optional cfg.enablePam "kanidm";
341
342 users.groups = lib.mkMerge [
343 (lib.mkIf cfg.enableServer {
344 kanidm = { };
345 })
346 (lib.mkIf cfg.enablePam {
347 kanidm-unixd = { };
348 })
349 ];
350 users.users = lib.mkMerge [
351 (lib.mkIf cfg.enableServer {
352 kanidm = {
353 description = "Kanidm server";
354 isSystemUser = true;
355 group = "kanidm";
356 packages = with pkgs; [ kanidm ];
357 };
358 })
359 (lib.mkIf cfg.enablePam {
360 kanidm-unixd = {
361 description = "Kanidm PAM daemon";
362 isSystemUser = true;
363 group = "kanidm-unixd";
364 };
365 })
366 ];
367 };
368
369 meta.maintainers = with lib.maintainers; [ erictapen Flakebi ];
370 meta.buildDocsInSandbox = false;
371}