at 25.11-pre 39 kB view raw
1{ 2 config, 3 lib, 4 options, 5 pkgs, 6 ... 7}: 8let 9 inherit (lib) 10 any 11 attrNames 12 attrValues 13 concatLines 14 concatLists 15 converge 16 filter 17 filterAttrs 18 filterAttrsRecursive 19 flip 20 foldl' 21 getExe 22 hasInfix 23 hasPrefix 24 isStorePath 25 last 26 mapAttrsToList 27 mkEnableOption 28 mkForce 29 mkIf 30 mkMerge 31 mkOption 32 mkPackageOption 33 optional 34 optionals 35 optionalString 36 splitString 37 subtractLists 38 types 39 unique 40 ; 41 42 cfg = config.services.kanidm; 43 settingsFormat = pkgs.formats.toml { }; 44 # Remove null values, so we can document optional values that don't end up in the generated TOML file. 45 filterConfig = converge (filterAttrsRecursive (_: v: v != null)); 46 serverConfigFile = settingsFormat.generate "server.toml" (filterConfig cfg.serverSettings); 47 clientConfigFile = settingsFormat.generate "kanidm-config.toml" (filterConfig cfg.clientSettings); 48 unixConfigFile = settingsFormat.generate "kanidm-unixd.toml" (filterConfig cfg.unixSettings); 49 provisionSecretFiles = filter (x: x != null) ( 50 [ 51 cfg.provision.idmAdminPasswordFile 52 cfg.provision.adminPasswordFile 53 ] 54 ++ optional (cfg.provision.extraJsonFile != null) cfg.provision.extraJsonFile 55 ++ mapAttrsToList (_: x: x.basicSecretFile) cfg.provision.systems.oauth2 56 ); 57 secretDirectories = unique ( 58 map builtins.dirOf ( 59 [ 60 cfg.serverSettings.tls_chain 61 cfg.serverSettings.tls_key 62 ] 63 ++ optionals cfg.provision.enable provisionSecretFiles 64 ) 65 ); 66 67 # Merge bind mount paths and remove paths where a prefix is already mounted. 68 # This makes sure that if e.g. the tls_chain is in the nix store and /nix/store is already in the mount 69 # paths, no new bind mount is added. Adding subpaths caused problems on ofborg. 70 hasPrefixInList = 71 list: newPath: any (path: hasPrefix (builtins.toString path) (builtins.toString newPath)) list; 72 mergePaths = foldl' ( 73 merged: newPath: 74 let 75 # If the new path is a prefix to some existing path, we need to filter it out 76 filteredPaths = filter (p: !hasPrefix (builtins.toString newPath) (builtins.toString p)) merged; 77 # If a prefix of the new path is already in the list, do not add it 78 filteredNew = optional (!hasPrefixInList filteredPaths newPath) newPath; 79 in 80 filteredPaths ++ filteredNew 81 ) [ ]; 82 83 defaultServiceConfig = { 84 # Setting the type to notify enables additional healthchecks, ensuring units 85 # after and requiring kanidm-* wait for it to complete startup 86 Type = "notify"; 87 BindReadOnlyPaths = [ 88 "/nix/store" 89 # For healthcheck notifications 90 "/run/systemd/notify" 91 "-/etc/resolv.conf" 92 "-/etc/nsswitch.conf" 93 "-/etc/hosts" 94 "-/etc/localtime" 95 ]; 96 CapabilityBoundingSet = [ ]; 97 # ProtectClock= adds DeviceAllow=char-rtc r 98 DeviceAllow = ""; 99 # Implies ProtectSystem=strict, which re-mounts all paths 100 # DynamicUser = true; 101 LockPersonality = true; 102 MemoryDenyWriteExecute = true; 103 NoNewPrivileges = true; 104 PrivateDevices = true; 105 PrivateMounts = true; 106 PrivateNetwork = true; 107 PrivateTmp = true; 108 PrivateUsers = true; 109 ProcSubset = "pid"; 110 ProtectClock = true; 111 ProtectHome = true; 112 ProtectHostname = true; 113 # Would re-mount paths ignored by temporary root 114 #ProtectSystem = "strict"; 115 ProtectControlGroups = true; 116 ProtectKernelLogs = true; 117 ProtectKernelModules = true; 118 ProtectKernelTunables = true; 119 ProtectProc = "invisible"; 120 RestrictAddressFamilies = [ ]; 121 RestrictNamespaces = true; 122 RestrictRealtime = true; 123 RestrictSUIDSGID = true; 124 SystemCallArchitectures = "native"; 125 SystemCallFilter = [ 126 "@system-service" 127 "~@privileged @resources @setuid @keyring" 128 ]; 129 # Does not work well with the temporary root 130 #UMask = "0066"; 131 }; 132 133 mkPresentOption = 134 what: 135 mkOption { 136 description = "Whether to ensure that this ${what} is present or absent."; 137 type = types.bool; 138 default = true; 139 }; 140 141 filterPresent = filterAttrs (_: v: v.present); 142 143 provisionStateJson = pkgs.writeText "provision-state.json" ( 144 builtins.toJSON { inherit (cfg.provision) groups persons systems; } 145 ); 146 147 # Only recover the admin account if a password should explicitly be provisioned 148 # for the account. Otherwise it is not needed for provisioning. 149 maybeRecoverAdmin = optionalString (cfg.provision.adminPasswordFile != null) '' 150 KANIDM_ADMIN_PASSWORD=$(< ${cfg.provision.adminPasswordFile}) 151 # We always reset the admin account password if a desired password was specified. 152 if ! KANIDM_RECOVER_ACCOUNT_PASSWORD=$KANIDM_ADMIN_PASSWORD ${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} admin --from-environment >/dev/null; then 153 echo "Failed to recover admin account" >&2 154 exit 1 155 fi 156 ''; 157 158 # Recover the idm_admin account. If a password should explicitly be provisioned 159 # for the account we set it, otherwise we generate a new one because it is required 160 # for provisioning. 161 recoverIdmAdmin = 162 if cfg.provision.idmAdminPasswordFile != null then 163 '' 164 KANIDM_IDM_ADMIN_PASSWORD=$(< ${cfg.provision.idmAdminPasswordFile}) 165 # We always reset the idm_admin account password if a desired password was specified. 166 if ! KANIDM_RECOVER_ACCOUNT_PASSWORD=$KANIDM_IDM_ADMIN_PASSWORD ${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} idm_admin --from-environment >/dev/null; then 167 echo "Failed to recover idm_admin account" >&2 168 exit 1 169 fi 170 '' 171 else 172 '' 173 # Recover idm_admin account 174 if ! recover_out=$(${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} idm_admin -o json); then 175 echo "$recover_out" >&2 176 echo "kanidm provision: Failed to recover admin account" >&2 177 exit 1 178 fi 179 if ! KANIDM_IDM_ADMIN_PASSWORD=$(grep '{"password' <<< "$recover_out" | ${getExe pkgs.jq} -r .password); then 180 echo "$recover_out" >&2 181 echo "kanidm provision: Failed to parse password for idm_admin account" >&2 182 exit 1 183 fi 184 ''; 185 186 finalJson = 187 if cfg.provision.extraJsonFile != null then 188 "<(${lib.getExe pkgs.jq} -s '.[0] * .[1]' ${provisionStateJson} ${cfg.provision.extraJsonFile})" 189 else 190 provisionStateJson; 191 192 postStartScript = pkgs.writeShellScript "post-start" '' 193 set -euo pipefail 194 195 # Wait for the kanidm server to come online 196 count=0 197 while ! ${getExe pkgs.curl} -L --silent --max-time 1 --connect-timeout 1 --fail \ 198 ${optionalString cfg.provision.acceptInvalidCerts "--insecure"} \ 199 ${cfg.provision.instanceUrl} >/dev/null 200 do 201 sleep 1 202 if [[ "$count" -eq 30 ]]; then 203 echo "Tried for at least 30 seconds, giving up..." 204 exit 1 205 fi 206 count=$((count++)) 207 done 208 209 ${recoverIdmAdmin} 210 ${maybeRecoverAdmin} 211 212 KANIDM_PROVISION_IDM_ADMIN_TOKEN=$KANIDM_IDM_ADMIN_PASSWORD \ 213 ${getExe pkgs.kanidm-provision} \ 214 ${optionalString (!cfg.provision.autoRemove) "--no-auto-remove"} \ 215 ${optionalString cfg.provision.acceptInvalidCerts "--accept-invalid-certs"} \ 216 --url "${cfg.provision.instanceUrl}" \ 217 --state ${finalJson} 218 ''; 219 220 serverPort = 221 let 222 address = cfg.serverSettings.bindaddress; 223 in 224 # ipv6: 225 if hasInfix "]:" address then 226 last (splitString "]:" address) 227 else 228 # ipv4: 229 if hasInfix "." address then 230 last (splitString ":" address) 231 # default is 8443 232 else 233 throw "Address not parseable as IPv4 nor IPv6."; 234in 235{ 236 options.services.kanidm = { 237 enableClient = mkEnableOption "the Kanidm client"; 238 enableServer = mkEnableOption "the Kanidm server"; 239 enablePam = mkEnableOption "the Kanidm PAM and NSS integration"; 240 241 package = mkPackageOption pkgs "kanidm" { 242 example = "kanidm_1_4"; 243 extraDescription = "If not set will receive a specific version based on stateVersion. Set to `pkgs.kanidm` to always receive the latest version, with the understanding that this could introduce breaking changes."; 244 }; 245 246 serverSettings = mkOption { 247 type = types.submodule { 248 freeformType = settingsFormat.type; 249 250 options = { 251 bindaddress = mkOption { 252 description = "Address/port combination the webserver binds to."; 253 example = "[::1]:8443"; 254 default = "127.0.0.1:8443"; 255 type = types.str; 256 }; 257 # Should be optional but toml does not accept null 258 ldapbindaddress = mkOption { 259 description = '' 260 Address and port the LDAP server is bound to. Setting this to `null` disables the LDAP interface. 261 ''; 262 example = "[::1]:636"; 263 default = null; 264 type = types.nullOr types.str; 265 }; 266 origin = mkOption { 267 description = "The origin of your Kanidm instance. Must have https as protocol."; 268 example = "https://idm.example.org"; 269 type = types.strMatching "^https://.*"; 270 }; 271 domain = mkOption { 272 description = '' 273 The `domain` that Kanidm manages. Must be below or equal to the domain 274 specified in `serverSettings.origin`. 275 This can be left at `null`, only if your instance has the role `ReadOnlyReplica`. 276 While it is possible to change the domain later on, it requires extra steps! 277 Please consider the warnings and execute the steps described 278 [in the documentation](https://kanidm.github.io/kanidm/stable/administrivia.html#rename-the-domain). 279 ''; 280 example = "example.org"; 281 default = null; 282 type = types.nullOr types.str; 283 }; 284 db_path = mkOption { 285 description = "Path to Kanidm database."; 286 default = "/var/lib/kanidm/kanidm.db"; 287 readOnly = true; 288 type = types.path; 289 }; 290 tls_chain = mkOption { 291 description = "TLS chain in pem format."; 292 type = types.path; 293 }; 294 tls_key = mkOption { 295 description = "TLS key in pem format."; 296 type = types.path; 297 }; 298 log_level = mkOption { 299 description = "Log level of the server."; 300 default = "info"; 301 type = types.enum [ 302 "info" 303 "debug" 304 "trace" 305 ]; 306 }; 307 role = mkOption { 308 description = "The role of this server. This affects the replication relationship and thereby available features."; 309 default = "WriteReplica"; 310 type = types.enum [ 311 "WriteReplica" 312 "WriteReplicaNoUI" 313 "ReadOnlyReplica" 314 ]; 315 }; 316 online_backup = { 317 path = mkOption { 318 description = "Path to the output directory for backups."; 319 type = types.path; 320 default = "/var/lib/kanidm/backups"; 321 }; 322 schedule = mkOption { 323 description = "The schedule for backups in cron format."; 324 type = types.str; 325 default = "00 22 * * *"; 326 }; 327 versions = mkOption { 328 description = '' 329 Number of backups to keep. 330 331 The default is set to `0`, in order to disable backups by default. 332 ''; 333 type = types.ints.unsigned; 334 default = 0; 335 example = 7; 336 }; 337 }; 338 }; 339 }; 340 default = { }; 341 description = '' 342 Settings for Kanidm, see 343 [the documentation](https://kanidm.github.io/kanidm/stable/server_configuration.html) 344 and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/server.toml) 345 for possible values. 346 ''; 347 }; 348 349 clientSettings = mkOption { 350 type = types.submodule { 351 freeformType = settingsFormat.type; 352 353 options.uri = mkOption { 354 description = "Address of the Kanidm server."; 355 example = "http://127.0.0.1:8080"; 356 type = types.str; 357 }; 358 }; 359 description = '' 360 Configure Kanidm clients, needed for the PAM daemon. See 361 [the documentation](https://kanidm.github.io/kanidm/stable/client_tools.html#kanidm-configuration) 362 and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/config) 363 for possible values. 364 ''; 365 }; 366 367 unixSettings = mkOption { 368 type = types.submodule { 369 freeformType = settingsFormat.type; 370 371 options = { 372 pam_allowed_login_groups = mkOption { 373 description = "Kanidm groups that are allowed to login using PAM."; 374 example = "my_pam_group"; 375 type = types.listOf types.str; 376 }; 377 hsm_pin_path = mkOption { 378 description = "Path to a HSM pin."; 379 default = "/var/cache/kanidm-unixd/hsm-pin"; 380 type = types.path; 381 }; 382 }; 383 }; 384 description = '' 385 Configure Kanidm unix daemon. 386 See [the documentation](https://kanidm.github.io/kanidm/stable/integrations/pam_and_nsswitch.html#the-unix-daemon) 387 and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/unixd) 388 for possible values. 389 ''; 390 }; 391 392 provision = { 393 enable = mkEnableOption "provisioning of groups, users and oauth2 resource servers"; 394 395 instanceUrl = mkOption { 396 description = "The instance url to which the provisioning tool should connect."; 397 default = "https://localhost:${serverPort}"; 398 defaultText = ''"https://localhost:<port from serverSettings.bindaddress>"''; 399 type = types.str; 400 }; 401 402 acceptInvalidCerts = mkOption { 403 description = '' 404 Whether to allow invalid certificates when provisioning the target instance. 405 By default this is only allowed when the instanceUrl is localhost. This is 406 dangerous when used with an external URL. 407 ''; 408 type = types.bool; 409 default = hasPrefix "https://localhost:" cfg.provision.instanceUrl; 410 defaultText = ''hasPrefix "https://localhost:" cfg.provision.instanceUrl''; 411 }; 412 413 adminPasswordFile = mkOption { 414 description = "Path to a file containing the admin password for kanidm. Do NOT use a file from the nix store here!"; 415 example = "/run/secrets/kanidm-admin-password"; 416 default = null; 417 type = types.nullOr types.path; 418 }; 419 420 idmAdminPasswordFile = mkOption { 421 description = '' 422 Path to a file containing the idm admin password for kanidm. Do NOT use a file from the nix store here! 423 If this is not given but provisioning is enabled, the idm_admin password will be reset on each restart. 424 ''; 425 example = "/run/secrets/kanidm-idm-admin-password"; 426 default = null; 427 type = types.nullOr types.path; 428 }; 429 430 autoRemove = mkOption { 431 description = '' 432 Determines whether deleting an entity in this provisioning config should automatically 433 cause them to be removed from kanidm, too. This works because the provisioning tool tracks 434 all entities it has ever created. If this is set to false, you need to explicitly specify 435 `present = false` to delete an entity. 436 ''; 437 type = types.bool; 438 default = true; 439 }; 440 441 extraJsonFile = mkOption { 442 description = '' 443 A JSON file for provisioning persons, groups & systems. 444 Options set in this file take precedence over values set using the other options. 445 In the case of duplicates, `jq` will remove all but the last one 446 when merging this file with the options. 447 The accepted JSON schema can be found at <https://github.com/oddlama/kanidm-provision#json-schema>. 448 Note: theoretically `jq` cannot merge nested types, but this does not pose an issue as kanidm-provision's JSON scheme does not use nested types. 449 ''; 450 type = types.nullOr types.path; 451 default = null; 452 }; 453 454 groups = mkOption { 455 description = "Provisioning of kanidm groups"; 456 default = { }; 457 type = types.attrsOf ( 458 types.submodule (groupSubmod: { 459 options = { 460 present = mkPresentOption "group"; 461 462 members = mkOption { 463 description = "List of kanidm entities (persons, groups, ...) which are part of this group."; 464 type = types.listOf types.str; 465 apply = unique; 466 default = [ ]; 467 }; 468 }; 469 config.members = concatLists ( 470 flip mapAttrsToList cfg.provision.persons ( 471 person: personCfg: 472 optional ( 473 personCfg.present && builtins.elem groupSubmod.config._module.args.name personCfg.groups 474 ) person 475 ) 476 ); 477 }) 478 ); 479 }; 480 481 persons = mkOption { 482 description = "Provisioning of kanidm persons"; 483 default = { }; 484 type = types.attrsOf ( 485 types.submodule { 486 options = { 487 present = mkPresentOption "person"; 488 489 displayName = mkOption { 490 description = "Display name"; 491 type = types.str; 492 example = "My User"; 493 }; 494 495 legalName = mkOption { 496 description = "Full legal name"; 497 type = types.nullOr types.str; 498 example = "Jane Doe"; 499 default = null; 500 }; 501 502 mailAddresses = mkOption { 503 description = "Mail addresses. First given address is considered the primary address."; 504 type = types.listOf types.str; 505 example = [ "jane.doe@example.com" ]; 506 default = [ ]; 507 }; 508 509 groups = mkOption { 510 description = "List of groups this person should belong to."; 511 type = types.listOf types.str; 512 apply = unique; 513 default = [ ]; 514 }; 515 }; 516 } 517 ); 518 }; 519 520 systems.oauth2 = mkOption { 521 description = "Provisioning of oauth2 resource servers"; 522 default = { }; 523 type = types.attrsOf ( 524 types.submodule { 525 options = { 526 present = mkPresentOption "oauth2 resource server"; 527 528 public = mkOption { 529 description = "Whether this is a public client (enforces PKCE, doesn't use a basic secret)"; 530 type = types.bool; 531 default = false; 532 }; 533 534 displayName = mkOption { 535 description = "Display name"; 536 type = types.str; 537 example = "Some Service"; 538 }; 539 540 originUrl = mkOption { 541 description = "The redirect URL of the service. These need to exactly match the OAuth2 redirect target"; 542 type = 543 let 544 originStrType = types.strMatching ".*://.*$"; 545 in 546 types.either originStrType (types.nonEmptyListOf originStrType); 547 example = "https://someservice.example.com/auth/login"; 548 }; 549 550 originLanding = mkOption { 551 description = "When redirecting from the Kanidm Apps Listing page, some linked applications may need to land on a specific page to trigger oauth2/oidc interactions."; 552 type = types.str; 553 example = "https://someservice.example.com/home"; 554 }; 555 556 basicSecretFile = mkOption { 557 description = '' 558 The basic secret to use for this service. If null, the random secret generated 559 by kanidm will not be touched. Do NOT use a path from the nix store here! 560 ''; 561 type = types.nullOr types.path; 562 example = "/run/secrets/some-oauth2-basic-secret"; 563 default = null; 564 }; 565 566 imageFile = mkOption { 567 description = '' 568 Application image to display in the WebUI. 569 Kanidm supports "image/jpeg", "image/png", "image/gif", "image/svg+xml", and "image/webp". 570 The image will be uploaded each time kanidm-provision is run. 571 ''; 572 type = types.nullOr types.path; 573 default = null; 574 }; 575 576 enableLocalhostRedirects = mkOption { 577 description = "Allow localhost redirects. Only for public clients."; 578 type = types.bool; 579 default = false; 580 }; 581 582 enableLegacyCrypto = mkOption { 583 description = "Enable legacy crypto on this client. Allows JWT signing algorthms like RS256."; 584 type = types.bool; 585 default = false; 586 }; 587 588 allowInsecureClientDisablePkce = mkOption { 589 description = '' 590 Disable PKCE on this oauth2 resource server to work around insecure clients 591 that may not support it. You should request the client to enable PKCE! 592 Only for non-public clients. 593 ''; 594 type = types.bool; 595 default = false; 596 }; 597 598 preferShortUsername = mkOption { 599 description = "Use 'name' instead of 'spn' in the preferred_username claim"; 600 type = types.bool; 601 default = false; 602 }; 603 604 scopeMaps = mkOption { 605 description = '' 606 Maps kanidm groups to returned oauth scopes. 607 See [Scope Relations](https://kanidm.github.io/kanidm/stable/integrations/oauth2.html#scope-relationships) for more information. 608 ''; 609 type = types.attrsOf (types.listOf types.str); 610 default = { }; 611 }; 612 613 supplementaryScopeMaps = mkOption { 614 description = '' 615 Maps kanidm groups to additionally returned oauth scopes. 616 See [Scope Relations](https://kanidm.github.io/kanidm/stable/integrations/oauth2.html#scope-relationships) for more information. 617 ''; 618 type = types.attrsOf (types.listOf types.str); 619 default = { }; 620 }; 621 622 removeOrphanedClaimMaps = mkOption { 623 description = "Whether claim maps not specified here but present in kanidm should be removed from kanidm."; 624 type = types.bool; 625 default = true; 626 }; 627 628 claimMaps = mkOption { 629 description = '' 630 Adds additional claims (and values) based on which kanidm groups an authenticating party belongs to. 631 See [Claim Maps](https://kanidm.github.io/kanidm/master/integrations/oauth2.html#custom-claim-maps) for more information. 632 ''; 633 default = { }; 634 type = types.attrsOf ( 635 types.submodule { 636 options = { 637 joinType = mkOption { 638 description = '' 639 Determines how multiple values are joined to create the claim value. 640 See [Claim Maps](https://kanidm.github.io/kanidm/master/integrations/oauth2.html#custom-claim-maps) for more information. 641 ''; 642 type = types.enum [ 643 "array" 644 "csv" 645 "ssv" 646 ]; 647 default = "array"; 648 }; 649 650 valuesByGroup = mkOption { 651 description = "Maps kanidm groups to values for the claim."; 652 default = { }; 653 type = types.attrsOf (types.listOf types.str); 654 }; 655 }; 656 } 657 ); 658 }; 659 }; 660 } 661 ); 662 }; 663 }; 664 }; 665 666 config = mkIf (cfg.enableClient || cfg.enableServer || cfg.enablePam) { 667 assertions = 668 let 669 entityList = 670 type: attrs: flip mapAttrsToList (filterPresent attrs) (name: _: { inherit type name; }); 671 entities = 672 entityList "group" cfg.provision.groups 673 ++ entityList "person" cfg.provision.persons 674 ++ entityList "oauth2" cfg.provision.systems.oauth2; 675 676 # Accumulate entities by name. Track corresponding entity types for later duplicate check. 677 entitiesByName = foldl' ( 678 acc: { type, name }: acc // { ${name} = (acc.${name} or [ ]) ++ [ type ]; } 679 ) { } entities; 680 681 assertGroupsKnown = 682 opt: groups: 683 let 684 knownGroups = attrNames (filterPresent cfg.provision.groups); 685 unknownGroups = subtractLists knownGroups groups; 686 in 687 { 688 assertion = (cfg.enableServer && cfg.provision.enable) -> unknownGroups == [ ]; 689 message = "${opt} refers to unknown groups: ${toString unknownGroups}"; 690 }; 691 692 assertEntitiesKnown = 693 opt: entities: 694 let 695 unknownEntities = subtractLists (attrNames entitiesByName) entities; 696 in 697 { 698 assertion = (cfg.enableServer && cfg.provision.enable) -> unknownEntities == [ ]; 699 message = "${opt} refers to unknown entities: ${toString unknownEntities}"; 700 }; 701 in 702 [ 703 { 704 assertion = 705 !cfg.enableServer 706 || ((cfg.serverSettings.tls_chain or null) == null) 707 || (!isStorePath cfg.serverSettings.tls_chain); 708 message = '' 709 <option>services.kanidm.serverSettings.tls_chain</option> points to 710 a file in the Nix store. You should use a quoted absolute path to 711 prevent this. 712 ''; 713 } 714 { 715 assertion = 716 !cfg.enableServer 717 || ((cfg.serverSettings.tls_key or null) == null) 718 || (!isStorePath cfg.serverSettings.tls_key); 719 message = '' 720 <option>services.kanidm.serverSettings.tls_key</option> points to 721 a file in the Nix store. You should use a quoted absolute path to 722 prevent this. 723 ''; 724 } 725 { 726 assertion = !cfg.enableClient || options.services.kanidm.clientSettings.isDefined; 727 message = '' 728 <option>services.kanidm.clientSettings</option> needs to be configured 729 if the client is enabled. 730 ''; 731 } 732 { 733 assertion = !cfg.enablePam || options.services.kanidm.clientSettings.isDefined; 734 message = '' 735 <option>services.kanidm.clientSettings</option> needs to be configured 736 for the PAM daemon to connect to the Kanidm server. 737 ''; 738 } 739 { 740 assertion = 741 !cfg.enableServer 742 || ( 743 cfg.serverSettings.domain == null 744 -> cfg.serverSettings.role == "WriteReplica" || cfg.serverSettings.role == "WriteReplicaNoUI" 745 ); 746 message = '' 747 <option>services.kanidm.serverSettings.domain</option> can only be set if this instance 748 is not a ReadOnlyReplica. Otherwise the db would inherit it from 749 the instance it follows. 750 ''; 751 } 752 { 753 assertion = cfg.provision.enable -> cfg.enableServer; 754 message = "<option>services.kanidm.provision</option> requires <option>services.kanidm.enableServer</option> to be true"; 755 } 756 # If any secret is provisioned, the kanidm package must have some required patches applied to it 757 { 758 assertion = 759 ( 760 cfg.provision.enable 761 && ( 762 cfg.provision.adminPasswordFile != null 763 || cfg.provision.idmAdminPasswordFile != null 764 || any (x: x.basicSecretFile != null) (attrValues (filterPresent cfg.provision.systems.oauth2)) 765 ) 766 ) 767 -> cfg.package.enableSecretProvisioning; 768 message = '' 769 Specifying an admin account password or oauth2 basicSecretFile requires kanidm to be built with the secret provisioning patches. 770 You may want to set `services.kanidm.package = pkgs.kanidmWithSecretProvisioning;`. 771 ''; 772 } 773 # Entity names must be globally unique: 774 ( 775 let 776 # Filter all names that occurred in more than one entity type. 777 duplicateNames = filterAttrs (_: v: builtins.length v > 1) entitiesByName; 778 in 779 { 780 assertion = cfg.provision.enable -> duplicateNames == { }; 781 message = '' 782 services.kanidm.provision requires all entity names (group, person, oauth2, ...) to be unique! 783 ${concatLines ( 784 mapAttrsToList (name: xs: " - '${name}' used as: ${toString xs}") duplicateNames 785 )}''; 786 } 787 ) 788 ] 789 ++ (optionals (cfg.provision.extraJsonFile == null) ( 790 flip mapAttrsToList (filterPresent cfg.provision.persons) ( 791 person: personCfg: 792 assertGroupsKnown "services.kanidm.provision.persons.${person}.groups" personCfg.groups 793 ) 794 )) 795 ++ (optionals (cfg.provision.extraJsonFile == null) ( 796 flip mapAttrsToList (filterPresent cfg.provision.groups) ( 797 group: groupCfg: 798 assertEntitiesKnown "services.kanidm.provision.groups.${group}.members" groupCfg.members 799 ) 800 )) 801 ++ concatLists ( 802 flip mapAttrsToList (filterPresent cfg.provision.systems.oauth2) ( 803 oauth2: oauth2Cfg: 804 (optional (cfg.provision.extraJsonFile == null) ( 805 assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.scopeMaps" ( 806 attrNames oauth2Cfg.scopeMaps 807 ) 808 )) 809 ++ (optional (cfg.provision.extraJsonFile == null) ( 810 assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.supplementaryScopeMaps" ( 811 attrNames oauth2Cfg.supplementaryScopeMaps 812 ) 813 )) 814 ++ concatLists ( 815 flip mapAttrsToList oauth2Cfg.claimMaps ( 816 claim: claimCfg: [ 817 (mkIf (cfg.provision.extraJsonFile == null) ( 818 assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim}.valuesByGroup" ( 819 attrNames claimCfg.valuesByGroup 820 ) 821 )) 822 # At least one group must map to a value in each claim map 823 (mkIf (cfg.provision.extraJsonFile == null) { 824 assertion = 825 (cfg.provision.enable && cfg.enableServer) 826 -> any (xs: xs != [ ]) (attrValues claimCfg.valuesByGroup); 827 message = "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim} does not specify any values for any group"; 828 }) 829 # Public clients cannot define a basic secret 830 { 831 assertion = 832 (cfg.provision.enable && cfg.enableServer && oauth2Cfg.public) -> oauth2Cfg.basicSecretFile == null; 833 message = "services.kanidm.provision.systems.oauth2.${oauth2} is a public client and thus cannot specify a basic secret"; 834 } 835 # Public clients cannot disable PKCE 836 { 837 assertion = 838 (cfg.provision.enable && cfg.enableServer && oauth2Cfg.public) 839 -> !oauth2Cfg.allowInsecureClientDisablePkce; 840 message = "services.kanidm.provision.systems.oauth2.${oauth2} is a public client and thus cannot disable PKCE"; 841 } 842 # Non-public clients cannot enable localhost redirects 843 { 844 assertion = 845 (cfg.provision.enable && cfg.enableServer && !oauth2Cfg.public) 846 -> !oauth2Cfg.enableLocalhostRedirects; 847 message = "services.kanidm.provision.systems.oauth2.${oauth2} is a non-public client and thus cannot enable localhost redirects"; 848 } 849 ] 850 ) 851 ) 852 ) 853 ); 854 855 services.kanidm.package = 856 let 857 pkg = 858 if lib.versionAtLeast config.system.stateVersion "24.11" then 859 pkgs.kanidm_1_4 860 else 861 lib.warn "No default kanidm package found for stateVersion = '${config.system.stateVersion}'. Using unpinned version. Consider setting `services.kanidm.package = pkgs.kanidm_1_x` to avoid upgrades introducing breaking changes." pkgs.kanidm; 862 in 863 lib.mkDefault pkg; 864 865 environment.systemPackages = mkIf cfg.enableClient [ cfg.package ]; 866 867 systemd.tmpfiles.settings."10-kanidm" = { 868 ${cfg.serverSettings.online_backup.path}.d = { 869 mode = "0700"; 870 user = "kanidm"; 871 group = "kanidm"; 872 }; 873 }; 874 875 systemd.services.kanidm = mkIf cfg.enableServer { 876 description = "kanidm identity management daemon"; 877 wantedBy = [ "multi-user.target" ]; 878 after = [ "network.target" ]; 879 serviceConfig = mkMerge [ 880 # Merge paths and ignore existing prefixes needs to sidestep mkMerge 881 ( 882 defaultServiceConfig 883 // { 884 BindReadOnlyPaths = mergePaths (defaultServiceConfig.BindReadOnlyPaths ++ secretDirectories); 885 } 886 ) 887 { 888 StateDirectory = "kanidm"; 889 StateDirectoryMode = "0700"; 890 RuntimeDirectory = "kanidmd"; 891 ExecStart = "${cfg.package}/bin/kanidmd server -c ${serverConfigFile}"; 892 ExecStartPost = mkIf cfg.provision.enable postStartScript; 893 User = "kanidm"; 894 Group = "kanidm"; 895 896 BindPaths = 897 [ 898 # To create the socket 899 "/run/kanidmd:/run/kanidmd" 900 # To store backups 901 cfg.serverSettings.online_backup.path 902 ] 903 ++ optional ( 904 cfg.enablePam && cfg.unixSettings ? home_mount_prefix 905 ) cfg.unixSettings.home_mount_prefix; 906 907 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; 908 CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; 909 # This would otherwise override the CAP_NET_BIND_SERVICE capability. 910 PrivateUsers = mkForce false; 911 # Port needs to be exposed to the host network 912 PrivateNetwork = mkForce false; 913 RestrictAddressFamilies = [ 914 "AF_INET" 915 "AF_INET6" 916 "AF_UNIX" 917 ]; 918 TemporaryFileSystem = "/:ro"; 919 } 920 ]; 921 }; 922 923 systemd.services.kanidm-unixd = mkIf cfg.enablePam { 924 description = "Kanidm PAM daemon"; 925 wantedBy = [ "multi-user.target" ]; 926 after = [ "network.target" ]; 927 restartTriggers = [ 928 unixConfigFile 929 clientConfigFile 930 ]; 931 serviceConfig = mkMerge [ 932 defaultServiceConfig 933 { 934 CacheDirectory = "kanidm-unixd"; 935 CacheDirectoryMode = "0700"; 936 RuntimeDirectory = "kanidm-unixd"; 937 ExecStart = "${cfg.package}/bin/kanidm_unixd"; 938 User = "kanidm-unixd"; 939 Group = "kanidm-unixd"; 940 941 BindReadOnlyPaths = [ 942 "-/etc/kanidm" 943 "-/etc/static/kanidm" 944 "-/etc/ssl" 945 "-/etc/static/ssl" 946 "-/etc/passwd" 947 "-/etc/group" 948 ]; 949 BindPaths = [ 950 # To create the socket 951 "/run/kanidm-unixd:/var/run/kanidm-unixd" 952 ]; 953 # Needs to connect to kanidmd 954 PrivateNetwork = mkForce false; 955 RestrictAddressFamilies = [ 956 "AF_INET" 957 "AF_INET6" 958 "AF_UNIX" 959 ]; 960 TemporaryFileSystem = "/:ro"; 961 } 962 ]; 963 environment.RUST_LOG = "info"; 964 }; 965 966 systemd.services.kanidm-unixd-tasks = mkIf cfg.enablePam { 967 description = "Kanidm PAM home management daemon"; 968 wantedBy = [ "multi-user.target" ]; 969 after = [ 970 "network.target" 971 "kanidm-unixd.service" 972 ]; 973 partOf = [ "kanidm-unixd.service" ]; 974 restartTriggers = [ 975 unixConfigFile 976 clientConfigFile 977 ]; 978 serviceConfig = { 979 ExecStart = "${cfg.package}/bin/kanidm_unixd_tasks"; 980 981 BindReadOnlyPaths = [ 982 "/nix/store" 983 "-/etc/resolv.conf" 984 "-/etc/nsswitch.conf" 985 "-/etc/hosts" 986 "-/etc/localtime" 987 "-/etc/kanidm" 988 "-/etc/static/kanidm" 989 ]; 990 BindPaths = [ 991 # To manage home directories 992 "/home" 993 # To connect to kanidm-unixd 994 "/run/kanidm-unixd:/var/run/kanidm-unixd" 995 ]; 996 # CAP_DAC_OVERRIDE is needed to ignore ownership of unixd socket 997 CapabilityBoundingSet = [ 998 "CAP_CHOWN" 999 "CAP_FOWNER" 1000 "CAP_DAC_OVERRIDE" 1001 "CAP_DAC_READ_SEARCH" 1002 ]; 1003 IPAddressDeny = "any"; 1004 # Need access to users 1005 PrivateUsers = false; 1006 # Need access to home directories 1007 ProtectHome = false; 1008 RestrictAddressFamilies = [ "AF_UNIX" ]; 1009 TemporaryFileSystem = "/:ro"; 1010 Restart = "on-failure"; 1011 }; 1012 environment.RUST_LOG = "info"; 1013 }; 1014 1015 # These paths are hardcoded 1016 environment.etc = mkMerge [ 1017 (mkIf cfg.enableServer { "kanidm/server.toml".source = serverConfigFile; }) 1018 (mkIf options.services.kanidm.clientSettings.isDefined { 1019 "kanidm/config".source = clientConfigFile; 1020 }) 1021 (mkIf cfg.enablePam { "kanidm/unixd".source = unixConfigFile; }) 1022 ]; 1023 1024 system.nssModules = mkIf cfg.enablePam [ cfg.package ]; 1025 1026 system.nssDatabases.group = optional cfg.enablePam "kanidm"; 1027 system.nssDatabases.passwd = optional cfg.enablePam "kanidm"; 1028 1029 users.groups = mkMerge [ 1030 (mkIf cfg.enableServer { kanidm = { }; }) 1031 (mkIf cfg.enablePam { kanidm-unixd = { }; }) 1032 ]; 1033 users.users = mkMerge [ 1034 (mkIf cfg.enableServer { 1035 kanidm = { 1036 description = "Kanidm server"; 1037 isSystemUser = true; 1038 group = "kanidm"; 1039 packages = [ cfg.package ]; 1040 }; 1041 }) 1042 (mkIf cfg.enablePam { 1043 kanidm-unixd = { 1044 description = "Kanidm PAM daemon"; 1045 isSystemUser = true; 1046 group = "kanidm-unixd"; 1047 }; 1048 }) 1049 ]; 1050 }; 1051 1052 meta.maintainers = with lib.maintainers; [ 1053 Flakebi 1054 oddlama 1055 ]; 1056 meta.buildDocsInSandbox = false; 1057}