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