at 25.11-pre 43 kB view raw
1{ 2 config, 3 lib, 4 utils, 5 pkgs, 6 ... 7}: 8 9let 10 inherit (lib) 11 any 12 attrNames 13 attrValues 14 concatMap 15 concatMapStringsSep 16 concatStrings 17 elem 18 filter 19 filterAttrs 20 flatten 21 flip 22 foldr 23 generators 24 getAttr 25 hasAttr 26 id 27 length 28 listToAttrs 29 literalExpression 30 mapAttrs' 31 mapAttrsToList 32 match 33 mkAliasOptionModuleMD 34 mkDefault 35 mkIf 36 mkMerge 37 mkOption 38 mkRenamedOptionModule 39 optional 40 optionals 41 sort 42 stringAfter 43 stringLength 44 trace 45 types 46 xor 47 ; 48 49 ids = config.ids; 50 cfg = config.users; 51 52 # Check whether a password hash will allow login. 53 allowsLogin = 54 hash: 55 hash == "" # login without password 56 || !(lib.elem hash [ 57 null # password login disabled 58 "!" # password login disabled 59 "!!" # a variant of "!" 60 "*" # password unset 61 ]); 62 63 overrideOrderMutable = ''{option}`initialHashedPassword` -> {option}`initialPassword` -> {option}`hashedPassword` -> {option}`password` -> {option}`hashedPasswordFile`''; 64 65 overrideOrderImmutable = ''{option}`initialHashedPassword` -> {option}`hashedPassword` -> {option}`initialPassword` -> {option}`password` -> {option}`hashedPasswordFile`''; 66 67 overrideOrderText = isMutable: '' 68 If the option {option}`users.mutableUsers` is 69 `${if isMutable then "true" else "false"}`, then the order of precedence is as shown 70 below, where values on the left are overridden by values on the right: 71 ${if isMutable then overrideOrderMutable else overrideOrderImmutable} 72 ''; 73 74 multiplePasswordsWarning = '' 75 If multiple of these password options are set at the same time then a 76 specific order of precedence is followed, which can lead to surprising 77 results. The order of precedence differs depending on whether the 78 {option}`users.mutableUsers` option is set. 79 ''; 80 81 overrideDescription = '' 82 ${multiplePasswordsWarning} 83 84 ${overrideOrderText false} 85 86 ${overrideOrderText true} 87 ''; 88 89 passwordDescription = '' 90 The {option}`initialHashedPassword`, {option}`hashedPassword`, 91 {option}`initialPassword`, {option}`password` and 92 {option}`hashedPasswordFile` options all control what password is set for 93 the user. 94 95 In a system where [](#opt-systemd.sysusers.enable) is `false`, typically 96 only one of {option}`hashedPassword`, {option}`password`, or 97 {option}`hashedPasswordFile` will be set. 98 99 In a system where [](#opt-systemd.sysusers.enable) is `true`, typically 100 only one of {option}`initialPassword`, {option}`initialHashedPassword`, 101 or {option}`hashedPasswordFile` will be set. 102 103 If the option {option}`users.mutableUsers` is true, the password defined 104 in one of the above password options will only be set when the user is 105 created for the first time. After that, you are free to change the 106 password with the ordinary user management commands. If 107 {option}`users.mutableUsers` is false, you cannot change user passwords, 108 they will always be set according to the password options. 109 110 If none of the password options are set, then no password is assigned to 111 the user, and the user will not be able to do password-based logins. 112 113 ${overrideDescription} 114 ''; 115 116 hashedPasswordDescription = '' 117 To generate a hashed password run `mkpasswd`. 118 119 If set to an empty string (`""`), this user will be able to log in without 120 being asked for a password (but not via remote services such as SSH, or 121 indirectly via {command}`su` or {command}`sudo`). This should only be used 122 for e.g. bootable live systems. Note: this is different from setting an 123 empty password, which can be achieved using 124 {option}`users.users.<name?>.password`. 125 126 If set to `null` (default) this user will not be able to log in using a 127 password (i.e. via {command}`login` command). 128 ''; 129 130 userOpts = 131 { name, config, ... }: 132 { 133 134 options = { 135 136 enable = mkOption { 137 type = types.bool; 138 default = true; 139 example = false; 140 description = '' 141 If set to false, the user account will not be created. This is useful for when you wish to conditionally 142 disable user accounts. 143 ''; 144 }; 145 146 name = mkOption { 147 type = types.passwdEntry types.str; 148 apply = 149 x: 150 assert ( 151 stringLength x < 32 || abort "Username '${x}' is longer than 31 characters which is not allowed!" 152 ); 153 x; 154 description = '' 155 The name of the user account. If undefined, the name of the 156 attribute set will be used. 157 ''; 158 }; 159 160 description = mkOption { 161 type = types.passwdEntry types.str; 162 default = ""; 163 example = "Alice Q. User"; 164 description = '' 165 A short description of the user account, typically the 166 user's full name. This is actually the GECOS or comment 167 field in {file}`/etc/passwd`. 168 ''; 169 }; 170 171 uid = mkOption { 172 type = with types; nullOr int; 173 default = null; 174 description = '' 175 The account UID. If the UID is null, a free UID is picked on 176 activation. 177 ''; 178 }; 179 180 isSystemUser = mkOption { 181 type = types.bool; 182 default = false; 183 description = '' 184 Indicates if the user is a system user or not. This option 185 only has an effect if {option}`uid` is 186 {option}`null`, in which case it determines whether 187 the user's UID is allocated in the range for system users 188 (below 1000) or in the range for normal users (starting at 189 1000). 190 Exactly one of `isNormalUser` and 191 `isSystemUser` must be true. 192 ''; 193 }; 194 195 isNormalUser = mkOption { 196 type = types.bool; 197 default = false; 198 description = '' 199 Indicates whether this is an account for a real user. 200 This automatically sets {option}`group` to `users`, 201 {option}`createHome` to `true`, 202 {option}`home` to {file}`/home/«username»`, 203 {option}`useDefaultShell` to `true`, 204 and {option}`isSystemUser` to `false`. 205 Exactly one of `isNormalUser` and `isSystemUser` must be true. 206 ''; 207 }; 208 209 group = mkOption { 210 type = types.str; 211 apply = 212 x: 213 assert ( 214 stringLength x < 32 || abort "Group name '${x}' is longer than 31 characters which is not allowed!" 215 ); 216 x; 217 default = ""; 218 description = "The user's primary group."; 219 }; 220 221 extraGroups = mkOption { 222 type = types.listOf types.str; 223 default = [ ]; 224 description = "The user's auxiliary groups."; 225 }; 226 227 home = mkOption { 228 type = types.passwdEntry types.path; 229 default = "/var/empty"; 230 description = "The user's home directory."; 231 }; 232 233 homeMode = mkOption { 234 type = types.strMatching "[0-7]{1,5}"; 235 default = "700"; 236 description = "The user's home directory mode in numeric format. See {manpage}`chmod(1)`. The mode is only applied if {option}`users.users.<name>.createHome` is true."; 237 }; 238 239 cryptHomeLuks = mkOption { 240 type = with types; nullOr str; 241 default = null; 242 description = '' 243 Path to encrypted luks device that contains 244 the user's home directory. 245 ''; 246 }; 247 248 pamMount = mkOption { 249 type = with types; attrsOf str; 250 default = { }; 251 description = '' 252 Attributes for user's entry in 253 {file}`pam_mount.conf.xml`. 254 Useful attributes might include `path`, 255 `options`, `fstype`, and `server`. 256 See <https://pam-mount.sourceforge.net/pam_mount.conf.5.html> 257 for more information. 258 ''; 259 }; 260 261 shell = mkOption { 262 type = types.nullOr (types.either types.shellPackage (types.passwdEntry types.path)); 263 default = pkgs.shadow; 264 defaultText = literalExpression "pkgs.shadow"; 265 example = literalExpression "pkgs.bashInteractive"; 266 description = '' 267 The path to the user's shell. Can use shell derivations, 268 like `pkgs.bashInteractive`. Dont 269 forget to enable your shell in 270 `programs` if necessary, 271 like `programs.zsh.enable = true;`. 272 ''; 273 }; 274 275 ignoreShellProgramCheck = mkOption { 276 type = types.bool; 277 default = false; 278 description = '' 279 By default, nixos will check that programs.SHELL.enable is set to 280 true if the user has a custom shell specified. If that behavior isn't 281 required and there are custom overrides in place to make sure that the 282 shell is functional, set this to true. 283 ''; 284 }; 285 286 subUidRanges = mkOption { 287 type = with types; listOf (submodule subordinateUidRange); 288 default = [ ]; 289 example = [ 290 { 291 startUid = 1000; 292 count = 1; 293 } 294 { 295 startUid = 100001; 296 count = 65534; 297 } 298 ]; 299 description = '' 300 Subordinate user ids that user is allowed to use. 301 They are set into {file}`/etc/subuid` and are used 302 by `newuidmap` for user namespaces. 303 ''; 304 }; 305 306 subGidRanges = mkOption { 307 type = with types; listOf (submodule subordinateGidRange); 308 default = [ ]; 309 example = [ 310 { 311 startGid = 100; 312 count = 1; 313 } 314 { 315 startGid = 1001; 316 count = 999; 317 } 318 ]; 319 description = '' 320 Subordinate group ids that user is allowed to use. 321 They are set into {file}`/etc/subgid` and are used 322 by `newgidmap` for user namespaces. 323 ''; 324 }; 325 326 autoSubUidGidRange = mkOption { 327 type = types.bool; 328 default = false; 329 example = true; 330 description = '' 331 Automatically allocate subordinate user and group ids for this user. 332 Allocated range is currently always of size 65536. 333 ''; 334 }; 335 336 createHome = mkOption { 337 type = types.bool; 338 default = false; 339 description = '' 340 Whether to create the home directory and ensure ownership as well as 341 permissions to match the user. 342 ''; 343 }; 344 345 useDefaultShell = mkOption { 346 type = types.bool; 347 default = false; 348 description = '' 349 If true, the user's shell will be set to 350 {option}`users.defaultUserShell`. 351 ''; 352 }; 353 354 hashedPassword = mkOption { 355 type = with types; nullOr (passwdEntry str); 356 default = null; 357 description = '' 358 Specifies the hashed password for the user. 359 360 ${passwordDescription} 361 ${hashedPasswordDescription} 362 ''; 363 }; 364 365 password = mkOption { 366 type = with types; nullOr str; 367 default = null; 368 description = '' 369 Specifies the (clear text) password for the user. 370 Warning: do not set confidential information here 371 because it is world-readable in the Nix store. This option 372 should only be used for public accounts. 373 374 ${passwordDescription} 375 ''; 376 }; 377 378 hashedPasswordFile = mkOption { 379 type = with types; nullOr str; 380 default = cfg.users.${name}.passwordFile; 381 defaultText = literalExpression "null"; 382 description = '' 383 The full path to a file that contains the hash of the user's 384 password. The password file is read on each system activation. The 385 file should contain exactly one line, which should be the password in 386 an encrypted form that is suitable for the `chpasswd -e` command. 387 388 ${passwordDescription} 389 ''; 390 }; 391 392 passwordFile = mkOption { 393 type = with types; nullOr str; 394 default = null; 395 visible = false; 396 description = "Deprecated alias of hashedPasswordFile"; 397 }; 398 399 initialHashedPassword = mkOption { 400 type = with types; nullOr (passwdEntry str); 401 default = null; 402 description = '' 403 Specifies the initial hashed password for the user, i.e. the 404 hashed password assigned if the user does not already 405 exist. If {option}`users.mutableUsers` is true, the 406 password can be changed subsequently using the 407 {command}`passwd` command. Otherwise, it's 408 equivalent to setting the {option}`hashedPassword` option. 409 410 ${passwordDescription} 411 ${hashedPasswordDescription} 412 ''; 413 }; 414 415 initialPassword = mkOption { 416 type = with types; nullOr str; 417 default = null; 418 description = '' 419 Specifies the initial password for the user, i.e. the 420 password assigned if the user does not already exist. If 421 {option}`users.mutableUsers` is true, the password 422 can be changed subsequently using the 423 {command}`passwd` command. Otherwise, it's 424 equivalent to setting the {option}`password` 425 option. The same caveat applies: the password specified here 426 is world-readable in the Nix store, so it should only be 427 used for guest accounts or passwords that will be changed 428 promptly. 429 430 ${passwordDescription} 431 ''; 432 }; 433 434 packages = mkOption { 435 type = types.listOf types.package; 436 default = [ ]; 437 example = literalExpression "[ pkgs.firefox pkgs.thunderbird ]"; 438 description = '' 439 The set of packages that should be made available to the user. 440 This is in contrast to {option}`environment.systemPackages`, 441 which adds packages to all users. 442 ''; 443 }; 444 445 expires = mkOption { 446 type = types.nullOr (types.strMatching "[[:digit:]]{4}-[[:digit:]]{2}-[[:digit:]]{2}"); 447 default = null; 448 description = '' 449 Set the date on which the user's account will no longer be 450 accessible. The date is expressed in the format YYYY-MM-DD, or null 451 to disable the expiry. 452 A user whose account is locked must contact the system 453 administrator before being able to use the system again. 454 ''; 455 }; 456 457 linger = mkOption { 458 type = types.bool; 459 default = false; 460 description = '' 461 Whether to enable lingering for this user. If true, systemd user 462 units will start at boot, rather than starting at login and stopping 463 at logout. This is the declarative equivalent of running 464 `loginctl enable-linger` for this user. 465 466 If false, user units will not be started until the user logs in, and 467 may be stopped on logout depending on the settings in `logind.conf`. 468 ''; 469 }; 470 }; 471 472 config = mkMerge [ 473 { 474 name = mkDefault name; 475 shell = mkIf config.useDefaultShell (mkDefault cfg.defaultUserShell); 476 } 477 (mkIf config.isNormalUser { 478 group = mkDefault "users"; 479 createHome = mkDefault true; 480 home = mkDefault "/home/${config.name}"; 481 homeMode = mkDefault "700"; 482 useDefaultShell = mkDefault true; 483 isSystemUser = mkDefault false; 484 }) 485 # If !mutableUsers, setting ‘initialPassword’ is equivalent to 486 # setting ‘password’ (and similarly for hashed passwords). 487 (mkIf (!cfg.mutableUsers && config.initialPassword != null) { 488 password = mkDefault config.initialPassword; 489 }) 490 (mkIf (!cfg.mutableUsers && config.initialHashedPassword != null) { 491 hashedPassword = mkDefault config.initialHashedPassword; 492 }) 493 (mkIf (config.isNormalUser && config.subUidRanges == [ ] && config.subGidRanges == [ ]) { 494 autoSubUidGidRange = mkDefault true; 495 }) 496 ]; 497 498 }; 499 500 groupOpts = 501 { name, config, ... }: 502 { 503 504 options = { 505 506 name = mkOption { 507 type = types.passwdEntry types.str; 508 description = '' 509 The name of the group. If undefined, the name of the attribute set 510 will be used. 511 ''; 512 }; 513 514 gid = mkOption { 515 type = with types; nullOr int; 516 default = null; 517 description = '' 518 The group GID. If the GID is null, a free GID is picked on 519 activation. 520 ''; 521 }; 522 523 members = mkOption { 524 type = with types; listOf (passwdEntry str); 525 default = [ ]; 526 description = '' 527 The user names of the group members, added to the 528 `/etc/group` file. 529 ''; 530 }; 531 532 }; 533 534 config = { 535 name = mkDefault name; 536 537 members = mapAttrsToList (n: u: u.name) ( 538 filterAttrs (n: u: elem config.name u.extraGroups) cfg.users 539 ); 540 }; 541 542 }; 543 544 subordinateUidRange = { 545 options = { 546 startUid = mkOption { 547 type = types.int; 548 description = '' 549 Start of the range of subordinate user ids that user is 550 allowed to use. 551 ''; 552 }; 553 count = mkOption { 554 type = types.int; 555 default = 1; 556 description = "Count of subordinate user ids"; 557 }; 558 }; 559 }; 560 561 subordinateGidRange = { 562 options = { 563 startGid = mkOption { 564 type = types.int; 565 description = '' 566 Start of the range of subordinate group ids that user is 567 allowed to use. 568 ''; 569 }; 570 count = mkOption { 571 type = types.int; 572 default = 1; 573 description = "Count of subordinate group ids"; 574 }; 575 }; 576 }; 577 578 idsAreUnique = 579 set: idAttr: 580 !(foldr 581 ( 582 name: 583 args@{ dup, acc }: 584 let 585 id = toString (getAttr idAttr (getAttr name set)); 586 exists = hasAttr id acc; 587 newAcc = 588 acc 589 // (listToAttrs [ 590 { 591 name = id; 592 value = true; 593 } 594 ]); 595 in 596 if dup then 597 args 598 else if exists then 599 trace "Duplicate ${idAttr} ${id}" { 600 dup = true; 601 acc = null; 602 } 603 else 604 { 605 dup = false; 606 acc = newAcc; 607 } 608 ) 609 { 610 dup = false; 611 acc = { }; 612 } 613 (attrNames set) 614 ).dup; 615 616 uidsAreUnique = idsAreUnique (filterAttrs (n: u: u.uid != null) cfg.users) "uid"; 617 gidsAreUnique = idsAreUnique (filterAttrs (n: g: g.gid != null) cfg.groups) "gid"; 618 sdInitrdUidsAreUnique = idsAreUnique (filterAttrs ( 619 n: u: u.uid != null 620 ) config.boot.initrd.systemd.users) "uid"; 621 sdInitrdGidsAreUnique = idsAreUnique (filterAttrs ( 622 n: g: g.gid != null 623 ) config.boot.initrd.systemd.groups) "gid"; 624 groupNames = lib.mapAttrsToList (n: g: g.name) cfg.groups; 625 usersWithoutExistingGroup = lib.filterAttrs ( 626 n: u: u.group != "" && !lib.elem u.group groupNames 627 ) cfg.users; 628 usersWithNullShells = attrNames (filterAttrs (name: cfg: cfg.shell == null) cfg.users); 629 630 spec = pkgs.writeText "users-groups.json" ( 631 builtins.toJSON { 632 inherit (cfg) mutableUsers; 633 users = mapAttrsToList (_: u: { 634 inherit (u) 635 name 636 uid 637 group 638 description 639 home 640 homeMode 641 createHome 642 isSystemUser 643 password 644 hashedPasswordFile 645 hashedPassword 646 autoSubUidGidRange 647 subUidRanges 648 subGidRanges 649 initialPassword 650 initialHashedPassword 651 expires 652 ; 653 shell = utils.toShellPath u.shell; 654 }) (filterAttrs (_: u: u.enable) cfg.users); 655 groups = attrValues cfg.groups; 656 } 657 ); 658 659 systemShells = 660 let 661 shells = mapAttrsToList (_: u: u.shell) cfg.users; 662 in 663 filter types.shellPackage.check shells; 664 665 lingeringUsers = map (u: u.name) (attrValues (flip filterAttrs cfg.users (n: u: u.linger))); 666in 667{ 668 imports = [ 669 (mkAliasOptionModuleMD [ "users" "extraUsers" ] [ "users" "users" ]) 670 (mkAliasOptionModuleMD [ "users" "extraGroups" ] [ "users" "groups" ]) 671 (mkRenamedOptionModule 672 [ "security" "initialRootPassword" ] 673 [ "users" "users" "root" "initialHashedPassword" ] 674 ) 675 ]; 676 677 ###### interface 678 options = { 679 680 users.mutableUsers = mkOption { 681 type = types.bool; 682 default = true; 683 description = '' 684 If set to `true`, you are free to add new users and groups to the system 685 with the ordinary `useradd` and 686 `groupadd` commands. On system activation, the 687 existing contents of the `/etc/passwd` and 688 `/etc/group` files will be merged with the 689 contents generated from the `users.users` and 690 `users.groups` options. 691 The initial password for a user will be set 692 according to `users.users`, but existing passwords 693 will not be changed. 694 695 ::: {.warning} 696 If set to `false`, the contents of the user and 697 group files will simply be replaced on system activation. This also 698 holds for the user passwords; all changed 699 passwords will be reset according to the 700 `users.users` configuration on activation. 701 ::: 702 ''; 703 }; 704 705 users.enforceIdUniqueness = mkOption { 706 type = types.bool; 707 default = true; 708 description = '' 709 Whether to require that no two users/groups share the same uid/gid. 710 ''; 711 }; 712 713 users.users = mkOption { 714 default = { }; 715 type = with types; attrsOf (submodule userOpts); 716 example = { 717 alice = { 718 uid = 1234; 719 description = "Alice Q. User"; 720 home = "/home/alice"; 721 createHome = true; 722 group = "users"; 723 extraGroups = [ "wheel" ]; 724 shell = "/bin/sh"; 725 }; 726 }; 727 description = '' 728 Additional user accounts to be created automatically by the system. 729 This can also be used to set options for root. 730 ''; 731 }; 732 733 users.groups = mkOption { 734 default = { }; 735 example = { 736 students.gid = 1001; 737 hackers = { }; 738 }; 739 type = with types; attrsOf (submodule groupOpts); 740 description = '' 741 Additional groups to be created automatically by the system. 742 ''; 743 }; 744 745 users.allowNoPasswordLogin = mkOption { 746 type = types.bool; 747 default = false; 748 description = '' 749 Disable checking that at least the `root` user or a user in the `wheel` group can log in using 750 a password or an SSH key. 751 752 WARNING: enabling this can lock you out of your system. Enable this only if you know what are you doing. 753 ''; 754 }; 755 756 # systemd initrd 757 boot.initrd.systemd.users = mkOption { 758 description = '' 759 Users to include in initrd. 760 ''; 761 default = { }; 762 type = types.attrsOf ( 763 types.submodule ( 764 { name, ... }: 765 { 766 options.uid = mkOption { 767 type = types.int; 768 description = '' 769 ID of the user in initrd. 770 ''; 771 defaultText = literalExpression "config.users.users.\${name}.uid"; 772 default = cfg.users.${name}.uid; 773 }; 774 options.group = mkOption { 775 type = types.singleLineStr; 776 description = '' 777 Group the user belongs to in initrd. 778 ''; 779 defaultText = literalExpression "config.users.users.\${name}.group"; 780 default = cfg.users.${name}.group; 781 }; 782 options.shell = mkOption { 783 type = types.passwdEntry types.path; 784 description = '' 785 The path to the user's shell in initrd. 786 ''; 787 default = "${pkgs.shadow}/bin/nologin"; 788 defaultText = literalExpression "\${pkgs.shadow}/bin/nologin"; 789 }; 790 } 791 ) 792 ); 793 }; 794 795 boot.initrd.systemd.groups = mkOption { 796 description = '' 797 Groups to include in initrd. 798 ''; 799 default = { }; 800 type = types.attrsOf ( 801 types.submodule ( 802 { name, ... }: 803 { 804 options.gid = mkOption { 805 type = types.int; 806 description = '' 807 ID of the group in initrd. 808 ''; 809 defaultText = literalExpression "config.users.groups.\${name}.gid"; 810 default = cfg.groups.${name}.gid; 811 }; 812 } 813 ) 814 ); 815 }; 816 }; 817 818 ###### implementation 819 820 config = 821 let 822 cryptSchemeIdPatternGroup = "(${lib.concatStringsSep "|" pkgs.libxcrypt.enabledCryptSchemeIds})"; 823 in 824 { 825 826 users.users = { 827 root = { 828 uid = ids.uids.root; 829 description = "System administrator"; 830 home = "/root"; 831 shell = mkDefault cfg.defaultUserShell; 832 group = "root"; 833 }; 834 nobody = { 835 uid = ids.uids.nobody; 836 isSystemUser = true; 837 description = "Unprivileged account (don't use!)"; 838 group = "nogroup"; 839 }; 840 }; 841 842 users.groups = { 843 root.gid = ids.gids.root; 844 wheel.gid = ids.gids.wheel; 845 disk.gid = ids.gids.disk; 846 kmem.gid = ids.gids.kmem; 847 tty.gid = ids.gids.tty; 848 floppy.gid = ids.gids.floppy; 849 uucp.gid = ids.gids.uucp; 850 lp.gid = ids.gids.lp; 851 cdrom.gid = ids.gids.cdrom; 852 tape.gid = ids.gids.tape; 853 audio.gid = ids.gids.audio; 854 video.gid = ids.gids.video; 855 dialout.gid = ids.gids.dialout; 856 nogroup.gid = ids.gids.nogroup; 857 users.gid = ids.gids.users; 858 nixbld.gid = ids.gids.nixbld; 859 utmp.gid = ids.gids.utmp; 860 adm.gid = ids.gids.adm; 861 input.gid = ids.gids.input; 862 kvm.gid = ids.gids.kvm; 863 render.gid = ids.gids.render; 864 sgx.gid = ids.gids.sgx; 865 shadow.gid = ids.gids.shadow; 866 }; 867 868 system.activationScripts.users = 869 if !config.systemd.sysusers.enable then 870 { 871 supportsDryActivation = true; 872 text = '' 873 install -m 0700 -d /root 874 install -m 0755 -d /home 875 876 ${ 877 pkgs.perl.withPackages (p: [ 878 p.FileSlurp 879 p.JSON 880 ]) 881 }/bin/perl \ 882 -w ${./update-users-groups.pl} ${spec} 883 ''; 884 } 885 else 886 ""; # keep around for backwards compatibility 887 888 systemd.services.linger-users = lib.mkIf ((length lingeringUsers) > 0) { 889 wantedBy = [ "multi-user.target" ]; 890 after = [ "systemd-logind.service" ]; 891 requires = [ "systemd-logind.service" ]; 892 893 script = 894 let 895 lingerDir = "/var/lib/systemd/linger"; 896 lingeringUsersFile = builtins.toFile "lingering-users" ( 897 concatStrings (map (s: "${s}\n") (sort (a: b: a < b) lingeringUsers)) 898 ); # this sorting is important for `comm` to work correctly 899 in 900 '' 901 mkdir -vp ${lingerDir} 902 cd ${lingerDir} 903 for user in $(ls); do 904 if ! id "$user" >/dev/null; then 905 echo "Removing linger for missing user $user" 906 rm --force -- "$user" 907 fi 908 done 909 ls | sort | comm -3 -1 ${lingeringUsersFile} - | xargs -r ${pkgs.systemd}/bin/loginctl disable-linger 910 ls | sort | comm -3 -2 ${lingeringUsersFile} - | xargs -r ${pkgs.systemd}/bin/loginctl enable-linger 911 ''; 912 913 serviceConfig.Type = "oneshot"; 914 }; 915 916 # Warn about user accounts with deprecated password hashing schemes 917 # This does not work when the users and groups are created by 918 # systemd-sysusers because the users are created too late then. 919 system.activationScripts.hashes = 920 if !config.systemd.sysusers.enable then 921 { 922 deps = [ "users" ]; 923 text = '' 924 users=() 925 while IFS=: read -r user hash _; do 926 if [[ "$hash" = "$"* && ! "$hash" =~ ^\''$${cryptSchemeIdPatternGroup}\$ ]]; then 927 users+=("$user") 928 fi 929 done </etc/shadow 930 931 if (( "''${#users[@]}" )); then 932 echo " 933 WARNING: The following user accounts rely on password hashing algorithms 934 that have been removed. They need to be renewed as soon as possible, as 935 they do prevent their users from logging in." 936 printf ' - %s\n' "''${users[@]}" 937 fi 938 ''; 939 } 940 else 941 ""; # keep around for backwards compatibility 942 943 # for backwards compatibility 944 system.activationScripts.groups = stringAfter [ "users" ] ""; 945 946 # Install all the user shells 947 environment.systemPackages = systemShells; 948 949 environment.etc = mapAttrs' ( 950 _: 951 { packages, name, ... }: 952 { 953 name = "profiles/per-user/${name}"; 954 value.source = pkgs.buildEnv { 955 name = "user-environment"; 956 paths = packages; 957 inherit (config.environment) pathsToLink extraOutputsToInstall; 958 inherit (config.system.path) ignoreCollisions postBuild; 959 }; 960 } 961 ) (filterAttrs (_: u: u.packages != [ ]) cfg.users); 962 963 environment.profiles = [ 964 "$HOME/.nix-profile" 965 "\${XDG_STATE_HOME}/nix/profile" 966 "$HOME/.local/state/nix/profile" 967 "/etc/profiles/per-user/$USER" 968 ]; 969 970 # systemd initrd 971 boot.initrd.systemd = lib.mkIf config.boot.initrd.systemd.enable { 972 contents = { 973 "/etc/passwd".text = '' 974 ${lib.concatStringsSep "\n" ( 975 lib.mapAttrsToList ( 976 n: 977 { 978 uid, 979 group, 980 shell, 981 }: 982 let 983 g = config.boot.initrd.systemd.groups.${group}; 984 in 985 "${n}:x:${toString uid}:${toString g.gid}::/var/empty:${shell}" 986 ) config.boot.initrd.systemd.users 987 )} 988 ''; 989 "/etc/group".text = '' 990 ${lib.concatStringsSep "\n" ( 991 lib.mapAttrsToList (n: { gid }: "${n}:x:${toString gid}:") config.boot.initrd.systemd.groups 992 )} 993 ''; 994 "/etc/shells".text = 995 lib.concatStringsSep "\n" ( 996 lib.unique (lib.mapAttrsToList (_: u: u.shell) config.boot.initrd.systemd.users) 997 ) 998 + "\n"; 999 }; 1000 1001 storePaths = [ "${pkgs.shadow}/bin/nologin" ]; 1002 1003 users = { 1004 root = { 1005 shell = lib.mkDefault "/bin/bash"; 1006 }; 1007 nobody = { }; 1008 }; 1009 1010 groups = { 1011 root = { }; 1012 nogroup = { }; 1013 systemd-journal = { }; 1014 tty = { }; 1015 dialout = { }; 1016 kmem = { }; 1017 input = { }; 1018 video = { }; 1019 render = { }; 1020 sgx = { }; 1021 audio = { }; 1022 video = { }; 1023 lp = { }; 1024 disk = { }; 1025 cdrom = { }; 1026 tape = { }; 1027 kvm = { }; 1028 }; 1029 }; 1030 1031 assertions = 1032 [ 1033 { 1034 assertion = !cfg.enforceIdUniqueness || (uidsAreUnique && gidsAreUnique); 1035 message = "UIDs and GIDs must be unique!"; 1036 } 1037 { 1038 assertion = !cfg.enforceIdUniqueness || (sdInitrdUidsAreUnique && sdInitrdGidsAreUnique); 1039 message = "systemd initrd UIDs and GIDs must be unique!"; 1040 } 1041 { 1042 assertion = usersWithoutExistingGroup == { }; 1043 message = 1044 let 1045 errUsers = lib.attrNames usersWithoutExistingGroup; 1046 missingGroups = lib.unique (lib.mapAttrsToList (n: u: u.group) usersWithoutExistingGroup); 1047 mkConfigHint = group: "users.groups.${group} = {};"; 1048 in 1049 '' 1050 The following users have a primary group that is undefined: ${lib.concatStringsSep " " errUsers} 1051 Hint: Add this to your NixOS configuration: 1052 ${lib.concatStringsSep "\n " (map mkConfigHint missingGroups)} 1053 ''; 1054 } 1055 { 1056 assertion = !cfg.mutableUsers -> length usersWithNullShells == 0; 1057 message = '' 1058 users.mutableUsers = false has been set, 1059 but found users that have their shell set to null. 1060 If you wish to disable login, set their shell to pkgs.shadow (the default). 1061 Misconfigured users: ${lib.concatStringsSep " " usersWithNullShells} 1062 ''; 1063 } 1064 { 1065 # If mutableUsers is false, to prevent users creating a 1066 # configuration that locks them out of the system, ensure that 1067 # there is at least one "privileged" account that has a 1068 # password or an SSH authorized key. Privileged accounts are 1069 # root and users in the wheel group. 1070 # The check does not apply when users.allowNoPasswordLogin 1071 # The check does not apply when users.mutableUsers 1072 assertion = 1073 !cfg.mutableUsers 1074 -> !cfg.allowNoPasswordLogin 1075 -> any id ( 1076 mapAttrsToList ( 1077 name: cfg: 1078 (name == "root" || cfg.group == "wheel" || elem "wheel" cfg.extraGroups) 1079 && ( 1080 allowsLogin cfg.hashedPassword 1081 || cfg.password != null 1082 || cfg.hashedPasswordFile != null 1083 || cfg.openssh.authorizedKeys.keys != [ ] 1084 || cfg.openssh.authorizedKeys.keyFiles != [ ] 1085 ) 1086 ) cfg.users 1087 ++ [ 1088 config.security.googleOsLogin.enable 1089 ] 1090 ); 1091 message = '' 1092 Neither the root account nor any wheel user has a password or SSH authorized key. 1093 You must set one to prevent being locked out of your system. 1094 If you really want to be locked out of your system, set users.allowNoPasswordLogin = true; 1095 However you are most probably better off by setting users.mutableUsers = true; and 1096 manually running passwd root to set the root password. 1097 ''; 1098 } 1099 ] 1100 ++ flatten ( 1101 flip mapAttrsToList cfg.users ( 1102 name: user: 1103 [ 1104 ( 1105 let 1106 # Things fail in various ways with especially non-ascii usernames. 1107 # This regex mirrors the one from shadow's is_valid_name: 1108 # https://github.com/shadow-maint/shadow/blob/bee77ffc291dfed2a133496db465eaa55e2b0fec/lib/chkname.c#L68 1109 # though without the trailing $, because Samba 3 got its last release 1110 # over 10 years ago and is not in Nixpkgs anymore, 1111 # while later versions don't appear to require anything like that. 1112 nameRegex = "[a-zA-Z0-9_.][a-zA-Z0-9_.-]*"; 1113 in 1114 { 1115 assertion = builtins.match nameRegex user.name != null; 1116 message = "The username \"${user.name}\" is not valid, it does not match the regex \"${nameRegex}\"."; 1117 } 1118 ) 1119 { 1120 assertion = (user.hashedPassword != null) -> (match ".*:.*" user.hashedPassword == null); 1121 message = '' 1122 The password hash of user "${user.name}" contains a ":" character. 1123 This is invalid and would break the login system because the fields 1124 of /etc/shadow (file where hashes are stored) are colon-separated. 1125 Please check the value of option `users.users."${user.name}".hashedPassword`.''; 1126 } 1127 { 1128 assertion = user.isNormalUser && user.uid != null -> user.uid >= 1000; 1129 message = '' 1130 A user cannot have a users.users.${user.name}.uid set below 1000 and set users.users.${user.name}.isNormalUser. 1131 Either users.users.${user.name}.isSystemUser must be set to true instead of users.users.${user.name}.isNormalUser 1132 or users.users.${user.name}.uid must be changed to 1000 or above. 1133 ''; 1134 } 1135 { 1136 assertion = 1137 let 1138 # we do an extra check on isNormalUser here, to not trigger this assertion when isNormalUser is set and uid to < 1000 1139 isEffectivelySystemUser = 1140 user.isSystemUser || (user.uid != null && user.uid < 1000 && !user.isNormalUser); 1141 in 1142 xor isEffectivelySystemUser user.isNormalUser; 1143 message = '' 1144 Exactly one of users.users.${user.name}.isSystemUser and users.users.${user.name}.isNormalUser must be set. 1145 ''; 1146 } 1147 { 1148 assertion = user.group != ""; 1149 message = '' 1150 users.users.${user.name}.group is unset. This used to default to 1151 nogroup, but this is unsafe. For example you can create a group 1152 for this user with: 1153 users.users.${user.name}.group = "${user.name}"; 1154 users.groups.${user.name} = {}; 1155 ''; 1156 } 1157 ] 1158 ++ (map 1159 (shell: { 1160 assertion = 1161 !user.ignoreShellProgramCheck 1162 -> (user.shell == pkgs.${shell}) 1163 -> (config.programs.${shell}.enable == true); 1164 message = '' 1165 users.users.${user.name}.shell is set to ${shell}, but 1166 programs.${shell}.enable is not true. This will cause the ${shell} 1167 shell to lack the basic nix directories in its PATH and might make 1168 logging in as that user impossible. You can fix it with: 1169 programs.${shell}.enable = true; 1170 1171 If you know what you're doing and you are fine with the behavior, 1172 set users.users.${user.name}.ignoreShellProgramCheck = true; 1173 instead. 1174 ''; 1175 }) 1176 [ 1177 "fish" 1178 "xonsh" 1179 "zsh" 1180 ] 1181 ) 1182 ) 1183 ); 1184 1185 warnings = 1186 flip concatMap (attrValues cfg.users) ( 1187 user: 1188 let 1189 passwordOptions = 1190 [ 1191 "hashedPassword" 1192 "hashedPasswordFile" 1193 "password" 1194 ] 1195 ++ optionals cfg.mutableUsers [ 1196 # For immutable users, initialHashedPassword is set to hashedPassword, 1197 # so using these options would always trigger the assertion. 1198 "initialHashedPassword" 1199 "initialPassword" 1200 ]; 1201 unambiguousPasswordConfiguration = 1202 1 >= length (filter (x: x != null) (map (flip getAttr user) passwordOptions)); 1203 in 1204 optional (!unambiguousPasswordConfiguration) '' 1205 The user '${user.name}' has multiple of the options 1206 `initialHashedPassword`, `hashedPassword`, `initialPassword`, `password` 1207 & `hashedPasswordFile` set to a non-null value. 1208 1209 ${multiplePasswordsWarning} 1210 ${overrideOrderText cfg.mutableUsers} 1211 The values of these options are: 1212 ${concatMapStringsSep "\n" ( 1213 value: "* users.users.\"${user.name}\".${value}: ${generators.toPretty { } user.${value}}" 1214 ) passwordOptions} 1215 '' 1216 ) 1217 ++ filter (x: x != null) ( 1218 flip mapAttrsToList cfg.users ( 1219 _: user: 1220 # This regex matches a subset of the Modular Crypto Format (MCF)[1] 1221 # informal standard. Since this depends largely on the OS or the 1222 # specific implementation of crypt(3) we only support the (sane) 1223 # schemes implemented by glibc and BSDs. In particular the original 1224 # DES hash is excluded since, having no structure, it would validate 1225 # common mistakes like typing the plaintext password. 1226 # 1227 # [1]: https://en.wikipedia.org/wiki/Crypt_(C) 1228 let 1229 sep = "\\$"; 1230 base64 = "[a-zA-Z0-9./]+"; 1231 id = cryptSchemeIdPatternGroup; 1232 name = "[a-z0-9-]+"; 1233 value = "[a-zA-Z0-9/+.-]+"; 1234 options = "${name}(=${value})?(,${name}=${value})*"; 1235 scheme = "${id}(${sep}${options})?"; 1236 content = "${base64}${sep}${base64}(${sep}${base64})?"; 1237 mcf = "^${sep}${scheme}${sep}${content}$"; 1238 in 1239 if 1240 ( 1241 allowsLogin user.hashedPassword 1242 && user.hashedPassword != "" # login without password 1243 && match mcf user.hashedPassword == null 1244 ) 1245 then 1246 '' 1247 The password hash of user "${user.name}" may be invalid. You must set a 1248 valid hash or the user will be locked out of their account. Please 1249 check the value of option `users.users."${user.name}".hashedPassword`.'' 1250 else 1251 null 1252 ) 1253 ++ flip mapAttrsToList cfg.users ( 1254 name: user: 1255 if user.passwordFile != null then 1256 ''The option `users.users."${name}".passwordFile' has been renamed '' 1257 + ''to `users.users."${name}".hashedPasswordFile'.'' 1258 else 1259 null 1260 ) 1261 ); 1262 }; 1263 1264}