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 = ''
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 = ''
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 = ''
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 = ''
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 = ''
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 = "The user's primary group.";
119 };
120
121 extraGroups = mkOption {
122 type = types.listOf types.str;
123 default = [];
124 description = "The user's auxiliary groups.";
125 };
126
127 home = mkOption {
128 type = types.passwdEntry types.path;
129 default = "/var/empty";
130 description = "The user's home directory.";
131 };
132
133 homeMode = mkOption {
134 type = types.strMatching "[0-7]{1,5}";
135 default = "700";
136 description = "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 = ''
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 = ''
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 = ''
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 = ''
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 = ''
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 = ''
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 = ''
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 = ''
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 = ''
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 = ''
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 = ''
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 = ''
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 = "Deprecated alias of hashedPasswordFile";
282 };
283
284 initialHashedPassword = mkOption {
285 type = with types; nullOr (passwdEntry str);
286 default = null;
287 description = ''
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 = ''
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 = ''
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 = ''
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 = ''
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 = ''
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 = ''
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 = ''
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 = ''
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 = "Count of subordinate user ids";
442 };
443 };
444 };
445
446 subordinateGidRange = {
447 options = {
448 startGid = mkOption {
449 type = types.int;
450 description = ''
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 = "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: u.group != "" && !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
499 lingeringUsers = map (u: u.name) (attrValues (flip filterAttrs cfg.users (n: u: u.linger)));
500in {
501 imports = [
502 (mkAliasOptionModuleMD [ "users" "extraUsers" ] [ "users" "users" ])
503 (mkAliasOptionModuleMD [ "users" "extraGroups" ] [ "users" "groups" ])
504 (mkRenamedOptionModule ["security" "initialRootPassword"] ["users" "users" "root" "initialHashedPassword"])
505 ];
506
507 ###### interface
508 options = {
509
510 users.mutableUsers = mkOption {
511 type = types.bool;
512 default = true;
513 description = ''
514 If set to `true`, you are free to add new users and groups to the system
515 with the ordinary `useradd` and
516 `groupadd` commands. On system activation, the
517 existing contents of the `/etc/passwd` and
518 `/etc/group` files will be merged with the
519 contents generated from the `users.users` and
520 `users.groups` options.
521 The initial password for a user will be set
522 according to `users.users`, but existing passwords
523 will not be changed.
524
525 ::: {.warning}
526 If set to `false`, the contents of the user and
527 group files will simply be replaced on system activation. This also
528 holds for the user passwords; all changed
529 passwords will be reset according to the
530 `users.users` configuration on activation.
531 :::
532 '';
533 };
534
535 users.enforceIdUniqueness = mkOption {
536 type = types.bool;
537 default = true;
538 description = ''
539 Whether to require that no two users/groups share the same uid/gid.
540 '';
541 };
542
543 users.users = mkOption {
544 default = {};
545 type = with types; attrsOf (submodule userOpts);
546 example = {
547 alice = {
548 uid = 1234;
549 description = "Alice Q. User";
550 home = "/home/alice";
551 createHome = true;
552 group = "users";
553 extraGroups = ["wheel"];
554 shell = "/bin/sh";
555 };
556 };
557 description = ''
558 Additional user accounts to be created automatically by the system.
559 This can also be used to set options for root.
560 '';
561 };
562
563 users.groups = mkOption {
564 default = {};
565 example =
566 { students.gid = 1001;
567 hackers = { };
568 };
569 type = with types; attrsOf (submodule groupOpts);
570 description = ''
571 Additional groups to be created automatically by the system.
572 '';
573 };
574
575
576 users.allowNoPasswordLogin = mkOption {
577 type = types.bool;
578 default = false;
579 description = ''
580 Disable checking that at least the `root` user or a user in the `wheel` group can log in using
581 a password or an SSH key.
582
583 WARNING: enabling this can lock you out of your system. Enable this only if you know what are you doing.
584 '';
585 };
586
587 # systemd initrd
588 boot.initrd.systemd.users = mkOption {
589 description = ''
590 Users to include in initrd.
591 '';
592 default = {};
593 type = types.attrsOf (types.submodule ({ name, ... }: {
594 options.uid = mkOption {
595 type = types.int;
596 description = ''
597 ID of the user in initrd.
598 '';
599 defaultText = literalExpression "config.users.users.\${name}.uid";
600 default = cfg.users.${name}.uid;
601 };
602 options.group = mkOption {
603 type = types.singleLineStr;
604 description = ''
605 Group the user belongs to in initrd.
606 '';
607 defaultText = literalExpression "config.users.users.\${name}.group";
608 default = cfg.users.${name}.group;
609 };
610 options.shell = mkOption {
611 type = types.passwdEntry types.path;
612 description = ''
613 The path to the user's shell in initrd.
614 '';
615 default = "${pkgs.shadow}/bin/nologin";
616 defaultText = literalExpression "\${pkgs.shadow}/bin/nologin";
617 };
618 }));
619 };
620
621 boot.initrd.systemd.groups = mkOption {
622 description = ''
623 Groups to include in initrd.
624 '';
625 default = {};
626 type = types.attrsOf (types.submodule ({ name, ... }: {
627 options.gid = mkOption {
628 type = types.int;
629 description = ''
630 ID of the group in initrd.
631 '';
632 defaultText = literalExpression "config.users.groups.\${name}.gid";
633 default = cfg.groups.${name}.gid;
634 };
635 }));
636 };
637 };
638
639
640 ###### implementation
641
642 config = let
643 cryptSchemeIdPatternGroup = "(${lib.concatStringsSep "|" pkgs.libxcrypt.enabledCryptSchemeIds})";
644 in {
645
646 users.users = {
647 root = {
648 uid = ids.uids.root;
649 description = "System administrator";
650 home = "/root";
651 shell = mkDefault cfg.defaultUserShell;
652 group = "root";
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 = if !config.systemd.sysusers.enable then {
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 } else ""; # keep around for backwards compatibility
698
699 systemd.services.linger-users = lib.mkIf ((builtins.length lingeringUsers) > 0) {
700 wantedBy = ["multi-user.target"];
701 after = ["systemd-logind.service"];
702 requires = ["systemd-logind.service"];
703
704 script = let
705 lingerDir = "/var/lib/systemd/linger";
706 lingeringUsersFile = builtins.toFile "lingering-users"
707 (concatStrings (map (s: "${s}\n")
708 (sort (a: b: a < b) lingeringUsers))); # this sorting is important for `comm` to work correctly
709 in ''
710 mkdir -vp ${lingerDir}
711 cd ${lingerDir}
712 for user in $(ls); do
713 if ! id "$user" >/dev/null; then
714 echo "Removing linger for missing user $user"
715 rm --force -- "$user"
716 fi
717 done
718 ls | sort | comm -3 -1 ${lingeringUsersFile} - | xargs -r ${pkgs.systemd}/bin/loginctl disable-linger
719 ls | sort | comm -3 -2 ${lingeringUsersFile} - | xargs -r ${pkgs.systemd}/bin/loginctl enable-linger
720 '';
721
722 serviceConfig.Type = "oneshot";
723 };
724
725 # Warn about user accounts with deprecated password hashing schemes
726 # This does not work when the users and groups are created by
727 # systemd-sysusers because the users are created too late then.
728 system.activationScripts.hashes = if !config.systemd.sysusers.enable then {
729 deps = [ "users" ];
730 text = ''
731 users=()
732 while IFS=: read -r user hash _; do
733 if [[ "$hash" = "$"* && ! "$hash" =~ ^\''$${cryptSchemeIdPatternGroup}\$ ]]; then
734 users+=("$user")
735 fi
736 done </etc/shadow
737
738 if (( "''${#users[@]}" )); then
739 echo "
740 WARNING: The following user accounts rely on password hashing algorithms
741 that have been removed. They need to be renewed as soon as possible, as
742 they do prevent their users from logging in."
743 printf ' - %s\n' "''${users[@]}"
744 fi
745 '';
746 } else ""; # keep around for backwards compatibility
747
748 # for backwards compatibility
749 system.activationScripts.groups = stringAfter [ "users" ] "";
750
751 # Install all the user shells
752 environment.systemPackages = systemShells;
753
754 environment.etc = mapAttrs' (_: { packages, name, ... }: {
755 name = "profiles/per-user/${name}";
756 value.source = pkgs.buildEnv {
757 name = "user-environment";
758 paths = packages;
759 inherit (config.environment) pathsToLink extraOutputsToInstall;
760 inherit (config.system.path) ignoreCollisions postBuild;
761 };
762 }) (filterAttrs (_: u: u.packages != []) cfg.users);
763
764 environment.profiles = [
765 "$HOME/.nix-profile"
766 "\${XDG_STATE_HOME}/nix/profile"
767 "$HOME/.local/state/nix/profile"
768 "/etc/profiles/per-user/$USER"
769 ];
770
771 # systemd initrd
772 boot.initrd.systemd = lib.mkIf config.boot.initrd.systemd.enable {
773 contents = {
774 "/etc/passwd".text = ''
775 ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: { uid, group, shell }: let
776 g = config.boot.initrd.systemd.groups.${group};
777 in "${n}:x:${toString uid}:${toString g.gid}::/var/empty:${shell}") config.boot.initrd.systemd.users)}
778 '';
779 "/etc/group".text = ''
780 ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: { gid }: "${n}:x:${toString gid}:") config.boot.initrd.systemd.groups)}
781 '';
782 "/etc/shells".text = lib.concatStringsSep "\n" (lib.unique (lib.mapAttrsToList (_: u: u.shell) config.boot.initrd.systemd.users)) + "\n";
783 };
784
785 storePaths = [ "${pkgs.shadow}/bin/nologin" ];
786
787 users = {
788 root = { shell = lib.mkDefault "/bin/bash"; };
789 nobody = {};
790 };
791
792 groups = {
793 root = {};
794 nogroup = {};
795 systemd-journal = {};
796 tty = {};
797 dialout = {};
798 kmem = {};
799 input = {};
800 video = {};
801 render = {};
802 sgx = {};
803 audio = {};
804 video = {};
805 lp = {};
806 disk = {};
807 cdrom = {};
808 tape = {};
809 kvm = {};
810 };
811 };
812
813 assertions = [
814 { assertion = !cfg.enforceIdUniqueness || (uidsAreUnique && gidsAreUnique);
815 message = "UIDs and GIDs must be unique!";
816 }
817 { assertion = !cfg.enforceIdUniqueness || (sdInitrdUidsAreUnique && sdInitrdGidsAreUnique);
818 message = "systemd initrd UIDs and GIDs must be unique!";
819 }
820 { assertion = usersWithoutExistingGroup == {};
821 message =
822 let
823 errUsers = lib.attrNames usersWithoutExistingGroup;
824 missingGroups = lib.unique (lib.mapAttrsToList (n: u: u.group) usersWithoutExistingGroup);
825 mkConfigHint = group: "users.groups.${group} = {};";
826 in ''
827 The following users have a primary group that is undefined: ${lib.concatStringsSep " " errUsers}
828 Hint: Add this to your NixOS configuration:
829 ${lib.concatStringsSep "\n " (map mkConfigHint missingGroups)}
830 '';
831 }
832 { # If mutableUsers is false, to prevent users creating a
833 # configuration that locks them out of the system, ensure that
834 # there is at least one "privileged" account that has a
835 # password or an SSH authorized key. Privileged accounts are
836 # root and users in the wheel group.
837 # The check does not apply when users.disableLoginPossibilityAssertion
838 # The check does not apply when users.mutableUsers
839 assertion = !cfg.mutableUsers -> !cfg.allowNoPasswordLogin ->
840 any id (mapAttrsToList (name: cfg:
841 (name == "root"
842 || cfg.group == "wheel"
843 || elem "wheel" cfg.extraGroups)
844 &&
845 (allowsLogin cfg.hashedPassword
846 || cfg.password != null
847 || cfg.hashedPasswordFile != null
848 || cfg.openssh.authorizedKeys.keys != []
849 || cfg.openssh.authorizedKeys.keyFiles != [])
850 ) cfg.users ++ [
851 config.security.googleOsLogin.enable
852 ]);
853 message = ''
854 Neither the root account nor any wheel user has a password or SSH authorized key.
855 You must set one to prevent being locked out of your system.
856 If you really want to be locked out of your system, set users.allowNoPasswordLogin = true;
857 However you are most probably better off by setting users.mutableUsers = true; and
858 manually running passwd root to set the root password.
859 '';
860 }
861 ] ++ flatten (flip mapAttrsToList cfg.users (name: user:
862 [
863 {
864 assertion = (user.hashedPassword != null)
865 -> (builtins.match ".*:.*" user.hashedPassword == null);
866 message = ''
867 The password hash of user "${user.name}" contains a ":" character.
868 This is invalid and would break the login system because the fields
869 of /etc/shadow (file where hashes are stored) are colon-separated.
870 Please check the value of option `users.users."${user.name}".hashedPassword`.'';
871 }
872 {
873 assertion = let
874 isEffectivelySystemUser = user.isSystemUser || (user.uid != null && user.uid < 1000);
875 in xor isEffectivelySystemUser user.isNormalUser;
876 message = ''
877 Exactly one of users.users.${user.name}.isSystemUser and users.users.${user.name}.isNormalUser must be set.
878 '';
879 }
880 {
881 assertion = user.group != "";
882 message = ''
883 users.users.${user.name}.group is unset. This used to default to
884 nogroup, but this is unsafe. For example you can create a group
885 for this user with:
886 users.users.${user.name}.group = "${user.name}";
887 users.groups.${user.name} = {};
888 '';
889 }
890 ] ++ (map (shell: {
891 assertion = !user.ignoreShellProgramCheck -> (user.shell == pkgs.${shell}) -> (config.programs.${shell}.enable == true);
892 message = ''
893 users.users.${user.name}.shell is set to ${shell}, but
894 programs.${shell}.enable is not true. This will cause the ${shell}
895 shell to lack the basic nix directories in its PATH and might make
896 logging in as that user impossible. You can fix it with:
897 programs.${shell}.enable = true;
898
899 If you know what you're doing and you are fine with the behavior,
900 set users.users.${user.name}.ignoreShellProgramCheck = true;
901 instead.
902 '';
903 }) [
904 "fish"
905 "xonsh"
906 "zsh"
907 ])
908 ));
909
910 warnings =
911 flip concatMap (attrValues cfg.users) (user: let
912 unambiguousPasswordConfiguration = 1 >= length (filter (x: x != null) ([
913 user.hashedPassword
914 user.hashedPasswordFile
915 user.password
916 ] ++ optionals cfg.mutableUsers [
917 # For immutable users, initialHashedPassword is set to hashedPassword,
918 # so using these options would always trigger the assertion.
919 user.initialHashedPassword
920 user.initialPassword
921 ]));
922 in optional (!unambiguousPasswordConfiguration) ''
923 The user '${user.name}' has multiple of the options
924 `hashedPassword`, `password`, `hashedPasswordFile`, `initialPassword`
925 & `initialHashedPassword` set to a non-null value.
926 The options silently discard others by the order of precedence
927 given above which can lead to surprising results. To resolve this warning,
928 set at most one of the options above to a non-`null` value.
929 '')
930 ++ builtins.filter (x: x != null) (
931 flip mapAttrsToList cfg.users (_: user:
932 # This regex matches a subset of the Modular Crypto Format (MCF)[1]
933 # informal standard. Since this depends largely on the OS or the
934 # specific implementation of crypt(3) we only support the (sane)
935 # schemes implemented by glibc and BSDs. In particular the original
936 # DES hash is excluded since, having no structure, it would validate
937 # common mistakes like typing the plaintext password.
938 #
939 # [1]: https://en.wikipedia.org/wiki/Crypt_(C)
940 let
941 sep = "\\$";
942 base64 = "[a-zA-Z0-9./]+";
943 id = cryptSchemeIdPatternGroup;
944 name = "[a-z0-9-]+";
945 value = "[a-zA-Z0-9/+.-]+";
946 options = "${name}(=${value})?(,${name}=${value})*";
947 scheme = "${id}(${sep}${options})?";
948 content = "${base64}${sep}${base64}(${sep}${base64})?";
949 mcf = "^${sep}${scheme}${sep}${content}$";
950 in
951 if (allowsLogin user.hashedPassword
952 && user.hashedPassword != "" # login without password
953 && builtins.match mcf user.hashedPassword == null)
954 then ''
955 The password hash of user "${user.name}" may be invalid. You must set a
956 valid hash or the user will be locked out of their account. Please
957 check the value of option `users.users."${user.name}".hashedPassword`.''
958 else null)
959 ++ flip mapAttrsToList cfg.users (name: user:
960 if user.passwordFile != null then
961 ''The option `users.users."${name}".passwordFile' has been renamed '' +
962 ''to `users.users."${name}".hashedPasswordFile'.''
963 else null)
964 );
965 };
966
967}