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`. Don’t
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 "${cfg.defaultUserHome}/${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: u.enable && 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 users.defaultUserHome = mkOption {
757 type = types.str;
758 default = "/home";
759 description = ''
760 The default home directory for normal users.
761 '';
762 };
763
764 # systemd initrd
765 boot.initrd.systemd.users = mkOption {
766 description = ''
767 Users to include in initrd.
768 '';
769 default = { };
770 type = types.attrsOf (
771 types.submodule (
772 { name, ... }:
773 {
774 options.uid = mkOption {
775 type = types.int;
776 description = ''
777 ID of the user in initrd.
778 '';
779 defaultText = literalExpression "config.users.users.\${name}.uid";
780 default = cfg.users.${name}.uid;
781 };
782 options.group = mkOption {
783 type = types.singleLineStr;
784 description = ''
785 Group the user belongs to in initrd.
786 '';
787 defaultText = literalExpression "config.users.users.\${name}.group";
788 default = cfg.users.${name}.group;
789 };
790 options.shell = mkOption {
791 type = types.passwdEntry types.path;
792 description = ''
793 The path to the user's shell in initrd.
794 '';
795 default = "${pkgs.shadow}/bin/nologin";
796 defaultText = literalExpression "\${pkgs.shadow}/bin/nologin";
797 };
798 }
799 )
800 );
801 };
802
803 boot.initrd.systemd.groups = mkOption {
804 description = ''
805 Groups to include in initrd.
806 '';
807 default = { };
808 type = types.attrsOf (
809 types.submodule (
810 { name, ... }:
811 {
812 options.gid = mkOption {
813 type = types.int;
814 description = ''
815 ID of the group in initrd.
816 '';
817 defaultText = literalExpression "config.users.groups.\${name}.gid";
818 default = cfg.groups.${name}.gid;
819 };
820 }
821 )
822 );
823 };
824 };
825
826 ###### implementation
827
828 config =
829 let
830 cryptSchemeIdPatternGroup = "(${lib.concatStringsSep "|" pkgs.libxcrypt.enabledCryptSchemeIds})";
831 in
832 {
833
834 users.users = {
835 root = {
836 uid = ids.uids.root;
837 description = "System administrator";
838 home = "/root";
839 shell = mkDefault cfg.defaultUserShell;
840 group = "root";
841 };
842 nobody = {
843 uid = ids.uids.nobody;
844 isSystemUser = true;
845 description = "Unprivileged account (don't use!)";
846 group = "nogroup";
847 };
848 };
849
850 users.groups = {
851 root.gid = ids.gids.root;
852 wheel.gid = ids.gids.wheel;
853 disk.gid = ids.gids.disk;
854 kmem.gid = ids.gids.kmem;
855 tty.gid = ids.gids.tty;
856 floppy.gid = ids.gids.floppy;
857 uucp.gid = ids.gids.uucp;
858 lp.gid = ids.gids.lp;
859 cdrom.gid = ids.gids.cdrom;
860 tape.gid = ids.gids.tape;
861 audio.gid = ids.gids.audio;
862 video.gid = ids.gids.video;
863 dialout.gid = ids.gids.dialout;
864 nogroup.gid = ids.gids.nogroup;
865 users.gid = ids.gids.users;
866 nixbld.gid = ids.gids.nixbld;
867 utmp.gid = ids.gids.utmp;
868 adm.gid = ids.gids.adm;
869 input.gid = ids.gids.input;
870 kvm.gid = ids.gids.kvm;
871 render.gid = ids.gids.render;
872 sgx.gid = ids.gids.sgx;
873 shadow.gid = ids.gids.shadow;
874 };
875
876 system.activationScripts.users =
877 if !config.systemd.sysusers.enable then
878 {
879 supportsDryActivation = true;
880 text = ''
881 install -m 0700 -d /root
882 install -m 0755 -d /home
883
884 ${
885 pkgs.perl.withPackages (p: [
886 p.FileSlurp
887 p.JSON
888 ])
889 }/bin/perl \
890 -w ${./update-users-groups.pl} ${spec}
891 '';
892 }
893 else
894 ""; # keep around for backwards compatibility
895
896 systemd.services.linger-users = lib.mkIf ((length lingeringUsers) > 0) {
897 wantedBy = [ "multi-user.target" ];
898 after = [ "systemd-logind.service" ];
899 requires = [ "systemd-logind.service" ];
900
901 script =
902 let
903 lingerDir = "/var/lib/systemd/linger";
904 lingeringUsersFile = builtins.toFile "lingering-users" (
905 concatStrings (map (s: "${s}\n") (sort (a: b: a < b) lingeringUsers))
906 ); # this sorting is important for `comm` to work correctly
907 in
908 ''
909 mkdir -vp ${lingerDir}
910 cd ${lingerDir}
911 for user in $(ls); do
912 if ! id "$user" >/dev/null; then
913 echo "Removing linger for missing user $user"
914 rm --force -- "$user"
915 fi
916 done
917 ls | sort | comm -3 -1 ${lingeringUsersFile} - | xargs -r ${pkgs.systemd}/bin/loginctl disable-linger
918 ls | sort | comm -3 -2 ${lingeringUsersFile} - | xargs -r ${pkgs.systemd}/bin/loginctl enable-linger
919 '';
920
921 serviceConfig.Type = "oneshot";
922 };
923
924 # Warn about user accounts with deprecated password hashing schemes
925 # This does not work when the users and groups are created by
926 # systemd-sysusers because the users are created too late then.
927 system.activationScripts.hashes =
928 if !config.systemd.sysusers.enable then
929 {
930 deps = [ "users" ];
931 text = ''
932 users=()
933 while IFS=: read -r user hash _; do
934 if [[ "$hash" = "$"* && ! "$hash" =~ ^\''$${cryptSchemeIdPatternGroup}\$ ]]; then
935 users+=("$user")
936 fi
937 done </etc/shadow
938
939 if (( "''${#users[@]}" )); then
940 echo "
941 WARNING: The following user accounts rely on password hashing algorithms
942 that have been removed. They need to be renewed as soon as possible, as
943 they do prevent their users from logging in."
944 printf ' - %s\n' "''${users[@]}"
945 fi
946 '';
947 }
948 else
949 ""; # keep around for backwards compatibility
950
951 # for backwards compatibility
952 system.activationScripts.groups = stringAfter [ "users" ] "";
953
954 # Install all the user shells
955 environment.systemPackages = systemShells;
956
957 environment.etc = mapAttrs' (
958 _:
959 { packages, name, ... }:
960 {
961 name = "profiles/per-user/${name}";
962 value.source = pkgs.buildEnv {
963 name = "user-environment";
964 paths = packages;
965 inherit (config.environment) pathsToLink extraOutputsToInstall;
966 inherit (config.system.path) ignoreCollisions postBuild;
967 };
968 }
969 ) (filterAttrs (_: u: u.packages != [ ]) cfg.users);
970
971 environment.profiles = [
972 "$HOME/.nix-profile"
973 "\${XDG_STATE_HOME}/nix/profile"
974 "$HOME/.local/state/nix/profile"
975 "/etc/profiles/per-user/$USER"
976 ];
977
978 # systemd initrd
979 boot.initrd.systemd = lib.mkIf config.boot.initrd.systemd.enable {
980 contents = {
981 "/etc/passwd".text = ''
982 ${lib.concatStringsSep "\n" (
983 lib.mapAttrsToList (
984 n:
985 {
986 uid,
987 group,
988 shell,
989 }:
990 let
991 g = config.boot.initrd.systemd.groups.${group};
992 in
993 "${n}:x:${toString uid}:${toString g.gid}::/var/empty:${shell}"
994 ) config.boot.initrd.systemd.users
995 )}
996 '';
997 "/etc/group".text = ''
998 ${lib.concatStringsSep "\n" (
999 lib.mapAttrsToList (n: { gid }: "${n}:x:${toString gid}:") config.boot.initrd.systemd.groups
1000 )}
1001 '';
1002 "/etc/shells".text =
1003 lib.concatStringsSep "\n" (
1004 lib.unique (lib.mapAttrsToList (_: u: u.shell) config.boot.initrd.systemd.users)
1005 )
1006 + "\n";
1007 };
1008
1009 storePaths = [ "${pkgs.shadow}/bin/nologin" ];
1010
1011 users = {
1012 root = {
1013 shell = lib.mkDefault "/bin/bash";
1014 };
1015 nobody = { };
1016 };
1017
1018 groups = {
1019 root = { };
1020 nogroup = { };
1021 systemd-journal = { };
1022 tty = { };
1023 dialout = { };
1024 kmem = { };
1025 input = { };
1026 video = { };
1027 render = { };
1028 sgx = { };
1029 audio = { };
1030 video = { };
1031 lp = { };
1032 disk = { };
1033 cdrom = { };
1034 tape = { };
1035 kvm = { };
1036 };
1037 };
1038
1039 assertions = [
1040 {
1041 assertion = !cfg.enforceIdUniqueness || (uidsAreUnique && gidsAreUnique);
1042 message = "UIDs and GIDs must be unique!";
1043 }
1044 {
1045 assertion = !cfg.enforceIdUniqueness || (sdInitrdUidsAreUnique && sdInitrdGidsAreUnique);
1046 message = "systemd initrd UIDs and GIDs must be unique!";
1047 }
1048 {
1049 assertion = usersWithoutExistingGroup == { };
1050 message =
1051 let
1052 errUsers = lib.attrNames usersWithoutExistingGroup;
1053 missingGroups = lib.unique (lib.mapAttrsToList (n: u: u.group) usersWithoutExistingGroup);
1054 mkConfigHint = group: "users.groups.${group} = {};";
1055 in
1056 ''
1057 The following users have a primary group that is undefined: ${lib.concatStringsSep " " errUsers}
1058 Hint: Add this to your NixOS configuration:
1059 ${lib.concatStringsSep "\n " (map mkConfigHint missingGroups)}
1060 '';
1061 }
1062 {
1063 assertion = !cfg.mutableUsers -> length usersWithNullShells == 0;
1064 message = ''
1065 users.mutableUsers = false has been set,
1066 but found users that have their shell set to null.
1067 If you wish to disable login, set their shell to pkgs.shadow (the default).
1068 Misconfigured users: ${lib.concatStringsSep " " usersWithNullShells}
1069 '';
1070 }
1071 {
1072 # If mutableUsers is false, to prevent users creating a
1073 # configuration that locks them out of the system, ensure that
1074 # there is at least one "privileged" account that has a
1075 # password or an SSH authorized key. Privileged accounts are
1076 # root and users in the wheel group.
1077 # The check does not apply when users.allowNoPasswordLogin
1078 # The check does not apply when users.mutableUsers
1079 assertion =
1080 !cfg.mutableUsers
1081 -> !cfg.allowNoPasswordLogin
1082 -> any id (
1083 mapAttrsToList (
1084 name: cfg:
1085 (name == "root" || cfg.group == "wheel" || elem "wheel" cfg.extraGroups)
1086 && (
1087 allowsLogin cfg.hashedPassword
1088 || cfg.password != null
1089 || cfg.hashedPasswordFile != null
1090 || cfg.openssh.authorizedKeys.keys != [ ]
1091 || cfg.openssh.authorizedKeys.keyFiles != [ ]
1092 )
1093 ) cfg.users
1094 ++ [
1095 config.security.googleOsLogin.enable
1096 ]
1097 );
1098 message = ''
1099 Neither the root account nor any wheel user has a password or SSH authorized key.
1100 You must set one to prevent being locked out of your system.
1101 If you really want to be locked out of your system, set users.allowNoPasswordLogin = true;
1102 However you are most probably better off by setting users.mutableUsers = true; and
1103 manually running passwd root to set the root password.
1104 '';
1105 }
1106 ]
1107 ++ flatten (
1108 flip mapAttrsToList cfg.users (
1109 name: user:
1110 [
1111 (
1112 let
1113 # Things fail in various ways with especially non-ascii usernames.
1114 # This regex mirrors the one from shadow's is_valid_name:
1115 # https://github.com/shadow-maint/shadow/blob/bee77ffc291dfed2a133496db465eaa55e2b0fec/lib/chkname.c#L68
1116 # though without the trailing $, because Samba 3 got its last release
1117 # over 10 years ago and is not in Nixpkgs anymore,
1118 # while later versions don't appear to require anything like that.
1119 nameRegex = "[a-zA-Z0-9_.][a-zA-Z0-9_.-]*";
1120 in
1121 {
1122 assertion = builtins.match nameRegex user.name != null;
1123 message = "The username \"${user.name}\" is not valid, it does not match the regex \"${nameRegex}\".";
1124 }
1125 )
1126 {
1127 assertion = (user.hashedPassword != null) -> (match ".*:.*" user.hashedPassword == null);
1128 message = ''
1129 The password hash of user "${user.name}" contains a ":" character.
1130 This is invalid and would break the login system because the fields
1131 of /etc/shadow (file where hashes are stored) are colon-separated.
1132 Please check the value of option `users.users."${user.name}".hashedPassword`.'';
1133 }
1134 {
1135 assertion = user.isNormalUser && user.uid != null -> user.uid >= 1000;
1136 message = ''
1137 A user cannot have a users.users.${user.name}.uid set below 1000 and set users.users.${user.name}.isNormalUser.
1138 Either users.users.${user.name}.isSystemUser must be set to true instead of users.users.${user.name}.isNormalUser
1139 or users.users.${user.name}.uid must be changed to 1000 or above.
1140 '';
1141 }
1142 {
1143 assertion =
1144 let
1145 # we do an extra check on isNormalUser here, to not trigger this assertion when isNormalUser is set and uid to < 1000
1146 isEffectivelySystemUser =
1147 user.isSystemUser || (user.uid != null && user.uid < 1000 && !user.isNormalUser);
1148 in
1149 xor isEffectivelySystemUser user.isNormalUser;
1150 message = ''
1151 Exactly one of users.users.${user.name}.isSystemUser and users.users.${user.name}.isNormalUser must be set.
1152 '';
1153 }
1154 {
1155 assertion = user.group != "";
1156 message = ''
1157 users.users.${user.name}.group is unset. This used to default to
1158 nogroup, but this is unsafe. For example you can create a group
1159 for this user with:
1160 users.users.${user.name}.group = "${user.name}";
1161 users.groups.${user.name} = {};
1162 '';
1163 }
1164 ]
1165 ++ (map
1166 (shell: {
1167 assertion =
1168 !user.ignoreShellProgramCheck
1169 -> (user.shell == pkgs.${shell})
1170 -> (config.programs.${shell}.enable == true);
1171 message = ''
1172 users.users.${user.name}.shell is set to ${shell}, but
1173 programs.${shell}.enable is not true. This will cause the ${shell}
1174 shell to lack the basic nix directories in its PATH and might make
1175 logging in as that user impossible. You can fix it with:
1176 programs.${shell}.enable = true;
1177
1178 If you know what you're doing and you are fine with the behavior,
1179 set users.users.${user.name}.ignoreShellProgramCheck = true;
1180 instead.
1181 '';
1182 })
1183 [
1184 "fish"
1185 "xonsh"
1186 "zsh"
1187 ]
1188 )
1189 )
1190 );
1191
1192 warnings =
1193 flip concatMap (attrValues cfg.users) (
1194 user:
1195 let
1196 passwordOptions = [
1197 "hashedPassword"
1198 "hashedPasswordFile"
1199 "password"
1200 ]
1201 ++ optionals cfg.mutableUsers [
1202 # For immutable users, initialHashedPassword is set to hashedPassword,
1203 # so using these options would always trigger the assertion.
1204 "initialHashedPassword"
1205 "initialPassword"
1206 ];
1207 unambiguousPasswordConfiguration =
1208 1 >= length (filter (x: x != null) (map (flip getAttr user) passwordOptions));
1209 in
1210 optional (!unambiguousPasswordConfiguration) ''
1211 The user '${user.name}' has multiple of the options
1212 `initialHashedPassword`, `hashedPassword`, `initialPassword`, `password`
1213 & `hashedPasswordFile` set to a non-null value.
1214
1215 ${multiplePasswordsWarning}
1216 ${overrideOrderText cfg.mutableUsers}
1217 The values of these options are:
1218 ${concatMapStringsSep "\n" (
1219 value: "* users.users.\"${user.name}\".${value}: ${generators.toPretty { } user.${value}}"
1220 ) passwordOptions}
1221 ''
1222 )
1223 ++ filter (x: x != null) (
1224 flip mapAttrsToList cfg.users (
1225 _: user:
1226 # This regex matches a subset of the Modular Crypto Format (MCF)[1]
1227 # informal standard. Since this depends largely on the OS or the
1228 # specific implementation of crypt(3) we only support the (sane)
1229 # schemes implemented by glibc and BSDs. In particular the original
1230 # DES hash is excluded since, having no structure, it would validate
1231 # common mistakes like typing the plaintext password.
1232 #
1233 # [1]: https://en.wikipedia.org/wiki/Crypt_(C)
1234 let
1235 sep = "\\$";
1236 base64 = "[a-zA-Z0-9./]+";
1237 id = cryptSchemeIdPatternGroup;
1238 name = "[a-z0-9-]+";
1239 value = "[a-zA-Z0-9/+.-]+";
1240 options = "${name}(=${value})?(,${name}=${value})*";
1241 scheme = "${id}(${sep}${options})?";
1242 content = "${base64}${sep}${base64}(${sep}${base64})?";
1243 mcf = "^${sep}${scheme}${sep}${content}$";
1244 in
1245 if
1246 (
1247 allowsLogin user.hashedPassword
1248 && user.hashedPassword != "" # login without password
1249 && match mcf user.hashedPassword == null
1250 )
1251 then
1252 ''
1253 The password hash of user "${user.name}" may be invalid. You must set a
1254 valid hash or the user will be locked out of their account. Please
1255 check the value of option `users.users."${user.name}".hashedPassword`.''
1256 else
1257 null
1258 )
1259 ++ flip mapAttrsToList cfg.users (
1260 name: user:
1261 if user.passwordFile != null then
1262 ''The option `users.users."${name}".passwordFile' has been renamed ''
1263 + ''to `users.users."${name}".hashedPasswordFile'.''
1264 else
1265 null
1266 )
1267 );
1268 };
1269
1270}