at 22.05-pre 24 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 = ""; 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 = literalExpression "pkgs.shadow"; 169 example = literalExpression "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 = literalExpression "[ 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, config, ... }: { 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 members = mapAttrsToList (n: u: u.name) ( 363 filterAttrs (n: u: elem config.name u.extraGroups) cfg.users 364 ); 365 }; 366 367 }; 368 369 subordinateUidRange = { 370 options = { 371 startUid = mkOption { 372 type = types.int; 373 description = '' 374 Start of the range of subordinate user ids that user is 375 allowed to use. 376 ''; 377 }; 378 count = mkOption { 379 type = types.int; 380 default = 1; 381 description = "Count of subordinate user ids"; 382 }; 383 }; 384 }; 385 386 subordinateGidRange = { 387 options = { 388 startGid = mkOption { 389 type = types.int; 390 description = '' 391 Start of the range of subordinate group ids that user is 392 allowed to use. 393 ''; 394 }; 395 count = mkOption { 396 type = types.int; 397 default = 1; 398 description = "Count of subordinate group ids"; 399 }; 400 }; 401 }; 402 403 idsAreUnique = set: idAttr: !(foldr (name: args@{ dup, acc }: 404 let 405 id = builtins.toString (builtins.getAttr idAttr (builtins.getAttr name set)); 406 exists = builtins.hasAttr id acc; 407 newAcc = acc // (builtins.listToAttrs [ { name = id; value = true; } ]); 408 in if dup then args else if exists 409 then builtins.trace "Duplicate ${idAttr} ${id}" { dup = true; acc = null; } 410 else { dup = false; acc = newAcc; } 411 ) { dup = false; acc = {}; } (builtins.attrNames set)).dup; 412 413 uidsAreUnique = idsAreUnique (filterAttrs (n: u: u.uid != null) cfg.users) "uid"; 414 gidsAreUnique = idsAreUnique (filterAttrs (n: g: g.gid != null) cfg.groups) "gid"; 415 416 spec = pkgs.writeText "users-groups.json" (builtins.toJSON { 417 inherit (cfg) mutableUsers; 418 users = mapAttrsToList (_: u: 419 { inherit (u) 420 name uid group description home createHome isSystemUser 421 password passwordFile hashedPassword 422 isNormalUser subUidRanges subGidRanges 423 initialPassword initialHashedPassword; 424 shell = utils.toShellPath u.shell; 425 }) cfg.users; 426 groups = attrValues cfg.groups; 427 }); 428 429 systemShells = 430 let 431 shells = mapAttrsToList (_: u: u.shell) cfg.users; 432 in 433 filter types.shellPackage.check shells; 434 435in { 436 imports = [ 437 (mkAliasOptionModule [ "users" "extraUsers" ] [ "users" "users" ]) 438 (mkAliasOptionModule [ "users" "extraGroups" ] [ "users" "groups" ]) 439 (mkChangedOptionModule 440 [ "security" "initialRootPassword" ] 441 [ "users" "users" "root" "initialHashedPassword" ] 442 (cfg: if cfg.security.initialRootPassword == "!" 443 then null 444 else cfg.security.initialRootPassword)) 445 ]; 446 447 ###### interface 448 449 options = { 450 451 users.mutableUsers = mkOption { 452 type = types.bool; 453 default = true; 454 description = '' 455 If set to <literal>true</literal>, you are free to add new users and groups to the system 456 with the ordinary <literal>useradd</literal> and 457 <literal>groupadd</literal> commands. On system activation, the 458 existing contents of the <literal>/etc/passwd</literal> and 459 <literal>/etc/group</literal> files will be merged with the 460 contents generated from the <literal>users.users</literal> and 461 <literal>users.groups</literal> options. 462 The initial password for a user will be set 463 according to <literal>users.users</literal>, but existing passwords 464 will not be changed. 465 466 <warning><para> 467 If set to <literal>false</literal>, the contents of the user and 468 group files will simply be replaced on system activation. This also 469 holds for the user passwords; all changed 470 passwords will be reset according to the 471 <literal>users.users</literal> configuration on activation. 472 </para></warning> 473 ''; 474 }; 475 476 users.enforceIdUniqueness = mkOption { 477 type = types.bool; 478 default = true; 479 description = '' 480 Whether to require that no two users/groups share the same uid/gid. 481 ''; 482 }; 483 484 users.users = mkOption { 485 default = {}; 486 type = with types; attrsOf (submodule userOpts); 487 example = { 488 alice = { 489 uid = 1234; 490 description = "Alice Q. User"; 491 home = "/home/alice"; 492 createHome = true; 493 group = "users"; 494 extraGroups = ["wheel"]; 495 shell = "/bin/sh"; 496 }; 497 }; 498 description = '' 499 Additional user accounts to be created automatically by the system. 500 This can also be used to set options for root. 501 ''; 502 }; 503 504 users.groups = mkOption { 505 default = {}; 506 example = 507 { students.gid = 1001; 508 hackers = { }; 509 }; 510 type = with types; attrsOf (submodule groupOpts); 511 description = '' 512 Additional groups to be created automatically by the system. 513 ''; 514 }; 515 516 }; 517 518 519 ###### implementation 520 521 config = { 522 523 users.users = { 524 root = { 525 uid = ids.uids.root; 526 description = "System administrator"; 527 home = "/root"; 528 shell = mkDefault cfg.defaultUserShell; 529 group = "root"; 530 }; 531 nobody = { 532 uid = ids.uids.nobody; 533 isSystemUser = true; 534 description = "Unprivileged account (don't use!)"; 535 group = "nogroup"; 536 }; 537 }; 538 539 users.groups = { 540 root.gid = ids.gids.root; 541 wheel.gid = ids.gids.wheel; 542 disk.gid = ids.gids.disk; 543 kmem.gid = ids.gids.kmem; 544 tty.gid = ids.gids.tty; 545 floppy.gid = ids.gids.floppy; 546 uucp.gid = ids.gids.uucp; 547 lp.gid = ids.gids.lp; 548 cdrom.gid = ids.gids.cdrom; 549 tape.gid = ids.gids.tape; 550 audio.gid = ids.gids.audio; 551 video.gid = ids.gids.video; 552 dialout.gid = ids.gids.dialout; 553 nogroup.gid = ids.gids.nogroup; 554 users.gid = ids.gids.users; 555 nixbld.gid = ids.gids.nixbld; 556 utmp.gid = ids.gids.utmp; 557 adm.gid = ids.gids.adm; 558 input.gid = ids.gids.input; 559 kvm.gid = ids.gids.kvm; 560 render.gid = ids.gids.render; 561 shadow.gid = ids.gids.shadow; 562 }; 563 564 system.activationScripts.users = { 565 supportsDryActivation = true; 566 text = '' 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 575 # for backwards compatibility 576 system.activationScripts.groups = stringAfter [ "users" ] ""; 577 578 # Install all the user shells 579 environment.systemPackages = systemShells; 580 581 environment.etc = (mapAttrs' (_: { packages, name, ... }: { 582 name = "profiles/per-user/${name}"; 583 value.source = pkgs.buildEnv { 584 name = "user-environment"; 585 paths = packages; 586 inherit (config.environment) pathsToLink extraOutputsToInstall; 587 inherit (config.system.path) ignoreCollisions postBuild; 588 }; 589 }) (filterAttrs (_: u: u.packages != []) cfg.users)); 590 591 environment.profiles = [ 592 "$HOME/.nix-profile" 593 "/etc/profiles/per-user/$USER" 594 ]; 595 596 assertions = [ 597 { assertion = !cfg.enforceIdUniqueness || (uidsAreUnique && gidsAreUnique); 598 message = "UIDs and GIDs must be unique!"; 599 } 600 { # If mutableUsers is false, to prevent users creating a 601 # configuration that locks them out of the system, ensure that 602 # there is at least one "privileged" account that has a 603 # password or an SSH authorized key. Privileged accounts are 604 # root and users in the wheel group. 605 assertion = !cfg.mutableUsers -> 606 any id ((mapAttrsToList (_: cfg: 607 (cfg.name == "root" 608 || cfg.group == "wheel" 609 || elem "wheel" cfg.extraGroups) 610 && 611 (allowsLogin cfg.hashedPassword 612 || cfg.password != null 613 || cfg.passwordFile != null 614 || cfg.openssh.authorizedKeys.keys != [] 615 || cfg.openssh.authorizedKeys.keyFiles != []) 616 ) cfg.users) ++ [ 617 config.security.googleOsLogin.enable 618 ]); 619 message = '' 620 Neither the root account nor any wheel user has a password or SSH authorized key. 621 You must set one to prevent being locked out of your system.''; 622 } 623 ] ++ flatten (flip mapAttrsToList cfg.users (name: user: 624 [ 625 { 626 assertion = (user.hashedPassword != null) 627 -> (builtins.match ".*:.*" user.hashedPassword == null); 628 message = '' 629 The password hash of user "${user.name}" contains a ":" character. 630 This is invalid and would break the login system because the fields 631 of /etc/shadow (file where hashes are stored) are colon-separated. 632 Please check the value of option `users.users."${user.name}".hashedPassword`.''; 633 } 634 { 635 assertion = let 636 xor = a: b: a && !b || b && !a; 637 isEffectivelySystemUser = user.isSystemUser || (user.uid != null && user.uid < 500); 638 in xor isEffectivelySystemUser user.isNormalUser; 639 message = '' 640 Exactly one of users.users.${user.name}.isSystemUser and users.users.${user.name}.isNormalUser must be set. 641 ''; 642 } 643 { 644 assertion = user.group != ""; 645 message = '' 646 users.users.${user.name}.group is unset. This used to default to 647 nogroup, but this is unsafe. For example you can create a group 648 for this user with: 649 users.users.${user.name}.group = "${user.name}"; 650 users.groups.${user.name} = {}; 651 ''; 652 } 653 ] 654 )); 655 656 warnings = 657 builtins.filter (x: x != null) ( 658 flip mapAttrsToList cfg.users (_: user: 659 # This regex matches a subset of the Modular Crypto Format (MCF)[1] 660 # informal standard. Since this depends largely on the OS or the 661 # specific implementation of crypt(3) we only support the (sane) 662 # schemes implemented by glibc and BSDs. In particular the original 663 # DES hash is excluded since, having no structure, it would validate 664 # common mistakes like typing the plaintext password. 665 # 666 # [1]: https://en.wikipedia.org/wiki/Crypt_(C) 667 let 668 sep = "\\$"; 669 base64 = "[a-zA-Z0-9./]+"; 670 id = "[a-z0-9-]+"; 671 value = "[a-zA-Z0-9/+.-]+"; 672 options = "${id}(=${value})?(,${id}=${value})*"; 673 scheme = "${id}(${sep}${options})?"; 674 content = "${base64}${sep}${base64}"; 675 mcf = "^${sep}${scheme}${sep}${content}$"; 676 in 677 if (allowsLogin user.hashedPassword 678 && user.hashedPassword != "" # login without password 679 && builtins.match mcf user.hashedPassword == null) 680 then '' 681 The password hash of user "${user.name}" may be invalid. You must set a 682 valid hash or the user will be locked out of their account. Please 683 check the value of option `users.users."${user.name}".hashedPassword`.'' 684 else null 685 )); 686 687 }; 688 689}