1 { config, lib, options, pkgs, ... }:
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);
11 defaultServiceConfig = {
19 CapabilityBoundingSet = "";
20 # ProtectClock= adds DeviceAllow=char-rtc r
22 # Implies ProtectSystem=strict, which re-mounts all paths
24 LockPersonality = true;
25 MemoryDenyWriteExecute = true;
26 NoNewPrivileges = true;
27 PrivateDevices = true;
29 PrivateNetwork = true;
35 ProtectHostname = true;
36 # Would re-mount paths ignored by temporary root
37 #ProtectSystem = "strict";
38 ProtectControlGroups = true;
39 ProtectKernelLogs = true;
40 ProtectKernelModules = true;
41 ProtectKernelTunables = true;
42 ProtectProc = "invisible";
43 RestrictAddressFamilies = [ ];
44 RestrictNamespaces = true;
45 RestrictRealtime = true;
46 RestrictSUIDSGID = true;
47 SystemCallArchitectures = "native";
48 SystemCallFilter = [ "@system-service" "~@privileged @resources @setuid @keyring" ];
49 # Does not work well with the temporary root
55 options.services.kanidm = {
56 enableClient = lib.mkEnableOption (lib.mdDoc "the Kanidm client");
57 enableServer = lib.mkEnableOption (lib.mdDoc "the Kanidm server");
58 enablePam = lib.mkEnableOption (lib.mdDoc "the Kanidm PAM and NSS integration.");
60 serverSettings = lib.mkOption {
61 type = lib.types.submodule {
62 freeformType = settingsFormat.type;
65 bindaddress = lib.mkOption {
66 description = lib.mdDoc "Address/port combination the webserver binds to.";
67 example = "[::1]:8443";
70 # Should be optional but toml does not accept null
71 ldapbindaddress = lib.mkOption {
72 description = lib.mdDoc ''
73 Address and port the LDAP server is bound to. Setting this to `null` disables the LDAP interface.
75 example = "[::1]:636";
77 type = lib.types.nullOr lib.types.str;
79 origin = lib.mkOption {
80 description = lib.mdDoc "The origin of your Kanidm instance. Must have https as protocol.";
81 example = "https://idm.example.org";
82 type = lib.types.strMatching "^https://.*";
84 domain = lib.mkOption {
85 description = lib.mdDoc ''
86 The `domain` that Kanidm manages. Must be below or equal to the domain
87 specified in `serverSettings.origin`.
88 This can be left at `null`, only if your instance has the role `ReadOnlyReplica`.
89 While it is possible to change the domain later on, it requires extra steps!
90 Please consider the warnings and execute the steps described
91 [in the documentation](https://kanidm.github.io/kanidm/stable/administrivia.html#rename-the-domain).
93 example = "example.org";
95 type = lib.types.nullOr lib.types.str;
97 db_path = lib.mkOption {
98 description = lib.mdDoc "Path to Kanidm database.";
99 default = "/var/lib/kanidm/kanidm.db";
101 type = lib.types.path;
103 log_level = lib.mkOption {
104 description = lib.mdDoc "Log level of the server.";
106 type = lib.types.enum [ "default" "verbose" "perfbasic" "perffull" ];
108 role = lib.mkOption {
109 description = lib.mdDoc "The role of this server. This affects the replication relationship and thereby available features.";
110 default = "WriteReplica";
111 type = lib.types.enum [ "WriteReplica" "WriteReplicaNoUI" "ReadOnlyReplica" ];
116 description = lib.mdDoc ''
117 Settings for Kanidm, see
118 [the documentation](https://github.com/kanidm/kanidm/blob/master/kanidm_book/src/server_configuration.md)
119 and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/server.toml)
124 clientSettings = lib.mkOption {
125 type = lib.types.submodule {
126 freeformType = settingsFormat.type;
128 options.uri = lib.mkOption {
129 description = lib.mdDoc "Address of the Kanidm server.";
130 example = "http://127.0.0.1:8080";
131 type = lib.types.str;
134 description = lib.mdDoc ''
135 Configure Kanidm clients, needed for the PAM daemon. See
136 [the documentation](https://github.com/kanidm/kanidm/blob/master/kanidm_book/src/client_tools.md#kanidm-configuration)
137 and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/config)
142 unixSettings = lib.mkOption {
143 type = lib.types.submodule {
144 freeformType = settingsFormat.type;
146 options.pam_allowed_login_groups = lib.mkOption {
147 description = lib.mdDoc "Kanidm groups that are allowed to login using PAM.";
148 example = "my_pam_group";
149 type = lib.types.listOf lib.types.str;
152 description = lib.mdDoc ''
153 Configure Kanidm unix daemon.
154 See [the documentation](https://github.com/kanidm/kanidm/blob/master/kanidm_book/src/pam_and_nsswitch.md#the-unix-daemon)
155 and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/unixd)
161 config = lib.mkIf (cfg.enableClient || cfg.enableServer || cfg.enablePam) {
165 assertion = !cfg.enableServer || ((cfg.serverSettings.tls_chain or null) == null) || (!lib.isStorePath cfg.serverSettings.tls_chain);
167 <option>services.kanidm.serverSettings.tls_chain</option> points to
168 a file in the Nix store. You should use a quoted absolute path to
173 assertion = !cfg.enableServer || ((cfg.serverSettings.tls_key or null) == null) || (!lib.isStorePath cfg.serverSettings.tls_key);
175 <option>services.kanidm.serverSettings.tls_key</option> points to
176 a file in the Nix store. You should use a quoted absolute path to
181 assertion = !cfg.enableClient || options.services.kanidm.clientSettings.isDefined;
183 <option>services.kanidm.clientSettings</option> needs to be configured
184 if the client is enabled.
188 assertion = !cfg.enablePam || options.services.kanidm.clientSettings.isDefined;
190 <option>services.kanidm.clientSettings</option> needs to be configured
191 for the PAM daemon to connect to the Kanidm server.
195 assertion = !cfg.enableServer || (cfg.serverSettings.domain == null
196 -> cfg.serverSettings.role == "WriteReplica" || cfg.serverSettings.role == "WriteReplicaNoUI");
198 <option>services.kanidm.serverSettings.domain</option> can only be set if this instance
199 is not a ReadOnlyReplica. Otherwise the db would inherit it from
200 the instance it follows.
205 environment.systemPackages = lib.mkIf cfg.enableClient [ pkgs.kanidm ];
207 systemd.services.kanidm = lib.mkIf cfg.enableServer {
208 description = "kanidm identity management daemon";
209 wantedBy = [ "multi-user.target" ];
210 after = [ "network.target" ];
211 serviceConfig = defaultServiceConfig // {
212 StateDirectory = "kanidm";
213 StateDirectoryMode = "0700";
214 ExecStart = "${pkgs.kanidm}/bin/kanidmd server -c ${serverConfigFile}";
218 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
219 CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
220 # This would otherwise override the CAP_NET_BIND_SERVICE capability.
221 PrivateUsers = false;
222 # Port needs to be exposed to the host network
223 PrivateNetwork = false;
224 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
225 TemporaryFileSystem = "/:ro";
227 environment.RUST_LOG = "info";
230 systemd.services.kanidm-unixd = lib.mkIf cfg.enablePam {
231 description = "Kanidm PAM daemon";
232 wantedBy = [ "multi-user.target" ];
233 after = [ "network.target" ];
234 restartTriggers = [ unixConfigFile clientConfigFile ];
235 serviceConfig = defaultServiceConfig // {
236 CacheDirectory = "kanidm-unixd";
237 CacheDirectoryMode = "0700";
238 RuntimeDirectory = "kanidm-unixd";
239 ExecStart = "${pkgs.kanidm}/bin/kanidm_unixd";
240 User = "kanidm-unixd";
241 Group = "kanidm-unixd";
243 BindReadOnlyPaths = [
246 "-/etc/nsswitch.conf"
250 "-/etc/static/kanidm"
255 # To create the socket
256 "/run/kanidm-unixd:/var/run/kanidm-unixd"
258 # Needs to connect to kanidmd
259 PrivateNetwork = false;
260 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
261 TemporaryFileSystem = "/:ro";
263 environment.RUST_LOG = "info";
266 systemd.services.kanidm-unixd-tasks = lib.mkIf cfg.enablePam {
267 description = "Kanidm PAM home management daemon";
268 wantedBy = [ "multi-user.target" ];
269 after = [ "network.target" "kanidm-unixd.service" ];
270 partOf = [ "kanidm-unixd.service" ];
271 restartTriggers = [ unixConfigFile clientConfigFile ];
273 ExecStart = "${pkgs.kanidm}/bin/kanidm_unixd_tasks";
275 BindReadOnlyPaths = [
278 "-/etc/nsswitch.conf"
282 "-/etc/static/kanidm"
285 # To manage home directories
287 # To connect to kanidm-unixd
288 "/run/kanidm-unixd:/var/run/kanidm-unixd"
290 # CAP_DAC_OVERRIDE is needed to ignore ownership of unixd socket
291 CapabilityBoundingSet = [ "CAP_CHOWN" "CAP_FOWNER" "CAP_DAC_OVERRIDE" "CAP_DAC_READ_SEARCH" ];
292 IPAddressDeny = "any";
293 # Need access to users
294 PrivateUsers = false;
295 # Need access to home directories
297 RestrictAddressFamilies = [ "AF_UNIX" ];
298 TemporaryFileSystem = "/:ro";
300 environment.RUST_LOG = "info";
303 # These paths are hardcoded
304 environment.etc = lib.mkMerge [
305 (lib.mkIf options.services.kanidm.clientSettings.isDefined {
306 "kanidm/config".source = clientConfigFile;
308 (lib.mkIf cfg.enablePam {
309 "kanidm/unixd".source = unixConfigFile;
313 system.nssModules = lib.mkIf cfg.enablePam [ pkgs.kanidm ];
315 system.nssDatabases.group = lib.optional cfg.enablePam "kanidm";
316 system.nssDatabases.passwd = lib.optional cfg.enablePam "kanidm";
318 users.groups = lib.mkMerge [
319 (lib.mkIf cfg.enableServer {
322 (lib.mkIf cfg.enablePam {
326 users.users = lib.mkMerge [
327 (lib.mkIf cfg.enableServer {
329 description = "Kanidm server";
332 packages = with pkgs; [ kanidm ];
335 (lib.mkIf cfg.enablePam {
337 description = "Kanidm PAM daemon";
339 group = "kanidm-unixd";
345 meta.maintainers = with lib.maintainers; [ erictapen Flakebi ];
346 meta.buildDocsInSandbox = false;