at 21.11-pre 23 kB view raw
1{ config, lib, utils, pkgs, ... }: 2 3with lib; 4 5let 6 ids = config.ids; 7 cfg = config.users; 8 9 isPasswdCompatible = str: !(hasInfix ":" str || hasInfix "\n" str); 10 passwdEntry = type: lib.types.addCheck type isPasswdCompatible // { 11 name = "passwdEntry ${type.name}"; 12 description = "${type.description}, not containing newlines or colons"; 13 }; 14 15 # Check whether a password hash will allow login. 16 allowsLogin = hash: 17 hash == "" # login without password 18 || !(lib.elem hash 19 [ null # password login disabled 20 "!" # password login disabled 21 "!!" # a variant of "!" 22 "*" # password unset 23 ]); 24 25 passwordDescription = '' 26 The options <option>hashedPassword</option>, 27 <option>password</option> and <option>passwordFile</option> 28 controls what password is set for the user. 29 <option>hashedPassword</option> overrides both 30 <option>password</option> and <option>passwordFile</option>. 31 <option>password</option> overrides <option>passwordFile</option>. 32 If none of these three options are set, no password is assigned to 33 the user, and the user will not be able to do password logins. 34 If the option <option>users.mutableUsers</option> is true, the 35 password defined in one of the three options will only be set when 36 the user is created for the first time. After that, you are free to 37 change the password with the ordinary user management commands. If 38 <option>users.mutableUsers</option> is false, you cannot change 39 user passwords, they will always be set according to the password 40 options. 41 ''; 42 43 hashedPasswordDescription = '' 44 To generate a hashed password run <literal>mkpasswd -m sha-512</literal>. 45 46 If set to an empty string (<literal>""</literal>), this user will 47 be able to log in without being asked for a password (but not via remote 48 services such as SSH, or indirectly via <command>su</command> or 49 <command>sudo</command>). This should only be used for e.g. bootable 50 live systems. Note: this is different from setting an empty password, 51 which ca be achieved using <option>users.users.&lt;name?&gt;.password</option>. 52 53 If set to <literal>null</literal> (default) this user will not 54 be able to log in using a password (i.e. via <command>login</command> 55 command). 56 ''; 57 58 userOpts = { name, config, ... }: { 59 60 options = { 61 62 name = mkOption { 63 type = passwdEntry types.str; 64 apply = x: assert (builtins.stringLength x < 32 || abort "Username '${x}' is longer than 31 characters which is not allowed!"); x; 65 description = '' 66 The name of the user account. If undefined, the name of the 67 attribute set will be used. 68 ''; 69 }; 70 71 description = mkOption { 72 type = passwdEntry types.str; 73 default = ""; 74 example = "Alice Q. User"; 75 description = '' 76 A short description of the user account, typically the 77 user's full name. This is actually the GECOS or comment 78 field in <filename>/etc/passwd</filename>. 79 ''; 80 }; 81 82 uid = mkOption { 83 type = with types; nullOr int; 84 default = null; 85 description = '' 86 The account UID. If the UID is null, a free UID is picked on 87 activation. 88 ''; 89 }; 90 91 isSystemUser = mkOption { 92 type = types.bool; 93 default = false; 94 description = '' 95 Indicates if the user is a system user or not. This option 96 only has an effect if <option>uid</option> is 97 <option>null</option>, in which case it determines whether 98 the user's UID is allocated in the range for system users 99 (below 500) or in the range for normal users (starting at 100 1000). 101 Exactly one of <literal>isNormalUser</literal> and 102 <literal>isSystemUser</literal> must be true. 103 ''; 104 }; 105 106 isNormalUser = mkOption { 107 type = types.bool; 108 default = false; 109 description = '' 110 Indicates whether this is an account for a real user. This 111 automatically sets <option>group</option> to 112 <literal>users</literal>, <option>createHome</option> to 113 <literal>true</literal>, <option>home</option> to 114 <filename>/home/<replaceable>username</replaceable></filename>, 115 <option>useDefaultShell</option> to <literal>true</literal>, 116 and <option>isSystemUser</option> to 117 <literal>false</literal>. 118 Exactly one of <literal>isNormalUser</literal> and 119 <literal>isSystemUser</literal> must be true. 120 ''; 121 }; 122 123 group = mkOption { 124 type = types.str; 125 apply = x: assert (builtins.stringLength x < 32 || abort "Group name '${x}' is longer than 31 characters which is not allowed!"); x; 126 default = "nogroup"; 127 description = "The user's primary group."; 128 }; 129 130 extraGroups = mkOption { 131 type = types.listOf types.str; 132 default = []; 133 description = "The user's auxiliary groups."; 134 }; 135 136 home = mkOption { 137 type = passwdEntry types.path; 138 default = "/var/empty"; 139 description = "The user's home directory."; 140 }; 141 142 cryptHomeLuks = mkOption { 143 type = with types; nullOr str; 144 default = null; 145 description = '' 146 Path to encrypted luks device that contains 147 the user's home directory. 148 ''; 149 }; 150 151 pamMount = mkOption { 152 type = with types; attrsOf str; 153 default = {}; 154 description = '' 155 Attributes for user's entry in 156 <filename>pam_mount.conf.xml</filename>. 157 Useful attributes might include <code>path</code>, 158 <code>options</code>, <code>fstype</code>, and <code>server</code>. 159 See <link 160 xlink:href="http://pam-mount.sourceforge.net/pam_mount.conf.5.html" /> 161 for more information. 162 ''; 163 }; 164 165 shell = mkOption { 166 type = types.nullOr (types.either types.shellPackage (passwdEntry types.path)); 167 default = pkgs.shadow; 168 defaultText = "pkgs.shadow"; 169 example = literalExample "pkgs.bashInteractive"; 170 description = '' 171 The path to the user's shell. Can use shell derivations, 172 like <literal>pkgs.bashInteractive</literal>. Dont 173 forget to enable your shell in 174 <literal>programs</literal> if necessary, 175 like <code>programs.zsh.enable = true;</code>. 176 ''; 177 }; 178 179 subUidRanges = mkOption { 180 type = with types; listOf (submodule subordinateUidRange); 181 default = []; 182 example = [ 183 { startUid = 1000; count = 1; } 184 { startUid = 100001; count = 65534; } 185 ]; 186 description = '' 187 Subordinate user ids that user is allowed to use. 188 They are set into <filename>/etc/subuid</filename> and are used 189 by <literal>newuidmap</literal> for user namespaces. 190 ''; 191 }; 192 193 subGidRanges = mkOption { 194 type = with types; listOf (submodule subordinateGidRange); 195 default = []; 196 example = [ 197 { startGid = 100; count = 1; } 198 { startGid = 1001; count = 999; } 199 ]; 200 description = '' 201 Subordinate group ids that user is allowed to use. 202 They are set into <filename>/etc/subgid</filename> and are used 203 by <literal>newgidmap</literal> for user namespaces. 204 ''; 205 }; 206 207 createHome = mkOption { 208 type = types.bool; 209 default = false; 210 description = '' 211 Whether to create the home directory and ensure ownership as well as 212 permissions to match the user. 213 ''; 214 }; 215 216 useDefaultShell = mkOption { 217 type = types.bool; 218 default = false; 219 description = '' 220 If true, the user's shell will be set to 221 <option>users.defaultUserShell</option>. 222 ''; 223 }; 224 225 hashedPassword = mkOption { 226 type = with types; nullOr (passwdEntry str); 227 default = null; 228 description = '' 229 Specifies the hashed password for the user. 230 ${passwordDescription} 231 ${hashedPasswordDescription} 232 ''; 233 }; 234 235 password = mkOption { 236 type = with types; nullOr str; 237 default = null; 238 description = '' 239 Specifies the (clear text) password for the user. 240 Warning: do not set confidential information here 241 because it is world-readable in the Nix store. This option 242 should only be used for public accounts. 243 ${passwordDescription} 244 ''; 245 }; 246 247 passwordFile = mkOption { 248 type = with types; nullOr str; 249 default = null; 250 description = '' 251 The full path to a file that contains the user's password. The password 252 file is read on each system activation. The file should contain 253 exactly one line, which should be the password in an encrypted form 254 that is suitable for the <literal>chpasswd -e</literal> command. 255 ${passwordDescription} 256 ''; 257 }; 258 259 initialHashedPassword = mkOption { 260 type = with types; nullOr (passwdEntry str); 261 default = null; 262 description = '' 263 Specifies the initial hashed password for the user, i.e. the 264 hashed password assigned if the user does not already 265 exist. If <option>users.mutableUsers</option> is true, the 266 password can be changed subsequently using the 267 <command>passwd</command> command. Otherwise, it's 268 equivalent to setting the <option>hashedPassword</option> option. 269 270 ${hashedPasswordDescription} 271 ''; 272 }; 273 274 initialPassword = mkOption { 275 type = with types; nullOr str; 276 default = null; 277 description = '' 278 Specifies the initial password for the user, i.e. the 279 password assigned if the user does not already exist. If 280 <option>users.mutableUsers</option> is true, the password 281 can be changed subsequently using the 282 <command>passwd</command> command. Otherwise, it's 283 equivalent to setting the <option>password</option> 284 option. The same caveat applies: the password specified here 285 is world-readable in the Nix store, so it should only be 286 used for guest accounts or passwords that will be changed 287 promptly. 288 ''; 289 }; 290 291 packages = mkOption { 292 type = types.listOf types.package; 293 default = []; 294 example = literalExample "[ pkgs.firefox pkgs.thunderbird ]"; 295 description = '' 296 The set of packages that should be made available to the user. 297 This is in contrast to <option>environment.systemPackages</option>, 298 which adds packages to all users. 299 ''; 300 }; 301 302 }; 303 304 config = mkMerge 305 [ { name = mkDefault name; 306 shell = mkIf config.useDefaultShell (mkDefault cfg.defaultUserShell); 307 } 308 (mkIf config.isNormalUser { 309 group = mkDefault "users"; 310 createHome = mkDefault true; 311 home = mkDefault "/home/${config.name}"; 312 useDefaultShell = mkDefault true; 313 isSystemUser = mkDefault false; 314 }) 315 # If !mutableUsers, setting ‘initialPassword’ is equivalent to 316 # setting ‘password’ (and similarly for hashed passwords). 317 (mkIf (!cfg.mutableUsers && config.initialPassword != null) { 318 password = mkDefault config.initialPassword; 319 }) 320 (mkIf (!cfg.mutableUsers && config.initialHashedPassword != null) { 321 hashedPassword = mkDefault config.initialHashedPassword; 322 }) 323 ]; 324 325 }; 326 327 groupOpts = { name, ... }: { 328 329 options = { 330 331 name = mkOption { 332 type = passwdEntry types.str; 333 description = '' 334 The name of the group. If undefined, the name of the attribute set 335 will be used. 336 ''; 337 }; 338 339 gid = mkOption { 340 type = with types; nullOr int; 341 default = null; 342 description = '' 343 The group GID. If the GID is null, a free GID is picked on 344 activation. 345 ''; 346 }; 347 348 members = mkOption { 349 type = with types; listOf (passwdEntry str); 350 default = []; 351 description = '' 352 The user names of the group members, added to the 353 <literal>/etc/group</literal> file. 354 ''; 355 }; 356 357 }; 358 359 config = { 360 name = mkDefault name; 361 }; 362 363 }; 364 365 subordinateUidRange = { 366 options = { 367 startUid = mkOption { 368 type = types.int; 369 description = '' 370 Start of the range of subordinate user ids that user is 371 allowed to use. 372 ''; 373 }; 374 count = mkOption { 375 type = types.int; 376 default = 1; 377 description = "Count of subordinate user ids"; 378 }; 379 }; 380 }; 381 382 subordinateGidRange = { 383 options = { 384 startGid = mkOption { 385 type = types.int; 386 description = '' 387 Start of the range of subordinate group ids that user is 388 allowed to use. 389 ''; 390 }; 391 count = mkOption { 392 type = types.int; 393 default = 1; 394 description = "Count of subordinate group ids"; 395 }; 396 }; 397 }; 398 399 idsAreUnique = set: idAttr: !(fold (name: args@{ dup, acc }: 400 let 401 id = builtins.toString (builtins.getAttr idAttr (builtins.getAttr name set)); 402 exists = builtins.hasAttr id acc; 403 newAcc = acc // (builtins.listToAttrs [ { name = id; value = true; } ]); 404 in if dup then args else if exists 405 then builtins.trace "Duplicate ${idAttr} ${id}" { dup = true; acc = null; } 406 else { dup = false; acc = newAcc; } 407 ) { dup = false; acc = {}; } (builtins.attrNames set)).dup; 408 409 uidsAreUnique = idsAreUnique (filterAttrs (n: u: u.uid != null) cfg.users) "uid"; 410 gidsAreUnique = idsAreUnique (filterAttrs (n: g: g.gid != null) cfg.groups) "gid"; 411 412 spec = pkgs.writeText "users-groups.json" (builtins.toJSON { 413 inherit (cfg) mutableUsers; 414 users = mapAttrsToList (_: u: 415 { inherit (u) 416 name uid group description home createHome isSystemUser 417 password passwordFile hashedPassword 418 isNormalUser subUidRanges subGidRanges 419 initialPassword initialHashedPassword; 420 shell = utils.toShellPath u.shell; 421 }) cfg.users; 422 groups = mapAttrsToList (n: g: 423 { inherit (g) name gid; 424 members = g.members ++ (mapAttrsToList (n: u: u.name) ( 425 filterAttrs (n: u: elem g.name u.extraGroups) cfg.users 426 )); 427 }) cfg.groups; 428 }); 429 430 systemShells = 431 let 432 shells = mapAttrsToList (_: u: u.shell) cfg.users; 433 in 434 filter types.shellPackage.check shells; 435 436in { 437 imports = [ 438 (mkAliasOptionModule [ "users" "extraUsers" ] [ "users" "users" ]) 439 (mkAliasOptionModule [ "users" "extraGroups" ] [ "users" "groups" ]) 440 (mkChangedOptionModule 441 [ "security" "initialRootPassword" ] 442 [ "users" "users" "root" "initialHashedPassword" ] 443 (cfg: if cfg.security.initialRootPassword == "!" 444 then null 445 else cfg.security.initialRootPassword)) 446 ]; 447 448 ###### interface 449 450 options = { 451 452 users.mutableUsers = mkOption { 453 type = types.bool; 454 default = true; 455 description = '' 456 If set to <literal>true</literal>, you are free to add new users and groups to the system 457 with the ordinary <literal>useradd</literal> and 458 <literal>groupadd</literal> commands. On system activation, the 459 existing contents of the <literal>/etc/passwd</literal> and 460 <literal>/etc/group</literal> files will be merged with the 461 contents generated from the <literal>users.users</literal> and 462 <literal>users.groups</literal> options. 463 The initial password for a user will be set 464 according to <literal>users.users</literal>, but existing passwords 465 will not be changed. 466 467 <warning><para> 468 If set to <literal>false</literal>, the contents of the user and 469 group files will simply be replaced on system activation. This also 470 holds for the user passwords; all changed 471 passwords will be reset according to the 472 <literal>users.users</literal> configuration on activation. 473 </para></warning> 474 ''; 475 }; 476 477 users.enforceIdUniqueness = mkOption { 478 type = types.bool; 479 default = true; 480 description = '' 481 Whether to require that no two users/groups share the same uid/gid. 482 ''; 483 }; 484 485 users.users = mkOption { 486 default = {}; 487 type = with types; attrsOf (submodule userOpts); 488 example = { 489 alice = { 490 uid = 1234; 491 description = "Alice Q. User"; 492 home = "/home/alice"; 493 createHome = true; 494 group = "users"; 495 extraGroups = ["wheel"]; 496 shell = "/bin/sh"; 497 }; 498 }; 499 description = '' 500 Additional user accounts to be created automatically by the system. 501 This can also be used to set options for root. 502 ''; 503 }; 504 505 users.groups = mkOption { 506 default = {}; 507 example = 508 { students.gid = 1001; 509 hackers = { }; 510 }; 511 type = with types; attrsOf (submodule groupOpts); 512 description = '' 513 Additional groups to be created automatically by the system. 514 ''; 515 }; 516 517 }; 518 519 520 ###### implementation 521 522 config = { 523 524 users.users = { 525 root = { 526 uid = ids.uids.root; 527 description = "System administrator"; 528 home = "/root"; 529 shell = mkDefault cfg.defaultUserShell; 530 group = "root"; 531 }; 532 nobody = { 533 uid = ids.uids.nobody; 534 isSystemUser = true; 535 description = "Unprivileged account (don't use!)"; 536 group = "nogroup"; 537 }; 538 }; 539 540 users.groups = { 541 root.gid = ids.gids.root; 542 wheel.gid = ids.gids.wheel; 543 disk.gid = ids.gids.disk; 544 kmem.gid = ids.gids.kmem; 545 tty.gid = ids.gids.tty; 546 floppy.gid = ids.gids.floppy; 547 uucp.gid = ids.gids.uucp; 548 lp.gid = ids.gids.lp; 549 cdrom.gid = ids.gids.cdrom; 550 tape.gid = ids.gids.tape; 551 audio.gid = ids.gids.audio; 552 video.gid = ids.gids.video; 553 dialout.gid = ids.gids.dialout; 554 nogroup.gid = ids.gids.nogroup; 555 users.gid = ids.gids.users; 556 nixbld.gid = ids.gids.nixbld; 557 utmp.gid = ids.gids.utmp; 558 adm.gid = ids.gids.adm; 559 input.gid = ids.gids.input; 560 kvm.gid = ids.gids.kvm; 561 render.gid = ids.gids.render; 562 shadow.gid = ids.gids.shadow; 563 }; 564 565 system.activationScripts.users = stringAfter [ "stdio" ] 566 '' 567 install -m 0700 -d /root 568 install -m 0755 -d /home 569 570 ${pkgs.perl.withPackages (p: [ p.FileSlurp p.JSON ])}/bin/perl \ 571 -w ${./update-users-groups.pl} ${spec} 572 ''; 573 574 # for backwards compatibility 575 system.activationScripts.groups = stringAfter [ "users" ] ""; 576 577 # Install all the user shells 578 environment.systemPackages = systemShells; 579 580 environment.etc = (mapAttrs' (_: { packages, name, ... }: { 581 name = "profiles/per-user/${name}"; 582 value.source = pkgs.buildEnv { 583 name = "user-environment"; 584 paths = packages; 585 inherit (config.environment) pathsToLink extraOutputsToInstall; 586 inherit (config.system.path) ignoreCollisions postBuild; 587 }; 588 }) (filterAttrs (_: u: u.packages != []) cfg.users)); 589 590 environment.profiles = [ 591 "$HOME/.nix-profile" 592 "/etc/profiles/per-user/$USER" 593 ]; 594 595 assertions = [ 596 { assertion = !cfg.enforceIdUniqueness || (uidsAreUnique && gidsAreUnique); 597 message = "UIDs and GIDs must be unique!"; 598 } 599 { # If mutableUsers is false, to prevent users creating a 600 # configuration that locks them out of the system, ensure that 601 # there is at least one "privileged" account that has a 602 # password or an SSH authorized key. Privileged accounts are 603 # root and users in the wheel group. 604 assertion = !cfg.mutableUsers -> 605 any id ((mapAttrsToList (_: cfg: 606 (cfg.name == "root" 607 || cfg.group == "wheel" 608 || elem "wheel" cfg.extraGroups) 609 && 610 (allowsLogin cfg.hashedPassword 611 || cfg.password != null 612 || cfg.passwordFile != null 613 || cfg.openssh.authorizedKeys.keys != [] 614 || cfg.openssh.authorizedKeys.keyFiles != []) 615 ) cfg.users) ++ [ 616 config.security.googleOsLogin.enable 617 ]); 618 message = '' 619 Neither the root account nor any wheel user has a password or SSH authorized key. 620 You must set one to prevent being locked out of your system.''; 621 } 622 ] ++ flatten (flip mapAttrsToList cfg.users (name: user: 623 [ 624 { 625 assertion = (user.hashedPassword != null) 626 -> (builtins.match ".*:.*" user.hashedPassword == null); 627 message = '' 628 The password hash of user "${user.name}" contains a ":" character. 629 This is invalid and would break the login system because the fields 630 of /etc/shadow (file where hashes are stored) are colon-separated. 631 Please check the value of option `users.users."${user.name}".hashedPassword`.''; 632 } 633 { 634 assertion = let 635 xor = a: b: a && !b || b && !a; 636 isEffectivelySystemUser = user.isSystemUser || (user.uid != null && user.uid < 500); 637 in xor isEffectivelySystemUser user.isNormalUser; 638 message = '' 639 Exactly one of users.users.${user.name}.isSystemUser and users.users.${user.name}.isNormalUser must be set. 640 ''; 641 } 642 ] 643 )); 644 645 warnings = 646 builtins.filter (x: x != null) ( 647 flip mapAttrsToList cfg.users (_: user: 648 # This regex matches a subset of the Modular Crypto Format (MCF)[1] 649 # informal standard. Since this depends largely on the OS or the 650 # specific implementation of crypt(3) we only support the (sane) 651 # schemes implemented by glibc and BSDs. In particular the original 652 # DES hash is excluded since, having no structure, it would validate 653 # common mistakes like typing the plaintext password. 654 # 655 # [1]: https://en.wikipedia.org/wiki/Crypt_(C) 656 let 657 sep = "\\$"; 658 base64 = "[a-zA-Z0-9./]+"; 659 id = "[a-z0-9-]+"; 660 value = "[a-zA-Z0-9/+.-]+"; 661 options = "${id}(=${value})?(,${id}=${value})*"; 662 scheme = "${id}(${sep}${options})?"; 663 content = "${base64}${sep}${base64}"; 664 mcf = "^${sep}${scheme}${sep}${content}$"; 665 in 666 if (allowsLogin user.hashedPassword 667 && user.hashedPassword != "" # login without password 668 && builtins.match mcf user.hashedPassword == null) 669 then '' 670 The password hash of user "${user.name}" may be invalid. You must set a 671 valid hash or the user will be locked out of their account. Please 672 check the value of option `users.users."${user.name}".hashedPassword`.'' 673 else null 674 )); 675 676 }; 677 678}