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