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 = "nogroup";
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 = "pkgs.shadow";
169 example = literalExample "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 = literalExample "[ 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, ... }: {
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
363 };
364
365 subordinateUidRange = {
366 options = {
367 startUid = mkOption {
368 type = types.int;
369 description = ''
370 Start of the range of subordinate user ids that user is
371 allowed to use.
372 '';
373 };
374 count = mkOption {
375 type = types.int;
376 default = 1;
377 description = "Count of subordinate user ids";
378 };
379 };
380 };
381
382 subordinateGidRange = {
383 options = {
384 startGid = mkOption {
385 type = types.int;
386 description = ''
387 Start of the range of subordinate group ids that user is
388 allowed to use.
389 '';
390 };
391 count = mkOption {
392 type = types.int;
393 default = 1;
394 description = "Count of subordinate group ids";
395 };
396 };
397 };
398
399 idsAreUnique = set: idAttr: !(fold (name: args@{ dup, acc }:
400 let
401 id = builtins.toString (builtins.getAttr idAttr (builtins.getAttr name set));
402 exists = builtins.hasAttr id acc;
403 newAcc = acc // (builtins.listToAttrs [ { name = id; value = true; } ]);
404 in if dup then args else if exists
405 then builtins.trace "Duplicate ${idAttr} ${id}" { dup = true; acc = null; }
406 else { dup = false; acc = newAcc; }
407 ) { dup = false; acc = {}; } (builtins.attrNames set)).dup;
408
409 uidsAreUnique = idsAreUnique (filterAttrs (n: u: u.uid != null) cfg.users) "uid";
410 gidsAreUnique = idsAreUnique (filterAttrs (n: g: g.gid != null) cfg.groups) "gid";
411
412 spec = pkgs.writeText "users-groups.json" (builtins.toJSON {
413 inherit (cfg) mutableUsers;
414 users = mapAttrsToList (_: u:
415 { inherit (u)
416 name uid group description home createHome isSystemUser
417 password passwordFile hashedPassword
418 isNormalUser subUidRanges subGidRanges
419 initialPassword initialHashedPassword;
420 shell = utils.toShellPath u.shell;
421 }) cfg.users;
422 groups = mapAttrsToList (n: g:
423 { inherit (g) name gid;
424 members = g.members ++ (mapAttrsToList (n: u: u.name) (
425 filterAttrs (n: u: elem g.name u.extraGroups) cfg.users
426 ));
427 }) cfg.groups;
428 });
429
430 systemShells =
431 let
432 shells = mapAttrsToList (_: u: u.shell) cfg.users;
433 in
434 filter types.shellPackage.check shells;
435
436in {
437 imports = [
438 (mkAliasOptionModule [ "users" "extraUsers" ] [ "users" "users" ])
439 (mkAliasOptionModule [ "users" "extraGroups" ] [ "users" "groups" ])
440 (mkChangedOptionModule
441 [ "security" "initialRootPassword" ]
442 [ "users" "users" "root" "initialHashedPassword" ]
443 (cfg: if cfg.security.initialRootPassword == "!"
444 then null
445 else cfg.security.initialRootPassword))
446 ];
447
448 ###### interface
449
450 options = {
451
452 users.mutableUsers = mkOption {
453 type = types.bool;
454 default = true;
455 description = ''
456 If set to <literal>true</literal>, you are free to add new users and groups to the system
457 with the ordinary <literal>useradd</literal> and
458 <literal>groupadd</literal> commands. On system activation, the
459 existing contents of the <literal>/etc/passwd</literal> and
460 <literal>/etc/group</literal> files will be merged with the
461 contents generated from the <literal>users.users</literal> and
462 <literal>users.groups</literal> options.
463 The initial password for a user will be set
464 according to <literal>users.users</literal>, but existing passwords
465 will not be changed.
466
467 <warning><para>
468 If set to <literal>false</literal>, the contents of the user and
469 group files will simply be replaced on system activation. This also
470 holds for the user passwords; all changed
471 passwords will be reset according to the
472 <literal>users.users</literal> configuration on activation.
473 </para></warning>
474 '';
475 };
476
477 users.enforceIdUniqueness = mkOption {
478 type = types.bool;
479 default = true;
480 description = ''
481 Whether to require that no two users/groups share the same uid/gid.
482 '';
483 };
484
485 users.users = mkOption {
486 default = {};
487 type = with types; attrsOf (submodule userOpts);
488 example = {
489 alice = {
490 uid = 1234;
491 description = "Alice Q. User";
492 home = "/home/alice";
493 createHome = true;
494 group = "users";
495 extraGroups = ["wheel"];
496 shell = "/bin/sh";
497 };
498 };
499 description = ''
500 Additional user accounts to be created automatically by the system.
501 This can also be used to set options for root.
502 '';
503 };
504
505 users.groups = mkOption {
506 default = {};
507 example =
508 { students.gid = 1001;
509 hackers = { };
510 };
511 type = with types; attrsOf (submodule groupOpts);
512 description = ''
513 Additional groups to be created automatically by the system.
514 '';
515 };
516
517 };
518
519
520 ###### implementation
521
522 config = {
523
524 users.users = {
525 root = {
526 uid = ids.uids.root;
527 description = "System administrator";
528 home = "/root";
529 shell = mkDefault cfg.defaultUserShell;
530 group = "root";
531 };
532 nobody = {
533 uid = ids.uids.nobody;
534 isSystemUser = true;
535 description = "Unprivileged account (don't use!)";
536 group = "nogroup";
537 };
538 };
539
540 users.groups = {
541 root.gid = ids.gids.root;
542 wheel.gid = ids.gids.wheel;
543 disk.gid = ids.gids.disk;
544 kmem.gid = ids.gids.kmem;
545 tty.gid = ids.gids.tty;
546 floppy.gid = ids.gids.floppy;
547 uucp.gid = ids.gids.uucp;
548 lp.gid = ids.gids.lp;
549 cdrom.gid = ids.gids.cdrom;
550 tape.gid = ids.gids.tape;
551 audio.gid = ids.gids.audio;
552 video.gid = ids.gids.video;
553 dialout.gid = ids.gids.dialout;
554 nogroup.gid = ids.gids.nogroup;
555 users.gid = ids.gids.users;
556 nixbld.gid = ids.gids.nixbld;
557 utmp.gid = ids.gids.utmp;
558 adm.gid = ids.gids.adm;
559 input.gid = ids.gids.input;
560 kvm.gid = ids.gids.kvm;
561 render.gid = ids.gids.render;
562 shadow.gid = ids.gids.shadow;
563 };
564
565 system.activationScripts.users = stringAfter [ "stdio" ]
566 ''
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 # for backwards compatibility
575 system.activationScripts.groups = stringAfter [ "users" ] "";
576
577 # Install all the user shells
578 environment.systemPackages = systemShells;
579
580 environment.etc = (mapAttrs' (_: { packages, name, ... }: {
581 name = "profiles/per-user/${name}";
582 value.source = pkgs.buildEnv {
583 name = "user-environment";
584 paths = packages;
585 inherit (config.environment) pathsToLink extraOutputsToInstall;
586 inherit (config.system.path) ignoreCollisions postBuild;
587 };
588 }) (filterAttrs (_: u: u.packages != []) cfg.users));
589
590 environment.profiles = [
591 "$HOME/.nix-profile"
592 "/etc/profiles/per-user/$USER"
593 ];
594
595 assertions = [
596 { assertion = !cfg.enforceIdUniqueness || (uidsAreUnique && gidsAreUnique);
597 message = "UIDs and GIDs must be unique!";
598 }
599 { # If mutableUsers is false, to prevent users creating a
600 # configuration that locks them out of the system, ensure that
601 # there is at least one "privileged" account that has a
602 # password or an SSH authorized key. Privileged accounts are
603 # root and users in the wheel group.
604 assertion = !cfg.mutableUsers ->
605 any id ((mapAttrsToList (_: cfg:
606 (cfg.name == "root"
607 || cfg.group == "wheel"
608 || elem "wheel" cfg.extraGroups)
609 &&
610 (allowsLogin cfg.hashedPassword
611 || cfg.password != null
612 || cfg.passwordFile != null
613 || cfg.openssh.authorizedKeys.keys != []
614 || cfg.openssh.authorizedKeys.keyFiles != [])
615 ) cfg.users) ++ [
616 config.security.googleOsLogin.enable
617 ]);
618 message = ''
619 Neither the root account nor any wheel user has a password or SSH authorized key.
620 You must set one to prevent being locked out of your system.'';
621 }
622 ] ++ flatten (flip mapAttrsToList cfg.users (name: user:
623 [
624 {
625 assertion = (user.hashedPassword != null)
626 -> (builtins.match ".*:.*" user.hashedPassword == null);
627 message = ''
628 The password hash of user "${user.name}" contains a ":" character.
629 This is invalid and would break the login system because the fields
630 of /etc/shadow (file where hashes are stored) are colon-separated.
631 Please check the value of option `users.users."${user.name}".hashedPassword`.'';
632 }
633 {
634 assertion = let
635 xor = a: b: a && !b || b && !a;
636 isEffectivelySystemUser = user.isSystemUser || (user.uid != null && user.uid < 500);
637 in xor isEffectivelySystemUser user.isNormalUser;
638 message = ''
639 Exactly one of users.users.${user.name}.isSystemUser and users.users.${user.name}.isNormalUser must be set.
640 '';
641 }
642 ]
643 ));
644
645 warnings =
646 builtins.filter (x: x != null) (
647 flip mapAttrsToList cfg.users (_: user:
648 # This regex matches a subset of the Modular Crypto Format (MCF)[1]
649 # informal standard. Since this depends largely on the OS or the
650 # specific implementation of crypt(3) we only support the (sane)
651 # schemes implemented by glibc and BSDs. In particular the original
652 # DES hash is excluded since, having no structure, it would validate
653 # common mistakes like typing the plaintext password.
654 #
655 # [1]: https://en.wikipedia.org/wiki/Crypt_(C)
656 let
657 sep = "\\$";
658 base64 = "[a-zA-Z0-9./]+";
659 id = "[a-z0-9-]+";
660 value = "[a-zA-Z0-9/+.-]+";
661 options = "${id}(=${value})?(,${id}=${value})*";
662 scheme = "${id}(${sep}${options})?";
663 content = "${base64}${sep}${base64}";
664 mcf = "^${sep}${scheme}${sep}${content}$";
665 in
666 if (allowsLogin user.hashedPassword
667 && user.hashedPassword != "" # login without password
668 && builtins.match mcf user.hashedPassword == null)
669 then ''
670 The password hash of user "${user.name}" may be invalid. You must set a
671 valid hash or the user will be locked out of their account. Please
672 check the value of option `users.users."${user.name}".hashedPassword`.''
673 else null
674 ));
675
676 };
677
678}