1{ config, lib, pkgs, ... }: 2 3with lib; 4 5let 6 7 ids = config.ids; 8 cfg = config.users; 9 10 passwordDescription = '' 11 The options <option>hashedPassword</option>, 12 <option>password</option> and <option>passwordFile</option> 13 controls what password is set for the user. 14 <option>hashedPassword</option> overrides both 15 <option>password</option> and <option>passwordFile</option>. 16 <option>password</option> overrides <option>passwordFile</option>. 17 If none of these three options are set, no password is assigned to 18 the user, and the user will not be able to do password logins. 19 If the option <option>users.mutableUsers</option> is true, the 20 password defined in one of the three options will only be set when 21 the user is created for the first time. After that, you are free to 22 change the password with the ordinary user management commands. If 23 <option>users.mutableUsers</option> is false, you cannot change 24 user passwords, they will always be set according to the password 25 options. 26 ''; 27 28 hashedPasswordDescription = '' 29 To generate hashed password install <literal>mkpasswd</literal> 30 package and run <literal>mkpasswd -m sha-512</literal>. 31 ''; 32 33 userOpts = { name, config, ... }: { 34 35 options = { 36 37 name = mkOption { 38 type = types.str; 39 description = '' 40 The name of the user account. If undefined, the name of the 41 attribute set will be used. 42 ''; 43 }; 44 45 description = mkOption { 46 type = types.str; 47 default = ""; 48 example = "Alice Q. User"; 49 description = '' 50 A short description of the user account, typically the 51 user's full name. This is actually the GECOS or comment 52 field in <filename>/etc/passwd</filename>. 53 ''; 54 }; 55 56 uid = mkOption { 57 type = with types; nullOr int; 58 default = null; 59 description = '' 60 The account UID. If the UID is null, a free UID is picked on 61 activation. 62 ''; 63 }; 64 65 isSystemUser = mkOption { 66 type = types.bool; 67 default = false; 68 description = '' 69 Indicates if the user is a system user or not. This option 70 only has an effect if <option>uid</option> is 71 <option>null</option>, in which case it determines whether 72 the user's UID is allocated in the range for system users 73 (below 500) or in the range for normal users (starting at 74 1000). 75 ''; 76 }; 77 78 isNormalUser = mkOption { 79 type = types.bool; 80 default = false; 81 description = '' 82 Indicates whether this is an account for a real user. This 83 automatically sets <option>group</option> to 84 <literal>users</literal>, <option>createHome</option> to 85 <literal>true</literal>, <option>home</option> to 86 <filename>/home/<replaceable>username</replaceable></filename>, 87 <option>useDefaultShell</option> to <literal>true</literal>, 88 and <option>isSystemUser</option> to 89 <literal>false</literal>. 90 ''; 91 }; 92 93 group = mkOption { 94 type = types.str; 95 default = "nogroup"; 96 description = "The user's primary group."; 97 }; 98 99 extraGroups = mkOption { 100 type = types.listOf types.str; 101 default = []; 102 description = "The user's auxiliary groups."; 103 }; 104 105 home = mkOption { 106 type = types.str; 107 default = "/var/empty"; 108 description = "The user's home directory."; 109 }; 110 111 cryptHomeLuks = mkOption { 112 type = with types; nullOr str; 113 default = null; 114 description = '' 115 Path to encrypted luks device that contains 116 the user's home directory. 117 ''; 118 }; 119 120 shell = mkOption { 121 type = types.str; 122 default = "/run/current-system/sw/bin/nologin"; 123 description = "The path to the user's shell."; 124 }; 125 126 subUidRanges = mkOption { 127 type = types.listOf types.optionSet; 128 default = []; 129 example = [ 130 { startUid = 1000; count = 1; } 131 { startUid = 100001; count = 65534; } 132 ]; 133 options = [ subordinateUidRange ]; 134 description = '' 135 Subordinate user ids that user is allowed to use. 136 They are set into <filename>/etc/subuid</filename> and are used 137 by <literal>newuidmap</literal> for user namespaces. 138 ''; 139 }; 140 141 subGidRanges = mkOption { 142 type = types.listOf types.optionSet; 143 default = []; 144 example = [ 145 { startGid = 100; count = 1; } 146 { startGid = 1001; count = 999; } 147 ]; 148 options = [ subordinateGidRange ]; 149 description = '' 150 Subordinate group ids that user is allowed to use. 151 They are set into <filename>/etc/subgid</filename> and are used 152 by <literal>newgidmap</literal> for user namespaces. 153 ''; 154 }; 155 156 createHome = mkOption { 157 type = types.bool; 158 default = false; 159 description = '' 160 If true, the home directory will be created automatically. If this 161 option is true and the home directory already exists but is not 162 owned by the user, directory owner and group will be changed to 163 match the user. 164 ''; 165 }; 166 167 useDefaultShell = mkOption { 168 type = types.bool; 169 default = false; 170 description = '' 171 If true, the user's shell will be set to 172 <option>users.defaultUserShell</option>. 173 ''; 174 }; 175 176 hashedPassword = mkOption { 177 type = with types; uniq (nullOr str); 178 default = null; 179 description = '' 180 Specifies the hashed password for the user. 181 ${passwordDescription} 182 ${hashedPasswordDescription} 183 ''; 184 }; 185 186 password = mkOption { 187 type = with types; uniq (nullOr str); 188 default = null; 189 description = '' 190 Specifies the (clear text) password for the user. 191 Warning: do not set confidential information here 192 because it is world-readable in the Nix store. This option 193 should only be used for public accounts. 194 ${passwordDescription} 195 ''; 196 }; 197 198 passwordFile = mkOption { 199 type = with types; uniq (nullOr string); 200 default = null; 201 description = '' 202 The full path to a file that contains the user's password. The password 203 file is read on each system activation. The file should contain 204 exactly one line, which should be the password in an encrypted form 205 that is suitable for the <literal>chpasswd -e</literal> command. 206 ${passwordDescription} 207 ''; 208 }; 209 210 initialHashedPassword = mkOption { 211 type = with types; uniq (nullOr str); 212 default = null; 213 description = '' 214 Specifies the initial hashed password for the user, i.e. the 215 hashed password assigned if the user does not already 216 exist. If <option>users.mutableUsers</option> is true, the 217 password can be changed subsequently using the 218 <command>passwd</command> command. Otherwise, it's 219 equivalent to setting the <option>hashedPassword</option> option. 220 221 ${hashedPasswordDescription} 222 ''; 223 }; 224 225 initialPassword = mkOption { 226 type = with types; uniq (nullOr str); 227 default = null; 228 description = '' 229 Specifies the initial password for the user, i.e. the 230 password assigned if the user does not already exist. If 231 <option>users.mutableUsers</option> is true, the password 232 can be changed subsequently using the 233 <command>passwd</command> command. Otherwise, it's 234 equivalent to setting the <option>password</option> 235 option. The same caveat applies: the password specified here 236 is world-readable in the Nix store, so it should only be 237 used for guest accounts or passwords that will be changed 238 promptly. 239 ''; 240 }; 241 242 }; 243 244 config = mkMerge 245 [ { name = mkDefault name; 246 shell = mkIf config.useDefaultShell (mkDefault cfg.defaultUserShell); 247 } 248 (mkIf config.isNormalUser { 249 group = mkDefault "users"; 250 createHome = mkDefault true; 251 home = mkDefault "/home/${name}"; 252 useDefaultShell = mkDefault true; 253 isSystemUser = mkDefault false; 254 }) 255 # If !mutableUsers, setting ‘initialPassword’ is equivalent to 256 # setting ‘password’ (and similarly for hashed passwords). 257 (mkIf (!cfg.mutableUsers && config.initialPassword != null) { 258 password = mkDefault config.initialPassword; 259 }) 260 (mkIf (!cfg.mutableUsers && config.initialHashedPassword != null) { 261 hashedPassword = mkDefault config.initialHashedPassword; 262 }) 263 ]; 264 265 }; 266 267 groupOpts = { name, config, ... }: { 268 269 options = { 270 271 name = mkOption { 272 type = types.str; 273 description = '' 274 The name of the group. If undefined, the name of the attribute set 275 will be used. 276 ''; 277 }; 278 279 gid = mkOption { 280 type = with types; nullOr int; 281 default = null; 282 description = '' 283 The group GID. If the GID is null, a free GID is picked on 284 activation. 285 ''; 286 }; 287 288 members = mkOption { 289 type = with types; listOf string; 290 default = []; 291 description = '' 292 The user names of the group members, added to the 293 <literal>/etc/group</literal> file. 294 ''; 295 }; 296 297 }; 298 299 config = { 300 name = mkDefault name; 301 }; 302 303 }; 304 305 subordinateUidRange = { 306 startUid = mkOption { 307 type = types.int; 308 description = '' 309 Start of the range of subordinate user ids that user is 310 allowed to use. 311 ''; 312 }; 313 count = mkOption { 314 type = types.int; 315 default = 1; 316 description = ''Count of subordinate user ids''; 317 }; 318 }; 319 320 subordinateGidRange = { 321 startGid = mkOption { 322 type = types.int; 323 description = '' 324 Start of the range of subordinate group ids that user is 325 allowed to use. 326 ''; 327 }; 328 count = mkOption { 329 type = types.int; 330 default = 1; 331 description = ''Count of subordinate group ids''; 332 }; 333 }; 334 335 mkSubuidEntry = user: concatStrings ( 336 map (range: "${user.name}:${toString range.startUid}:${toString range.count}\n") 337 user.subUidRanges); 338 339 subuidFile = concatStrings (map mkSubuidEntry (attrValues cfg.users)); 340 341 mkSubgidEntry = user: concatStrings ( 342 map (range: "${user.name}:${toString range.startGid}:${toString range.count}\n") 343 user.subGidRanges); 344 345 subgidFile = concatStrings (map mkSubgidEntry (attrValues cfg.users)); 346 347 idsAreUnique = set: idAttr: !(fold (name: args@{ dup, acc }: 348 let 349 id = builtins.toString (builtins.getAttr idAttr (builtins.getAttr name set)); 350 exists = builtins.hasAttr id acc; 351 newAcc = acc // (builtins.listToAttrs [ { name = id; value = true; } ]); 352 in if dup then args else if exists 353 then builtins.trace "Duplicate ${idAttr} ${id}" { dup = true; acc = null; } 354 else { dup = false; acc = newAcc; } 355 ) { dup = false; acc = {}; } (builtins.attrNames set)).dup; 356 357 uidsAreUnique = idsAreUnique (filterAttrs (n: u: u.uid != null) cfg.users) "uid"; 358 gidsAreUnique = idsAreUnique (filterAttrs (n: g: g.gid != null) cfg.groups) "gid"; 359 360 spec = pkgs.writeText "users-groups.json" (builtins.toJSON { 361 inherit (cfg) mutableUsers; 362 users = mapAttrsToList (n: u: 363 { inherit (u) 364 name uid group description home shell createHome isSystemUser 365 password passwordFile hashedPassword 366 initialPassword initialHashedPassword; 367 }) cfg.users; 368 groups = mapAttrsToList (n: g: 369 { inherit (g) name gid; 370 members = g.members ++ (mapAttrsToList (n: u: u.name) ( 371 filterAttrs (n: u: elem g.name u.extraGroups) cfg.users 372 )); 373 }) cfg.groups; 374 }); 375 376in { 377 378 ###### interface 379 380 options = { 381 382 users.mutableUsers = mkOption { 383 type = types.bool; 384 default = true; 385 description = '' 386 If set to <literal>true</literal>, you are free to add new users and groups to the system 387 with the ordinary <literal>useradd</literal> and 388 <literal>groupadd</literal> commands. On system activation, the 389 existing contents of the <literal>/etc/passwd</literal> and 390 <literal>/etc/group</literal> files will be merged with the 391 contents generated from the <literal>users.users</literal> and 392 <literal>users.groups</literal> options. 393 The initial password for a user will be set 394 according to <literal>users.users</literal>, but existing passwords 395 will not be changed. 396 397 <warning><para> 398 If set to <literal>false</literal>, the contents of the user and 399 group files will simply be replaced on system activation. This also 400 holds for the user passwords; all changed 401 passwords will be reset according to the 402 <literal>users.users</literal> configuration on activation. 403 </para></warning> 404 ''; 405 }; 406 407 users.enforceIdUniqueness = mkOption { 408 type = types.bool; 409 default = true; 410 description = '' 411 Whether to require that no two users/groups share the same uid/gid. 412 ''; 413 }; 414 415 users.users = mkOption { 416 default = {}; 417 type = types.loaOf types.optionSet; 418 example = { 419 alice = { 420 uid = 1234; 421 description = "Alice Q. User"; 422 home = "/home/alice"; 423 createHome = true; 424 group = "users"; 425 extraGroups = ["wheel"]; 426 shell = "/bin/sh"; 427 }; 428 }; 429 description = '' 430 Additional user accounts to be created automatically by the system. 431 This can also be used to set options for root. 432 ''; 433 options = [ userOpts ]; 434 }; 435 436 users.groups = mkOption { 437 default = {}; 438 example = 439 { students.gid = 1001; 440 hackers = { }; 441 }; 442 type = types.loaOf types.optionSet; 443 description = '' 444 Additional groups to be created automatically by the system. 445 ''; 446 options = [ groupOpts ]; 447 }; 448 449 # FIXME: obsolete - will remove. 450 security.initialRootPassword = mkOption { 451 type = types.str; 452 default = "!"; 453 example = ""; 454 visible = false; 455 }; 456 457 }; 458 459 460 ###### implementation 461 462 config = { 463 464 users.users = { 465 root = { 466 uid = ids.uids.root; 467 description = "System administrator"; 468 home = "/root"; 469 shell = mkDefault cfg.defaultUserShell; 470 group = "root"; 471 extraGroups = [ "grsecurity" ]; 472 initialHashedPassword = mkDefault config.security.initialRootPassword; 473 }; 474 nobody = { 475 uid = ids.uids.nobody; 476 description = "Unprivileged account (don't use!)"; 477 group = "nogroup"; 478 }; 479 }; 480 481 users.groups = { 482 root.gid = ids.gids.root; 483 wheel.gid = ids.gids.wheel; 484 disk.gid = ids.gids.disk; 485 kmem.gid = ids.gids.kmem; 486 tty.gid = ids.gids.tty; 487 floppy.gid = ids.gids.floppy; 488 uucp.gid = ids.gids.uucp; 489 lp.gid = ids.gids.lp; 490 cdrom.gid = ids.gids.cdrom; 491 tape.gid = ids.gids.tape; 492 audio.gid = ids.gids.audio; 493 video.gid = ids.gids.video; 494 dialout.gid = ids.gids.dialout; 495 nogroup.gid = ids.gids.nogroup; 496 users.gid = ids.gids.users; 497 nixbld.gid = ids.gids.nixbld; 498 utmp.gid = ids.gids.utmp; 499 adm.gid = ids.gids.adm; 500 grsecurity.gid = ids.gids.grsecurity; 501 input.gid = ids.gids.input; 502 }; 503 504 system.activationScripts.users = stringAfter [ "etc" ] 505 '' 506 ${pkgs.perl}/bin/perl -w \ 507 -I${pkgs.perlPackages.FileSlurp}/lib/perl5/site_perl \ 508 -I${pkgs.perlPackages.JSON}/lib/perl5/site_perl \ 509 ${./update-users-groups.pl} ${spec} 510 ''; 511 512 # for backwards compatibility 513 system.activationScripts.groups = stringAfter [ "users" ] ""; 514 515 environment.etc."subuid" = { 516 text = subuidFile; 517 mode = "0644"; 518 }; 519 environment.etc."subgid" = { 520 text = subgidFile; 521 mode = "0644"; 522 }; 523 524 assertions = [ 525 { assertion = !cfg.enforceIdUniqueness || (uidsAreUnique && gidsAreUnique); 526 message = "UIDs and GIDs must be unique!"; 527 } 528 { # If mutableUsers is false, to prevent users creating a 529 # configuration that locks them out of the system, ensure that 530 # there is at least one "privileged" account that has a 531 # password or an SSH authorized key. Privileged accounts are 532 # root and users in the wheel group. 533 assertion = !cfg.mutableUsers -> 534 any id (mapAttrsToList (name: cfg: 535 (name == "root" 536 || cfg.group == "wheel" 537 || elem "wheel" cfg.extraGroups) 538 && 539 ((cfg.hashedPassword != null && cfg.hashedPassword != "!") 540 || cfg.password != null 541 || cfg.passwordFile != null 542 || cfg.openssh.authorizedKeys.keys != [] 543 || cfg.openssh.authorizedKeys.keyFiles != []) 544 ) cfg.users); 545 message = '' 546 Neither the root account nor any wheel user has a password or SSH authorized key. 547 You must set one to prevent being locked out of your system.''; 548 } 549 ]; 550 551 }; 552 553 imports = 554 [ (mkAliasOptionModule [ "users" "extraUsers" ] [ "users" "users" ]) 555 (mkAliasOptionModule [ "users" "extraGroups" ] [ "users" "groups" ]) 556 ]; 557}