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