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}`hashedPasswordFile`
22 controls what password is set for the user.
23 {option}`hashedPassword` overrides both
24 {option}`password` and {option}`hashedPasswordFile`.
25 {option}`password` overrides {option}`hashedPasswordFile`.
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 1000) 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.
105 This automatically sets {option}`group` to `users`,
106 {option}`createHome` to `true`,
107 {option}`home` to {file}`/home/«username»`,
108 {option}`useDefaultShell` to `true`,
109 and {option}`isSystemUser` to `false`.
110 Exactly one of `isNormalUser` and `isSystemUser` must be true.
111 '';
112 };
113
114 group = mkOption {
115 type = types.str;
116 apply = x: assert (builtins.stringLength x < 32 || abort "Group name '${x}' is longer than 31 characters which is not allowed!"); x;
117 default = "";
118 description = lib.mdDoc "The user's primary group.";
119 };
120
121 extraGroups = mkOption {
122 type = types.listOf types.str;
123 default = [];
124 description = lib.mdDoc "The user's auxiliary groups.";
125 };
126
127 home = mkOption {
128 type = types.passwdEntry types.path;
129 default = "/var/empty";
130 description = lib.mdDoc "The user's home directory.";
131 };
132
133 homeMode = mkOption {
134 type = types.strMatching "[0-7]{1,5}";
135 default = "700";
136 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.";
137 };
138
139 cryptHomeLuks = mkOption {
140 type = with types; nullOr str;
141 default = null;
142 description = lib.mdDoc ''
143 Path to encrypted luks device that contains
144 the user's home directory.
145 '';
146 };
147
148 pamMount = mkOption {
149 type = with types; attrsOf str;
150 default = {};
151 description = lib.mdDoc ''
152 Attributes for user's entry in
153 {file}`pam_mount.conf.xml`.
154 Useful attributes might include `path`,
155 `options`, `fstype`, and `server`.
156 See <https://pam-mount.sourceforge.net/pam_mount.conf.5.html>
157 for more information.
158 '';
159 };
160
161 shell = mkOption {
162 type = types.nullOr (types.either types.shellPackage (types.passwdEntry types.path));
163 default = pkgs.shadow;
164 defaultText = literalExpression "pkgs.shadow";
165 example = literalExpression "pkgs.bashInteractive";
166 description = lib.mdDoc ''
167 The path to the user's shell. Can use shell derivations,
168 like `pkgs.bashInteractive`. Don’t
169 forget to enable your shell in
170 `programs` if necessary,
171 like `programs.zsh.enable = true;`.
172 '';
173 };
174
175 ignoreShellProgramCheck = mkOption {
176 type = types.bool;
177 default = false;
178 description = lib.mdDoc ''
179 By default, nixos will check that programs.SHELL.enable is set to
180 true if the user has a custom shell specified. If that behavior isn't
181 required and there are custom overrides in place to make sure that the
182 shell is functional, set this to true.
183 '';
184 };
185
186 subUidRanges = mkOption {
187 type = with types; listOf (submodule subordinateUidRange);
188 default = [];
189 example = [
190 { startUid = 1000; count = 1; }
191 { startUid = 100001; count = 65534; }
192 ];
193 description = lib.mdDoc ''
194 Subordinate user ids that user is allowed to use.
195 They are set into {file}`/etc/subuid` and are used
196 by `newuidmap` for user namespaces.
197 '';
198 };
199
200 subGidRanges = mkOption {
201 type = with types; listOf (submodule subordinateGidRange);
202 default = [];
203 example = [
204 { startGid = 100; count = 1; }
205 { startGid = 1001; count = 999; }
206 ];
207 description = lib.mdDoc ''
208 Subordinate group ids that user is allowed to use.
209 They are set into {file}`/etc/subgid` and are used
210 by `newgidmap` for user namespaces.
211 '';
212 };
213
214 autoSubUidGidRange = mkOption {
215 type = types.bool;
216 default = false;
217 example = true;
218 description = lib.mdDoc ''
219 Automatically allocate subordinate user and group ids for this user.
220 Allocated range is currently always of size 65536.
221 '';
222 };
223
224 createHome = mkOption {
225 type = types.bool;
226 default = false;
227 description = lib.mdDoc ''
228 Whether to create the home directory and ensure ownership as well as
229 permissions to match the user.
230 '';
231 };
232
233 useDefaultShell = mkOption {
234 type = types.bool;
235 default = false;
236 description = lib.mdDoc ''
237 If true, the user's shell will be set to
238 {option}`users.defaultUserShell`.
239 '';
240 };
241
242 hashedPassword = mkOption {
243 type = with types; nullOr (passwdEntry str);
244 default = null;
245 description = lib.mdDoc ''
246 Specifies the hashed password for the user.
247 ${passwordDescription}
248 ${hashedPasswordDescription}
249 '';
250 };
251
252 password = mkOption {
253 type = with types; nullOr str;
254 default = null;
255 description = lib.mdDoc ''
256 Specifies the (clear text) password for the user.
257 Warning: do not set confidential information here
258 because it is world-readable in the Nix store. This option
259 should only be used for public accounts.
260 ${passwordDescription}
261 '';
262 };
263
264 hashedPasswordFile = mkOption {
265 type = with types; nullOr str;
266 default = cfg.users.${name}.passwordFile;
267 defaultText = literalExpression "null";
268 description = lib.mdDoc ''
269 The full path to a file that contains the hash of the user's
270 password. The password file is read on each system activation. The
271 file should contain exactly one line, which should be the password in
272 an encrypted form that is suitable for the `chpasswd -e` command.
273 ${passwordDescription}
274 '';
275 };
276
277 passwordFile = mkOption {
278 type = with types; nullOr str;
279 default = null;
280 visible = false;
281 description = lib.mdDoc "Deprecated alias of hashedPasswordFile";
282 };
283
284 initialHashedPassword = mkOption {
285 type = with types; nullOr (passwdEntry str);
286 default = null;
287 description = lib.mdDoc ''
288 Specifies the initial hashed password for the user, i.e. the
289 hashed password assigned if the user does not already
290 exist. If {option}`users.mutableUsers` is true, the
291 password can be changed subsequently using the
292 {command}`passwd` command. Otherwise, it's
293 equivalent to setting the {option}`hashedPassword` option.
294
295 Note that the {option}`hashedPassword` option will override
296 this option if both are set.
297
298 ${hashedPasswordDescription}
299 '';
300 };
301
302 initialPassword = mkOption {
303 type = with types; nullOr str;
304 default = null;
305 description = lib.mdDoc ''
306 Specifies the initial password for the user, i.e. the
307 password assigned if the user does not already exist. If
308 {option}`users.mutableUsers` is true, the password
309 can be changed subsequently using the
310 {command}`passwd` command. Otherwise, it's
311 equivalent to setting the {option}`password`
312 option. The same caveat applies: the password specified here
313 is world-readable in the Nix store, so it should only be
314 used for guest accounts or passwords that will be changed
315 promptly.
316
317 Note that the {option}`password` option will override this
318 option if both are set.
319 '';
320 };
321
322 packages = mkOption {
323 type = types.listOf types.package;
324 default = [];
325 example = literalExpression "[ pkgs.firefox pkgs.thunderbird ]";
326 description = lib.mdDoc ''
327 The set of packages that should be made available to the user.
328 This is in contrast to {option}`environment.systemPackages`,
329 which adds packages to all users.
330 '';
331 };
332
333 expires = mkOption {
334 type = types.nullOr (types.strMatching "[[:digit:]]{4}-[[:digit:]]{2}-[[:digit:]]{2}");
335 default = null;
336 description = lib.mdDoc ''
337 Set the date on which the user's account will no longer be
338 accessible. The date is expressed in the format YYYY-MM-DD, or null
339 to disable the expiry.
340 A user whose account is locked must contact the system
341 administrator before being able to use the system again.
342 '';
343 };
344
345 linger = mkOption {
346 type = types.bool;
347 default = false;
348 description = lib.mdDoc ''
349 Whether to enable lingering for this user. If true, systemd user
350 units will start at boot, rather than starting at login and stopping
351 at logout. This is the declarative equivalent of running
352 `loginctl enable-linger` for this user.
353
354 If false, user units will not be started until the user logs in, and
355 may be stopped on logout depending on the settings in `logind.conf`.
356 '';
357 };
358 };
359
360 config = mkMerge
361 [ { name = mkDefault name;
362 shell = mkIf config.useDefaultShell (mkDefault cfg.defaultUserShell);
363 }
364 (mkIf config.isNormalUser {
365 group = mkDefault "users";
366 createHome = mkDefault true;
367 home = mkDefault "/home/${config.name}";
368 homeMode = mkDefault "700";
369 useDefaultShell = mkDefault true;
370 isSystemUser = mkDefault false;
371 })
372 # If !mutableUsers, setting ‘initialPassword’ is equivalent to
373 # setting ‘password’ (and similarly for hashed passwords).
374 (mkIf (!cfg.mutableUsers && config.initialPassword != null) {
375 password = mkDefault config.initialPassword;
376 })
377 (mkIf (!cfg.mutableUsers && config.initialHashedPassword != null) {
378 hashedPassword = mkDefault config.initialHashedPassword;
379 })
380 (mkIf (config.isNormalUser && config.subUidRanges == [] && config.subGidRanges == []) {
381 autoSubUidGidRange = mkDefault true;
382 })
383 ];
384
385 };
386
387 groupOpts = { name, config, ... }: {
388
389 options = {
390
391 name = mkOption {
392 type = types.passwdEntry types.str;
393 description = lib.mdDoc ''
394 The name of the group. If undefined, the name of the attribute set
395 will be used.
396 '';
397 };
398
399 gid = mkOption {
400 type = with types; nullOr int;
401 default = null;
402 description = lib.mdDoc ''
403 The group GID. If the GID is null, a free GID is picked on
404 activation.
405 '';
406 };
407
408 members = mkOption {
409 type = with types; listOf (passwdEntry str);
410 default = [];
411 description = lib.mdDoc ''
412 The user names of the group members, added to the
413 `/etc/group` file.
414 '';
415 };
416
417 };
418
419 config = {
420 name = mkDefault name;
421
422 members = mapAttrsToList (n: u: u.name) (
423 filterAttrs (n: u: elem config.name u.extraGroups) cfg.users
424 );
425 };
426
427 };
428
429 subordinateUidRange = {
430 options = {
431 startUid = mkOption {
432 type = types.int;
433 description = lib.mdDoc ''
434 Start of the range of subordinate user ids that user is
435 allowed to use.
436 '';
437 };
438 count = mkOption {
439 type = types.int;
440 default = 1;
441 description = lib.mdDoc "Count of subordinate user ids";
442 };
443 };
444 };
445
446 subordinateGidRange = {
447 options = {
448 startGid = mkOption {
449 type = types.int;
450 description = lib.mdDoc ''
451 Start of the range of subordinate group ids that user is
452 allowed to use.
453 '';
454 };
455 count = mkOption {
456 type = types.int;
457 default = 1;
458 description = lib.mdDoc "Count of subordinate group ids";
459 };
460 };
461 };
462
463 idsAreUnique = set: idAttr: !(foldr (name: args@{ dup, acc }:
464 let
465 id = builtins.toString (builtins.getAttr idAttr (builtins.getAttr name set));
466 exists = builtins.hasAttr id acc;
467 newAcc = acc // (builtins.listToAttrs [ { name = id; value = true; } ]);
468 in if dup then args else if exists
469 then builtins.trace "Duplicate ${idAttr} ${id}" { dup = true; acc = null; }
470 else { dup = false; acc = newAcc; }
471 ) { dup = false; acc = {}; } (builtins.attrNames set)).dup;
472
473 uidsAreUnique = idsAreUnique (filterAttrs (n: u: u.uid != null) cfg.users) "uid";
474 gidsAreUnique = idsAreUnique (filterAttrs (n: g: g.gid != null) cfg.groups) "gid";
475 sdInitrdUidsAreUnique = idsAreUnique (filterAttrs (n: u: u.uid != null) config.boot.initrd.systemd.users) "uid";
476 sdInitrdGidsAreUnique = idsAreUnique (filterAttrs (n: g: g.gid != null) config.boot.initrd.systemd.groups) "gid";
477 groupNames = lib.mapAttrsToList (n: g: g.name) cfg.groups;
478 usersWithoutExistingGroup = lib.filterAttrs (n: u: !lib.elem u.group groupNames) cfg.users;
479
480 spec = pkgs.writeText "users-groups.json" (builtins.toJSON {
481 inherit (cfg) mutableUsers;
482 users = mapAttrsToList (_: u:
483 { inherit (u)
484 name uid group description home homeMode createHome isSystemUser
485 password hashedPasswordFile hashedPassword
486 autoSubUidGidRange subUidRanges subGidRanges
487 initialPassword initialHashedPassword expires;
488 shell = utils.toShellPath u.shell;
489 }) cfg.users;
490 groups = attrValues cfg.groups;
491 });
492
493 systemShells =
494 let
495 shells = mapAttrsToList (_: u: u.shell) cfg.users;
496 in
497 filter types.shellPackage.check shells;
498
499in {
500 imports = [
501 (mkAliasOptionModuleMD [ "users" "extraUsers" ] [ "users" "users" ])
502 (mkAliasOptionModuleMD [ "users" "extraGroups" ] [ "users" "groups" ])
503 (mkRenamedOptionModule ["security" "initialRootPassword"] ["users" "users" "root" "initialHashedPassword"])
504 ];
505
506 ###### interface
507 options = {
508
509 users.mutableUsers = mkOption {
510 type = types.bool;
511 default = true;
512 description = lib.mdDoc ''
513 If set to `true`, you are free to add new users and groups to the system
514 with the ordinary `useradd` and
515 `groupadd` commands. On system activation, the
516 existing contents of the `/etc/passwd` and
517 `/etc/group` files will be merged with the
518 contents generated from the `users.users` and
519 `users.groups` options.
520 The initial password for a user will be set
521 according to `users.users`, but existing passwords
522 will not be changed.
523
524 ::: {.warning}
525 If set to `false`, the contents of the user and
526 group files will simply be replaced on system activation. This also
527 holds for the user passwords; all changed
528 passwords will be reset according to the
529 `users.users` configuration on activation.
530 :::
531 '';
532 };
533
534 users.enforceIdUniqueness = mkOption {
535 type = types.bool;
536 default = true;
537 description = lib.mdDoc ''
538 Whether to require that no two users/groups share the same uid/gid.
539 '';
540 };
541
542 users.users = mkOption {
543 default = {};
544 type = with types; attrsOf (submodule userOpts);
545 example = {
546 alice = {
547 uid = 1234;
548 description = "Alice Q. User";
549 home = "/home/alice";
550 createHome = true;
551 group = "users";
552 extraGroups = ["wheel"];
553 shell = "/bin/sh";
554 };
555 };
556 description = lib.mdDoc ''
557 Additional user accounts to be created automatically by the system.
558 This can also be used to set options for root.
559 '';
560 };
561
562 users.groups = mkOption {
563 default = {};
564 example =
565 { students.gid = 1001;
566 hackers = { };
567 };
568 type = with types; attrsOf (submodule groupOpts);
569 description = lib.mdDoc ''
570 Additional groups to be created automatically by the system.
571 '';
572 };
573
574
575 users.allowNoPasswordLogin = mkOption {
576 type = types.bool;
577 default = false;
578 description = lib.mdDoc ''
579 Disable checking that at least the `root` user or a user in the `wheel` group can log in using
580 a password or an SSH key.
581
582 WARNING: enabling this can lock you out of your system. Enable this only if you know what are you doing.
583 '';
584 };
585
586 # systemd initrd
587 boot.initrd.systemd.users = mkOption {
588 description = ''
589 Users to include in initrd.
590 '';
591 default = {};
592 type = types.attrsOf (types.submodule ({ name, ... }: {
593 options.uid = mkOption {
594 type = types.int;
595 description = ''
596 ID of the user in initrd.
597 '';
598 defaultText = literalExpression "config.users.users.\${name}.uid";
599 default = cfg.users.${name}.uid;
600 };
601 options.group = mkOption {
602 type = types.singleLineStr;
603 description = ''
604 Group the user belongs to in initrd.
605 '';
606 defaultText = literalExpression "config.users.users.\${name}.group";
607 default = cfg.users.${name}.group;
608 };
609 options.shell = mkOption {
610 type = types.passwdEntry types.path;
611 description = ''
612 The path to the user's shell in initrd.
613 '';
614 default = "${pkgs.shadow}/bin/nologin";
615 defaultText = literalExpression "\${pkgs.shadow}/bin/nologin";
616 };
617 }));
618 };
619
620 boot.initrd.systemd.groups = mkOption {
621 description = ''
622 Groups to include in initrd.
623 '';
624 default = {};
625 type = types.attrsOf (types.submodule ({ name, ... }: {
626 options.gid = mkOption {
627 type = types.int;
628 description = ''
629 ID of the group in initrd.
630 '';
631 defaultText = literalExpression "config.users.groups.\${name}.gid";
632 default = cfg.groups.${name}.gid;
633 };
634 }));
635 };
636 };
637
638
639 ###### implementation
640
641 config = let
642 cryptSchemeIdPatternGroup = "(${lib.concatStringsSep "|" pkgs.libxcrypt.enabledCryptSchemeIds})";
643 in {
644
645 users.users = {
646 root = {
647 uid = ids.uids.root;
648 description = "System administrator";
649 home = "/root";
650 shell = mkDefault cfg.defaultUserShell;
651 group = "root";
652 initialHashedPassword = mkDefault "!";
653 };
654 nobody = {
655 uid = ids.uids.nobody;
656 isSystemUser = true;
657 description = "Unprivileged account (don't use!)";
658 group = "nogroup";
659 };
660 };
661
662 users.groups = {
663 root.gid = ids.gids.root;
664 wheel.gid = ids.gids.wheel;
665 disk.gid = ids.gids.disk;
666 kmem.gid = ids.gids.kmem;
667 tty.gid = ids.gids.tty;
668 floppy.gid = ids.gids.floppy;
669 uucp.gid = ids.gids.uucp;
670 lp.gid = ids.gids.lp;
671 cdrom.gid = ids.gids.cdrom;
672 tape.gid = ids.gids.tape;
673 audio.gid = ids.gids.audio;
674 video.gid = ids.gids.video;
675 dialout.gid = ids.gids.dialout;
676 nogroup.gid = ids.gids.nogroup;
677 users.gid = ids.gids.users;
678 nixbld.gid = ids.gids.nixbld;
679 utmp.gid = ids.gids.utmp;
680 adm.gid = ids.gids.adm;
681 input.gid = ids.gids.input;
682 kvm.gid = ids.gids.kvm;
683 render.gid = ids.gids.render;
684 sgx.gid = ids.gids.sgx;
685 shadow.gid = ids.gids.shadow;
686 };
687
688 system.activationScripts.users = {
689 supportsDryActivation = true;
690 text = ''
691 install -m 0700 -d /root
692 install -m 0755 -d /home
693
694 ${pkgs.perl.withPackages (p: [ p.FileSlurp p.JSON ])}/bin/perl \
695 -w ${./update-users-groups.pl} ${spec}
696 '';
697 };
698
699 system.activationScripts.update-lingering = let
700 lingerDir = "/var/lib/systemd/linger";
701 lingeringUsers = map (u: u.name) (attrValues (flip filterAttrs cfg.users (n: u: u.linger)));
702 lingeringUsersFile = builtins.toFile "lingering-users"
703 (concatStrings (map (s: "${s}\n")
704 (sort (a: b: a < b) lingeringUsers))); # this sorting is important for `comm` to work correctly
705 in stringAfter [ "users" ] ''
706 if [ -e ${lingerDir} ] ; then
707 cd ${lingerDir}
708 ls ${lingerDir} | sort | comm -3 -1 ${lingeringUsersFile} - | xargs -r ${pkgs.systemd}/bin/loginctl disable-linger
709 ls ${lingerDir} | sort | comm -3 -2 ${lingeringUsersFile} - | xargs -r ${pkgs.systemd}/bin/loginctl enable-linger
710 fi
711 '';
712
713 # Warn about user accounts with deprecated password hashing schemes
714 system.activationScripts.hashes = {
715 deps = [ "users" ];
716 text = ''
717 users=()
718 while IFS=: read -r user hash _; do
719 if [[ "$hash" = "$"* && ! "$hash" =~ ^\''$${cryptSchemeIdPatternGroup}\$ ]]; then
720 users+=("$user")
721 fi
722 done </etc/shadow
723
724 if (( "''${#users[@]}" )); then
725 echo "
726 WARNING: The following user accounts rely on password hashing algorithms
727 that have been removed. They need to be renewed as soon as possible, as
728 they do prevent their users from logging in."
729 printf ' - %s\n' "''${users[@]}"
730 fi
731 '';
732 };
733
734 # for backwards compatibility
735 system.activationScripts.groups = stringAfter [ "users" ] "";
736
737 # Install all the user shells
738 environment.systemPackages = systemShells;
739
740 environment.etc = mapAttrs' (_: { packages, name, ... }: {
741 name = "profiles/per-user/${name}";
742 value.source = pkgs.buildEnv {
743 name = "user-environment";
744 paths = packages;
745 inherit (config.environment) pathsToLink extraOutputsToInstall;
746 inherit (config.system.path) ignoreCollisions postBuild;
747 };
748 }) (filterAttrs (_: u: u.packages != []) cfg.users);
749
750 environment.profiles = [
751 "$HOME/.nix-profile"
752 "\${XDG_STATE_HOME}/nix/profile"
753 "$HOME/.local/state/nix/profile"
754 "/etc/profiles/per-user/$USER"
755 ];
756
757 # systemd initrd
758 boot.initrd.systemd = lib.mkIf config.boot.initrd.systemd.enable {
759 contents = {
760 "/etc/passwd".text = ''
761 ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: { uid, group, shell }: let
762 g = config.boot.initrd.systemd.groups.${group};
763 in "${n}:x:${toString uid}:${toString g.gid}::/var/empty:${shell}") config.boot.initrd.systemd.users)}
764 '';
765 "/etc/group".text = ''
766 ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: { gid }: "${n}:x:${toString gid}:") config.boot.initrd.systemd.groups)}
767 '';
768 "/etc/shells".text = lib.concatStringsSep "\n" (lib.unique (lib.mapAttrsToList (_: u: u.shell) config.boot.initrd.systemd.users)) + "\n";
769 };
770
771 storePaths = [ "${pkgs.shadow}/bin/nologin" ];
772
773 users = {
774 root = { shell = lib.mkDefault "/bin/bash"; };
775 nobody = {};
776 };
777
778 groups = {
779 root = {};
780 nogroup = {};
781 systemd-journal = {};
782 tty = {};
783 dialout = {};
784 kmem = {};
785 input = {};
786 video = {};
787 render = {};
788 sgx = {};
789 audio = {};
790 video = {};
791 lp = {};
792 disk = {};
793 cdrom = {};
794 tape = {};
795 kvm = {};
796 };
797 };
798
799 assertions = [
800 { assertion = !cfg.enforceIdUniqueness || (uidsAreUnique && gidsAreUnique);
801 message = "UIDs and GIDs must be unique!";
802 }
803 { assertion = !cfg.enforceIdUniqueness || (sdInitrdUidsAreUnique && sdInitrdGidsAreUnique);
804 message = "systemd initrd UIDs and GIDs must be unique!";
805 }
806 { assertion = usersWithoutExistingGroup == {};
807 message =
808 let
809 errUsers = lib.attrNames usersWithoutExistingGroup;
810 missingGroups = lib.unique (lib.mapAttrsToList (n: u: u.group) usersWithoutExistingGroup);
811 mkConfigHint = group: "users.groups.${group} = {};";
812 in ''
813 The following users have a primary group that is undefined: ${lib.concatStringsSep " " errUsers}
814 Hint: Add this to your NixOS configuration:
815 ${lib.concatStringsSep "\n " (map mkConfigHint missingGroups)}
816 '';
817 }
818 { # If mutableUsers is false, to prevent users creating a
819 # configuration that locks them out of the system, ensure that
820 # there is at least one "privileged" account that has a
821 # password or an SSH authorized key. Privileged accounts are
822 # root and users in the wheel group.
823 # The check does not apply when users.disableLoginPossibilityAssertion
824 # The check does not apply when users.mutableUsers
825 assertion = !cfg.mutableUsers -> !cfg.allowNoPasswordLogin ->
826 any id (mapAttrsToList (name: cfg:
827 (name == "root"
828 || cfg.group == "wheel"
829 || elem "wheel" cfg.extraGroups)
830 &&
831 (allowsLogin cfg.hashedPassword
832 || cfg.password != null
833 || cfg.hashedPasswordFile != null
834 || cfg.openssh.authorizedKeys.keys != []
835 || cfg.openssh.authorizedKeys.keyFiles != [])
836 ) cfg.users ++ [
837 config.security.googleOsLogin.enable
838 ]);
839 message = ''
840 Neither the root account nor any wheel user has a password or SSH authorized key.
841 You must set one to prevent being locked out of your system.
842 If you really want to be locked out of your system, set users.allowNoPasswordLogin = true;
843 However you are most probably better off by setting users.mutableUsers = true; and
844 manually running passwd root to set the root password.
845 '';
846 }
847 ] ++ flatten (flip mapAttrsToList cfg.users (name: user:
848 [
849 {
850 assertion = (user.hashedPassword != null)
851 -> (builtins.match ".*:.*" user.hashedPassword == null);
852 message = ''
853 The password hash of user "${user.name}" contains a ":" character.
854 This is invalid and would break the login system because the fields
855 of /etc/shadow (file where hashes are stored) are colon-separated.
856 Please check the value of option `users.users."${user.name}".hashedPassword`.'';
857 }
858 {
859 assertion = let
860 xor = a: b: a && !b || b && !a;
861 isEffectivelySystemUser = user.isSystemUser || (user.uid != null && user.uid < 1000);
862 in xor isEffectivelySystemUser user.isNormalUser;
863 message = ''
864 Exactly one of users.users.${user.name}.isSystemUser and users.users.${user.name}.isNormalUser must be set.
865 '';
866 }
867 {
868 assertion = user.group != "";
869 message = ''
870 users.users.${user.name}.group is unset. This used to default to
871 nogroup, but this is unsafe. For example you can create a group
872 for this user with:
873 users.users.${user.name}.group = "${user.name}";
874 users.groups.${user.name} = {};
875 '';
876 }
877 ] ++ (map (shell: {
878 assertion = !user.ignoreShellProgramCheck -> (user.shell == pkgs.${shell}) -> (config.programs.${shell}.enable == true);
879 message = ''
880 users.users.${user.name}.shell is set to ${shell}, but
881 programs.${shell}.enable is not true. This will cause the ${shell}
882 shell to lack the basic nix directories in its PATH and might make
883 logging in as that user impossible. You can fix it with:
884 programs.${shell}.enable = true;
885
886 If you know what you're doing and you are fine with the behavior,
887 set users.users.${user.name}.ignoreShellProgramCheck = true;
888 instead.
889 '';
890 }) [
891 "fish"
892 "xonsh"
893 "zsh"
894 ])
895 ));
896
897 warnings =
898 builtins.filter (x: x != null) (
899 flip mapAttrsToList cfg.users (_: user:
900 # This regex matches a subset of the Modular Crypto Format (MCF)[1]
901 # informal standard. Since this depends largely on the OS or the
902 # specific implementation of crypt(3) we only support the (sane)
903 # schemes implemented by glibc and BSDs. In particular the original
904 # DES hash is excluded since, having no structure, it would validate
905 # common mistakes like typing the plaintext password.
906 #
907 # [1]: https://en.wikipedia.org/wiki/Crypt_(C)
908 let
909 sep = "\\$";
910 base64 = "[a-zA-Z0-9./]+";
911 id = cryptSchemeIdPatternGroup;
912 name = "[a-z0-9-]+";
913 value = "[a-zA-Z0-9/+.-]+";
914 options = "${name}(=${value})?(,${name}=${value})*";
915 scheme = "${id}(${sep}${options})?";
916 content = "${base64}${sep}${base64}(${sep}${base64})?";
917 mcf = "^${sep}${scheme}${sep}${content}$";
918 in
919 if (allowsLogin user.hashedPassword
920 && user.hashedPassword != "" # login without password
921 && builtins.match mcf user.hashedPassword == null)
922 then ''
923 The password hash of user "${user.name}" may be invalid. You must set a
924 valid hash or the user will be locked out of their account. Please
925 check the value of option `users.users."${user.name}".hashedPassword`.''
926 else null)
927 ++ flip mapAttrsToList cfg.users (name: user:
928 if user.passwordFile != null then
929 ''The option `users.users."${name}".passwordFile' has been renamed '' +
930 ''to `users.users."${name}".hashedPasswordFile'.''
931 else null)
932 );
933 };
934
935}