at 24.11-pre 16 kB view raw
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}