2020-04-24 23:36:52 +00:00
|
|
|
|
{ config, lib, utils, pkgs, ... }:
|
|
|
|
|
|
|
|
|
|
with lib;
|
|
|
|
|
|
|
|
|
|
let
|
|
|
|
|
ids = config.ids;
|
|
|
|
|
cfg = config.users;
|
|
|
|
|
|
2020-07-18 16:06:22 +00:00
|
|
|
|
# Check whether a password hash will allow login.
|
|
|
|
|
allowsLogin = hash:
|
|
|
|
|
hash == "" # login without password
|
|
|
|
|
|| !(lib.elem hash
|
|
|
|
|
[ null # password login disabled
|
|
|
|
|
"!" # password login disabled
|
|
|
|
|
"!!" # a variant of "!"
|
|
|
|
|
"*" # password unset
|
|
|
|
|
]);
|
|
|
|
|
|
2020-04-24 23:36:52 +00:00
|
|
|
|
passwordDescription = ''
|
2022-09-09 14:08:57 +00:00
|
|
|
|
The options {option}`hashedPassword`,
|
|
|
|
|
{option}`password` and {option}`passwordFile`
|
2020-04-24 23:36:52 +00:00
|
|
|
|
controls what password is set for the user.
|
2022-09-09 14:08:57 +00:00
|
|
|
|
{option}`hashedPassword` overrides both
|
|
|
|
|
{option}`password` and {option}`passwordFile`.
|
|
|
|
|
{option}`password` overrides {option}`passwordFile`.
|
2020-04-24 23:36:52 +00:00
|
|
|
|
If none of these three options are set, no password is assigned to
|
|
|
|
|
the user, and the user will not be able to do password logins.
|
2022-09-09 14:08:57 +00:00
|
|
|
|
If the option {option}`users.mutableUsers` is true, the
|
2020-04-24 23:36:52 +00:00
|
|
|
|
password defined in one of the three options will only be set when
|
|
|
|
|
the user is created for the first time. After that, you are free to
|
|
|
|
|
change the password with the ordinary user management commands. If
|
2022-09-09 14:08:57 +00:00
|
|
|
|
{option}`users.mutableUsers` is false, you cannot change
|
2020-04-24 23:36:52 +00:00
|
|
|
|
user passwords, they will always be set according to the password
|
|
|
|
|
options.
|
|
|
|
|
'';
|
|
|
|
|
|
|
|
|
|
hashedPasswordDescription = ''
|
2022-11-21 17:40:18 +00:00
|
|
|
|
To generate a hashed password run `mkpasswd`.
|
2020-07-18 16:06:22 +00:00
|
|
|
|
|
2022-09-09 14:08:57 +00:00
|
|
|
|
If set to an empty string (`""`), this user will
|
2020-07-18 16:06:22 +00:00
|
|
|
|
be able to log in without being asked for a password (but not via remote
|
2022-09-09 14:08:57 +00:00
|
|
|
|
services such as SSH, or indirectly via {command}`su` or
|
|
|
|
|
{command}`sudo`). This should only be used for e.g. bootable
|
2020-07-18 16:06:22 +00:00
|
|
|
|
live systems. Note: this is different from setting an empty password,
|
2022-09-09 14:08:57 +00:00
|
|
|
|
which can be achieved using {option}`users.users.<name?>.password`.
|
2020-07-18 16:06:22 +00:00
|
|
|
|
|
2022-09-09 14:08:57 +00:00
|
|
|
|
If set to `null` (default) this user will not
|
|
|
|
|
be able to log in using a password (i.e. via {command}`login`
|
2020-07-18 16:06:22 +00:00
|
|
|
|
command).
|
2020-04-24 23:36:52 +00:00
|
|
|
|
'';
|
|
|
|
|
|
|
|
|
|
userOpts = { name, config, ... }: {
|
|
|
|
|
|
|
|
|
|
options = {
|
|
|
|
|
|
|
|
|
|
name = mkOption {
|
2022-08-12 12:06:08 +00:00
|
|
|
|
type = types.passwdEntry types.str;
|
2020-04-24 23:36:52 +00:00
|
|
|
|
apply = x: assert (builtins.stringLength x < 32 || abort "Username '${x}' is longer than 31 characters which is not allowed!"); x;
|
2022-08-12 12:06:08 +00:00
|
|
|
|
description = lib.mdDoc ''
|
2020-04-24 23:36:52 +00:00
|
|
|
|
The name of the user account. If undefined, the name of the
|
|
|
|
|
attribute set will be used.
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
description = mkOption {
|
2022-08-12 12:06:08 +00:00
|
|
|
|
type = types.passwdEntry types.str;
|
2020-04-24 23:36:52 +00:00
|
|
|
|
default = "";
|
|
|
|
|
example = "Alice Q. User";
|
2022-08-12 12:06:08 +00:00
|
|
|
|
description = lib.mdDoc ''
|
2020-04-24 23:36:52 +00:00
|
|
|
|
A short description of the user account, typically the
|
|
|
|
|
user's full name. This is actually the “GECOS” or “comment”
|
2022-08-12 12:06:08 +00:00
|
|
|
|
field in {file}`/etc/passwd`.
|
2020-04-24 23:36:52 +00:00
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
uid = mkOption {
|
|
|
|
|
type = with types; nullOr int;
|
|
|
|
|
default = null;
|
2022-08-12 12:06:08 +00:00
|
|
|
|
description = lib.mdDoc ''
|
2020-04-24 23:36:52 +00:00
|
|
|
|
The account UID. If the UID is null, a free UID is picked on
|
|
|
|
|
activation.
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
isSystemUser = mkOption {
|
|
|
|
|
type = types.bool;
|
|
|
|
|
default = false;
|
2022-08-12 12:06:08 +00:00
|
|
|
|
description = lib.mdDoc ''
|
2020-04-24 23:36:52 +00:00
|
|
|
|
Indicates if the user is a system user or not. This option
|
2022-08-12 12:06:08 +00:00
|
|
|
|
only has an effect if {option}`uid` is
|
|
|
|
|
{option}`null`, in which case it determines whether
|
2020-04-24 23:36:52 +00:00
|
|
|
|
the user's UID is allocated in the range for system users
|
2023-02-02 18:25:31 +00:00
|
|
|
|
(below 1000) or in the range for normal users (starting at
|
2020-04-24 23:36:52 +00:00
|
|
|
|
1000).
|
2022-08-12 12:06:08 +00:00
|
|
|
|
Exactly one of `isNormalUser` and
|
|
|
|
|
`isSystemUser` must be true.
|
2020-04-24 23:36:52 +00:00
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
isNormalUser = mkOption {
|
|
|
|
|
type = types.bool;
|
|
|
|
|
default = false;
|
2022-08-12 12:06:08 +00:00
|
|
|
|
description = lib.mdDoc ''
|
2022-12-28 21:21:41 +00:00
|
|
|
|
Indicates whether this is an account for a “real” user.
|
|
|
|
|
This automatically sets {option}`group` to `users`,
|
|
|
|
|
{option}`createHome` to `true`,
|
|
|
|
|
{option}`home` to {file}`/home/«username»`,
|
2022-08-12 12:06:08 +00:00
|
|
|
|
{option}`useDefaultShell` to `true`,
|
2022-12-28 21:21:41 +00:00
|
|
|
|
and {option}`isSystemUser` to `false`.
|
|
|
|
|
Exactly one of `isNormalUser` and `isSystemUser` must be true.
|
2020-04-24 23:36:52 +00:00
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
group = mkOption {
|
|
|
|
|
type = types.str;
|
|
|
|
|
apply = x: assert (builtins.stringLength x < 32 || abort "Group name '${x}' is longer than 31 characters which is not allowed!"); x;
|
2021-09-18 10:52:07 +00:00
|
|
|
|
default = "";
|
2022-08-12 12:06:08 +00:00
|
|
|
|
description = lib.mdDoc "The user's primary group.";
|
2020-04-24 23:36:52 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
extraGroups = mkOption {
|
|
|
|
|
type = types.listOf types.str;
|
|
|
|
|
default = [];
|
2022-08-12 12:06:08 +00:00
|
|
|
|
description = lib.mdDoc "The user's auxiliary groups.";
|
2020-04-24 23:36:52 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
home = mkOption {
|
2022-08-12 12:06:08 +00:00
|
|
|
|
type = types.passwdEntry types.path;
|
2020-04-24 23:36:52 +00:00
|
|
|
|
default = "/var/empty";
|
2022-08-12 12:06:08 +00:00
|
|
|
|
description = lib.mdDoc "The user's home directory.";
|
2020-04-24 23:36:52 +00:00
|
|
|
|
};
|
|
|
|
|
|
2022-06-16 17:23:12 +00:00
|
|
|
|
homeMode = mkOption {
|
|
|
|
|
type = types.strMatching "[0-7]{1,5}";
|
|
|
|
|
default = "700";
|
2022-08-12 12:06:08 +00:00
|
|
|
|
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.";
|
2022-06-16 17:23:12 +00:00
|
|
|
|
};
|
|
|
|
|
|
2020-04-24 23:36:52 +00:00
|
|
|
|
cryptHomeLuks = mkOption {
|
|
|
|
|
type = with types; nullOr str;
|
|
|
|
|
default = null;
|
2022-08-12 12:06:08 +00:00
|
|
|
|
description = lib.mdDoc ''
|
2020-04-24 23:36:52 +00:00
|
|
|
|
Path to encrypted luks device that contains
|
|
|
|
|
the user's home directory.
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
2020-10-20 17:27:49 +00:00
|
|
|
|
pamMount = mkOption {
|
|
|
|
|
type = with types; attrsOf str;
|
|
|
|
|
default = {};
|
2022-08-12 12:06:08 +00:00
|
|
|
|
description = lib.mdDoc ''
|
2020-10-20 17:27:49 +00:00
|
|
|
|
Attributes for user's entry in
|
2022-08-12 12:06:08 +00:00
|
|
|
|
{file}`pam_mount.conf.xml`.
|
|
|
|
|
Useful attributes might include `path`,
|
|
|
|
|
`options`, `fstype`, and `server`.
|
|
|
|
|
See <http://pam-mount.sourceforge.net/pam_mount.conf.5.html>
|
2020-10-20 17:27:49 +00:00
|
|
|
|
for more information.
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
2020-04-24 23:36:52 +00:00
|
|
|
|
shell = mkOption {
|
2022-08-12 12:06:08 +00:00
|
|
|
|
type = types.nullOr (types.either types.shellPackage (types.passwdEntry types.path));
|
2020-04-24 23:36:52 +00:00
|
|
|
|
default = pkgs.shadow;
|
2021-10-06 13:57:05 +00:00
|
|
|
|
defaultText = literalExpression "pkgs.shadow";
|
|
|
|
|
example = literalExpression "pkgs.bashInteractive";
|
2022-08-12 12:06:08 +00:00
|
|
|
|
description = lib.mdDoc ''
|
2020-04-24 23:36:52 +00:00
|
|
|
|
The path to the user's shell. Can use shell derivations,
|
2022-08-12 12:06:08 +00:00
|
|
|
|
like `pkgs.bashInteractive`. Don’t
|
2020-04-24 23:36:52 +00:00
|
|
|
|
forget to enable your shell in
|
2022-08-12 12:06:08 +00:00
|
|
|
|
`programs` if necessary,
|
|
|
|
|
like `programs.zsh.enable = true;`.
|
2020-04-24 23:36:52 +00:00
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
subUidRanges = mkOption {
|
|
|
|
|
type = with types; listOf (submodule subordinateUidRange);
|
|
|
|
|
default = [];
|
|
|
|
|
example = [
|
|
|
|
|
{ startUid = 1000; count = 1; }
|
|
|
|
|
{ startUid = 100001; count = 65534; }
|
|
|
|
|
];
|
2022-08-12 12:06:08 +00:00
|
|
|
|
description = lib.mdDoc ''
|
2020-04-24 23:36:52 +00:00
|
|
|
|
Subordinate user ids that user is allowed to use.
|
2022-08-12 12:06:08 +00:00
|
|
|
|
They are set into {file}`/etc/subuid` and are used
|
|
|
|
|
by `newuidmap` for user namespaces.
|
2020-04-24 23:36:52 +00:00
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
subGidRanges = mkOption {
|
|
|
|
|
type = with types; listOf (submodule subordinateGidRange);
|
|
|
|
|
default = [];
|
|
|
|
|
example = [
|
|
|
|
|
{ startGid = 100; count = 1; }
|
|
|
|
|
{ startGid = 1001; count = 999; }
|
|
|
|
|
];
|
2022-08-12 12:06:08 +00:00
|
|
|
|
description = lib.mdDoc ''
|
2020-04-24 23:36:52 +00:00
|
|
|
|
Subordinate group ids that user is allowed to use.
|
2022-08-12 12:06:08 +00:00
|
|
|
|
They are set into {file}`/etc/subgid` and are used
|
|
|
|
|
by `newgidmap` for user namespaces.
|
2020-04-24 23:36:52 +00:00
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
2022-01-13 20:06:32 +00:00
|
|
|
|
autoSubUidGidRange = mkOption {
|
|
|
|
|
type = types.bool;
|
|
|
|
|
default = false;
|
|
|
|
|
example = true;
|
2022-08-12 12:06:08 +00:00
|
|
|
|
description = lib.mdDoc ''
|
2022-01-13 20:06:32 +00:00
|
|
|
|
Automatically allocate subordinate user and group ids for this user.
|
|
|
|
|
Allocated range is currently always of size 65536.
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
2020-04-24 23:36:52 +00:00
|
|
|
|
createHome = mkOption {
|
|
|
|
|
type = types.bool;
|
|
|
|
|
default = false;
|
2022-08-12 12:06:08 +00:00
|
|
|
|
description = lib.mdDoc ''
|
2020-12-25 13:55:36 +00:00
|
|
|
|
Whether to create the home directory and ensure ownership as well as
|
|
|
|
|
permissions to match the user.
|
2020-04-24 23:36:52 +00:00
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
useDefaultShell = mkOption {
|
|
|
|
|
type = types.bool;
|
|
|
|
|
default = false;
|
2022-08-12 12:06:08 +00:00
|
|
|
|
description = lib.mdDoc ''
|
2020-04-24 23:36:52 +00:00
|
|
|
|
If true, the user's shell will be set to
|
2022-08-12 12:06:08 +00:00
|
|
|
|
{option}`users.defaultUserShell`.
|
2020-04-24 23:36:52 +00:00
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
hashedPassword = mkOption {
|
2021-04-25 03:57:28 +00:00
|
|
|
|
type = with types; nullOr (passwdEntry str);
|
2020-04-24 23:36:52 +00:00
|
|
|
|
default = null;
|
2022-09-09 14:08:57 +00:00
|
|
|
|
description = lib.mdDoc ''
|
2020-04-24 23:36:52 +00:00
|
|
|
|
Specifies the hashed password for the user.
|
|
|
|
|
${passwordDescription}
|
|
|
|
|
${hashedPasswordDescription}
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
password = mkOption {
|
|
|
|
|
type = with types; nullOr str;
|
|
|
|
|
default = null;
|
2022-09-09 14:08:57 +00:00
|
|
|
|
description = lib.mdDoc ''
|
2020-04-24 23:36:52 +00:00
|
|
|
|
Specifies the (clear text) password for the user.
|
|
|
|
|
Warning: do not set confidential information here
|
|
|
|
|
because it is world-readable in the Nix store. This option
|
|
|
|
|
should only be used for public accounts.
|
|
|
|
|
${passwordDescription}
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
passwordFile = mkOption {
|
|
|
|
|
type = with types; nullOr str;
|
|
|
|
|
default = null;
|
2022-09-09 14:08:57 +00:00
|
|
|
|
description = lib.mdDoc ''
|
2020-04-24 23:36:52 +00:00
|
|
|
|
The full path to a file that contains the user's password. The password
|
|
|
|
|
file is read on each system activation. The file should contain
|
|
|
|
|
exactly one line, which should be the password in an encrypted form
|
2022-09-09 14:08:57 +00:00
|
|
|
|
that is suitable for the `chpasswd -e` command.
|
2020-04-24 23:36:52 +00:00
|
|
|
|
${passwordDescription}
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
initialHashedPassword = mkOption {
|
2021-04-25 03:57:28 +00:00
|
|
|
|
type = with types; nullOr (passwdEntry str);
|
2020-04-24 23:36:52 +00:00
|
|
|
|
default = null;
|
2022-09-09 14:08:57 +00:00
|
|
|
|
description = lib.mdDoc ''
|
2020-04-24 23:36:52 +00:00
|
|
|
|
Specifies the initial hashed password for the user, i.e. the
|
|
|
|
|
hashed password assigned if the user does not already
|
2022-09-09 14:08:57 +00:00
|
|
|
|
exist. If {option}`users.mutableUsers` is true, the
|
2020-04-24 23:36:52 +00:00
|
|
|
|
password can be changed subsequently using the
|
2022-09-09 14:08:57 +00:00
|
|
|
|
{command}`passwd` command. Otherwise, it's
|
|
|
|
|
equivalent to setting the {option}`hashedPassword` option.
|
2020-04-24 23:36:52 +00:00
|
|
|
|
|
|
|
|
|
${hashedPasswordDescription}
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
initialPassword = mkOption {
|
|
|
|
|
type = with types; nullOr str;
|
|
|
|
|
default = null;
|
2022-08-12 12:06:08 +00:00
|
|
|
|
description = lib.mdDoc ''
|
2020-04-24 23:36:52 +00:00
|
|
|
|
Specifies the initial password for the user, i.e. the
|
|
|
|
|
password assigned if the user does not already exist. If
|
2022-08-12 12:06:08 +00:00
|
|
|
|
{option}`users.mutableUsers` is true, the password
|
2020-04-24 23:36:52 +00:00
|
|
|
|
can be changed subsequently using the
|
2022-08-12 12:06:08 +00:00
|
|
|
|
{command}`passwd` command. Otherwise, it's
|
|
|
|
|
equivalent to setting the {option}`password`
|
2020-04-24 23:36:52 +00:00
|
|
|
|
option. The same caveat applies: the password specified here
|
|
|
|
|
is world-readable in the Nix store, so it should only be
|
|
|
|
|
used for guest accounts or passwords that will be changed
|
|
|
|
|
promptly.
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
packages = mkOption {
|
|
|
|
|
type = types.listOf types.package;
|
|
|
|
|
default = [];
|
2021-10-06 13:57:05 +00:00
|
|
|
|
example = literalExpression "[ pkgs.firefox pkgs.thunderbird ]";
|
2022-08-12 12:06:08 +00:00
|
|
|
|
description = lib.mdDoc ''
|
2020-04-24 23:36:52 +00:00
|
|
|
|
The set of packages that should be made available to the user.
|
2022-08-12 12:06:08 +00:00
|
|
|
|
This is in contrast to {option}`environment.systemPackages`,
|
2020-04-24 23:36:52 +00:00
|
|
|
|
which adds packages to all users.
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
config = mkMerge
|
|
|
|
|
[ { name = mkDefault name;
|
|
|
|
|
shell = mkIf config.useDefaultShell (mkDefault cfg.defaultUserShell);
|
|
|
|
|
}
|
|
|
|
|
(mkIf config.isNormalUser {
|
|
|
|
|
group = mkDefault "users";
|
|
|
|
|
createHome = mkDefault true;
|
|
|
|
|
home = mkDefault "/home/${config.name}";
|
2022-06-16 17:23:12 +00:00
|
|
|
|
homeMode = mkDefault "700";
|
2020-04-24 23:36:52 +00:00
|
|
|
|
useDefaultShell = mkDefault true;
|
|
|
|
|
isSystemUser = mkDefault false;
|
|
|
|
|
})
|
|
|
|
|
# If !mutableUsers, setting ‘initialPassword’ is equivalent to
|
|
|
|
|
# setting ‘password’ (and similarly for hashed passwords).
|
|
|
|
|
(mkIf (!cfg.mutableUsers && config.initialPassword != null) {
|
|
|
|
|
password = mkDefault config.initialPassword;
|
|
|
|
|
})
|
|
|
|
|
(mkIf (!cfg.mutableUsers && config.initialHashedPassword != null) {
|
|
|
|
|
hashedPassword = mkDefault config.initialHashedPassword;
|
|
|
|
|
})
|
2022-01-13 20:06:32 +00:00
|
|
|
|
(mkIf (config.isNormalUser && config.subUidRanges == [] && config.subGidRanges == []) {
|
|
|
|
|
autoSubUidGidRange = mkDefault true;
|
|
|
|
|
})
|
2020-04-24 23:36:52 +00:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
2021-08-27 14:25:00 +00:00
|
|
|
|
groupOpts = { name, config, ... }: {
|
2020-04-24 23:36:52 +00:00
|
|
|
|
|
|
|
|
|
options = {
|
|
|
|
|
|
|
|
|
|
name = mkOption {
|
2022-08-12 12:06:08 +00:00
|
|
|
|
type = types.passwdEntry types.str;
|
|
|
|
|
description = lib.mdDoc ''
|
2020-04-24 23:36:52 +00:00
|
|
|
|
The name of the group. If undefined, the name of the attribute set
|
|
|
|
|
will be used.
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
gid = mkOption {
|
|
|
|
|
type = with types; nullOr int;
|
|
|
|
|
default = null;
|
2022-08-12 12:06:08 +00:00
|
|
|
|
description = lib.mdDoc ''
|
2020-04-24 23:36:52 +00:00
|
|
|
|
The group GID. If the GID is null, a free GID is picked on
|
|
|
|
|
activation.
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
members = mkOption {
|
2021-04-25 03:57:28 +00:00
|
|
|
|
type = with types; listOf (passwdEntry str);
|
2020-04-24 23:36:52 +00:00
|
|
|
|
default = [];
|
2022-08-12 12:06:08 +00:00
|
|
|
|
description = lib.mdDoc ''
|
2020-04-24 23:36:52 +00:00
|
|
|
|
The user names of the group members, added to the
|
2022-08-12 12:06:08 +00:00
|
|
|
|
`/etc/group` file.
|
2020-04-24 23:36:52 +00:00
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
config = {
|
|
|
|
|
name = mkDefault name;
|
2021-08-27 14:25:00 +00:00
|
|
|
|
|
|
|
|
|
members = mapAttrsToList (n: u: u.name) (
|
|
|
|
|
filterAttrs (n: u: elem config.name u.extraGroups) cfg.users
|
|
|
|
|
);
|
2020-04-24 23:36:52 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
subordinateUidRange = {
|
|
|
|
|
options = {
|
|
|
|
|
startUid = mkOption {
|
|
|
|
|
type = types.int;
|
2022-08-12 12:06:08 +00:00
|
|
|
|
description = lib.mdDoc ''
|
2020-04-24 23:36:52 +00:00
|
|
|
|
Start of the range of subordinate user ids that user is
|
|
|
|
|
allowed to use.
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
count = mkOption {
|
|
|
|
|
type = types.int;
|
|
|
|
|
default = 1;
|
2022-08-12 12:06:08 +00:00
|
|
|
|
description = lib.mdDoc "Count of subordinate user ids";
|
2020-04-24 23:36:52 +00:00
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
subordinateGidRange = {
|
|
|
|
|
options = {
|
|
|
|
|
startGid = mkOption {
|
|
|
|
|
type = types.int;
|
2022-08-12 12:06:08 +00:00
|
|
|
|
description = lib.mdDoc ''
|
2020-04-24 23:36:52 +00:00
|
|
|
|
Start of the range of subordinate group ids that user is
|
|
|
|
|
allowed to use.
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
count = mkOption {
|
|
|
|
|
type = types.int;
|
|
|
|
|
default = 1;
|
2022-08-12 12:06:08 +00:00
|
|
|
|
description = lib.mdDoc "Count of subordinate group ids";
|
2020-04-24 23:36:52 +00:00
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2021-08-05 21:33:18 +00:00
|
|
|
|
idsAreUnique = set: idAttr: !(foldr (name: args@{ dup, acc }:
|
2020-04-24 23:36:52 +00:00
|
|
|
|
let
|
|
|
|
|
id = builtins.toString (builtins.getAttr idAttr (builtins.getAttr name set));
|
|
|
|
|
exists = builtins.hasAttr id acc;
|
|
|
|
|
newAcc = acc // (builtins.listToAttrs [ { name = id; value = true; } ]);
|
|
|
|
|
in if dup then args else if exists
|
|
|
|
|
then builtins.trace "Duplicate ${idAttr} ${id}" { dup = true; acc = null; }
|
|
|
|
|
else { dup = false; acc = newAcc; }
|
|
|
|
|
) { dup = false; acc = {}; } (builtins.attrNames set)).dup;
|
|
|
|
|
|
|
|
|
|
uidsAreUnique = idsAreUnique (filterAttrs (n: u: u.uid != null) cfg.users) "uid";
|
|
|
|
|
gidsAreUnique = idsAreUnique (filterAttrs (n: g: g.gid != null) cfg.groups) "gid";
|
|
|
|
|
|
|
|
|
|
spec = pkgs.writeText "users-groups.json" (builtins.toJSON {
|
|
|
|
|
inherit (cfg) mutableUsers;
|
|
|
|
|
users = mapAttrsToList (_: u:
|
|
|
|
|
{ inherit (u)
|
2022-06-16 17:23:12 +00:00
|
|
|
|
name uid group description home homeMode createHome isSystemUser
|
2020-04-24 23:36:52 +00:00
|
|
|
|
password passwordFile hashedPassword
|
2022-01-13 20:06:32 +00:00
|
|
|
|
autoSubUidGidRange subUidRanges subGidRanges
|
2020-04-24 23:36:52 +00:00
|
|
|
|
initialPassword initialHashedPassword;
|
|
|
|
|
shell = utils.toShellPath u.shell;
|
|
|
|
|
}) cfg.users;
|
2021-08-27 14:25:00 +00:00
|
|
|
|
groups = attrValues cfg.groups;
|
2020-04-24 23:36:52 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
systemShells =
|
|
|
|
|
let
|
|
|
|
|
shells = mapAttrsToList (_: u: u.shell) cfg.users;
|
|
|
|
|
in
|
|
|
|
|
filter types.shellPackage.check shells;
|
|
|
|
|
|
|
|
|
|
in {
|
|
|
|
|
imports = [
|
2023-01-11 07:51:40 +00:00
|
|
|
|
(mkAliasOptionModuleMD [ "users" "extraUsers" ] [ "users" "users" ])
|
|
|
|
|
(mkAliasOptionModuleMD [ "users" "extraGroups" ] [ "users" "groups" ])
|
2022-02-10 20:34:41 +00:00
|
|
|
|
(mkRenamedOptionModule ["security" "initialRootPassword"] ["users" "users" "root" "initialHashedPassword"])
|
2020-04-24 23:36:52 +00:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
###### interface
|
|
|
|
|
options = {
|
|
|
|
|
|
|
|
|
|
users.mutableUsers = mkOption {
|
|
|
|
|
type = types.bool;
|
|
|
|
|
default = true;
|
2022-09-09 14:08:57 +00:00
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
|
If set to `true`, you are free to add new users and groups to the system
|
|
|
|
|
with the ordinary `useradd` and
|
|
|
|
|
`groupadd` commands. On system activation, the
|
|
|
|
|
existing contents of the `/etc/passwd` and
|
|
|
|
|
`/etc/group` files will be merged with the
|
|
|
|
|
contents generated from the `users.users` and
|
|
|
|
|
`users.groups` options.
|
2020-04-24 23:36:52 +00:00
|
|
|
|
The initial password for a user will be set
|
2022-09-09 14:08:57 +00:00
|
|
|
|
according to `users.users`, but existing passwords
|
2020-04-24 23:36:52 +00:00
|
|
|
|
will not be changed.
|
|
|
|
|
|
2022-09-09 14:08:57 +00:00
|
|
|
|
::: {.warning}
|
|
|
|
|
If set to `false`, the contents of the user and
|
2020-04-24 23:36:52 +00:00
|
|
|
|
group files will simply be replaced on system activation. This also
|
|
|
|
|
holds for the user passwords; all changed
|
|
|
|
|
passwords will be reset according to the
|
2022-09-09 14:08:57 +00:00
|
|
|
|
`users.users` configuration on activation.
|
|
|
|
|
:::
|
2020-04-24 23:36:52 +00:00
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
users.enforceIdUniqueness = mkOption {
|
|
|
|
|
type = types.bool;
|
|
|
|
|
default = true;
|
2022-08-12 12:06:08 +00:00
|
|
|
|
description = lib.mdDoc ''
|
2020-04-24 23:36:52 +00:00
|
|
|
|
Whether to require that no two users/groups share the same uid/gid.
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
users.users = mkOption {
|
|
|
|
|
default = {};
|
2020-09-25 04:45:31 +00:00
|
|
|
|
type = with types; attrsOf (submodule userOpts);
|
2020-04-24 23:36:52 +00:00
|
|
|
|
example = {
|
|
|
|
|
alice = {
|
|
|
|
|
uid = 1234;
|
|
|
|
|
description = "Alice Q. User";
|
|
|
|
|
home = "/home/alice";
|
|
|
|
|
createHome = true;
|
|
|
|
|
group = "users";
|
|
|
|
|
extraGroups = ["wheel"];
|
|
|
|
|
shell = "/bin/sh";
|
|
|
|
|
};
|
|
|
|
|
};
|
2022-08-12 12:06:08 +00:00
|
|
|
|
description = lib.mdDoc ''
|
2020-04-24 23:36:52 +00:00
|
|
|
|
Additional user accounts to be created automatically by the system.
|
|
|
|
|
This can also be used to set options for root.
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
users.groups = mkOption {
|
|
|
|
|
default = {};
|
|
|
|
|
example =
|
|
|
|
|
{ students.gid = 1001;
|
|
|
|
|
hackers = { };
|
|
|
|
|
};
|
2020-09-25 04:45:31 +00:00
|
|
|
|
type = with types; attrsOf (submodule groupOpts);
|
2022-08-12 12:06:08 +00:00
|
|
|
|
description = lib.mdDoc ''
|
2020-04-24 23:36:52 +00:00
|
|
|
|
Additional groups to be created automatically by the system.
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
2022-02-10 20:34:41 +00:00
|
|
|
|
|
|
|
|
|
users.allowNoPasswordLogin = mkOption {
|
|
|
|
|
type = types.bool;
|
|
|
|
|
default = false;
|
2022-08-12 12:06:08 +00:00
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
|
Disable checking that at least the `root` user or a user in the `wheel` group can log in using
|
2022-02-10 20:34:41 +00:00
|
|
|
|
a password or an SSH key.
|
|
|
|
|
|
|
|
|
|
WARNING: enabling this can lock you out of your system. Enable this only if you know what are you doing.
|
|
|
|
|
'';
|
|
|
|
|
};
|
2020-04-24 23:36:52 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
###### implementation
|
|
|
|
|
|
|
|
|
|
config = {
|
|
|
|
|
|
|
|
|
|
users.users = {
|
|
|
|
|
root = {
|
|
|
|
|
uid = ids.uids.root;
|
|
|
|
|
description = "System administrator";
|
|
|
|
|
home = "/root";
|
|
|
|
|
shell = mkDefault cfg.defaultUserShell;
|
|
|
|
|
group = "root";
|
2022-02-10 20:34:41 +00:00
|
|
|
|
initialHashedPassword = mkDefault "!";
|
2020-04-24 23:36:52 +00:00
|
|
|
|
};
|
|
|
|
|
nobody = {
|
|
|
|
|
uid = ids.uids.nobody;
|
2021-04-15 00:37:46 +00:00
|
|
|
|
isSystemUser = true;
|
2020-04-24 23:36:52 +00:00
|
|
|
|
description = "Unprivileged account (don't use!)";
|
|
|
|
|
group = "nogroup";
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
users.groups = {
|
|
|
|
|
root.gid = ids.gids.root;
|
|
|
|
|
wheel.gid = ids.gids.wheel;
|
|
|
|
|
disk.gid = ids.gids.disk;
|
|
|
|
|
kmem.gid = ids.gids.kmem;
|
|
|
|
|
tty.gid = ids.gids.tty;
|
|
|
|
|
floppy.gid = ids.gids.floppy;
|
|
|
|
|
uucp.gid = ids.gids.uucp;
|
|
|
|
|
lp.gid = ids.gids.lp;
|
|
|
|
|
cdrom.gid = ids.gids.cdrom;
|
|
|
|
|
tape.gid = ids.gids.tape;
|
|
|
|
|
audio.gid = ids.gids.audio;
|
|
|
|
|
video.gid = ids.gids.video;
|
|
|
|
|
dialout.gid = ids.gids.dialout;
|
|
|
|
|
nogroup.gid = ids.gids.nogroup;
|
|
|
|
|
users.gid = ids.gids.users;
|
|
|
|
|
nixbld.gid = ids.gids.nixbld;
|
|
|
|
|
utmp.gid = ids.gids.utmp;
|
|
|
|
|
adm.gid = ids.gids.adm;
|
|
|
|
|
input.gid = ids.gids.input;
|
|
|
|
|
kvm.gid = ids.gids.kvm;
|
|
|
|
|
render.gid = ids.gids.render;
|
2021-12-06 16:07:01 +00:00
|
|
|
|
sgx.gid = ids.gids.sgx;
|
2020-10-07 09:15:18 +00:00
|
|
|
|
shadow.gid = ids.gids.shadow;
|
2020-04-24 23:36:52 +00:00
|
|
|
|
};
|
|
|
|
|
|
2021-09-18 10:52:07 +00:00
|
|
|
|
system.activationScripts.users = {
|
|
|
|
|
supportsDryActivation = true;
|
|
|
|
|
text = ''
|
2020-04-24 23:36:52 +00:00
|
|
|
|
install -m 0700 -d /root
|
|
|
|
|
install -m 0755 -d /home
|
|
|
|
|
|
2021-04-12 18:23:04 +00:00
|
|
|
|
${pkgs.perl.withPackages (p: [ p.FileSlurp p.JSON ])}/bin/perl \
|
|
|
|
|
-w ${./update-users-groups.pl} ${spec}
|
2020-04-24 23:36:52 +00:00
|
|
|
|
'';
|
2021-09-18 10:52:07 +00:00
|
|
|
|
};
|
2020-04-24 23:36:52 +00:00
|
|
|
|
|
2022-11-21 17:40:18 +00:00
|
|
|
|
# Warn about user accounts with deprecated password hashing schemes
|
|
|
|
|
system.activationScripts.hashes = {
|
|
|
|
|
deps = [ "users" ];
|
|
|
|
|
text = ''
|
|
|
|
|
users=()
|
|
|
|
|
while IFS=: read -r user hash tail; do
|
|
|
|
|
if [[ "$hash" = "$"* && ! "$hash" =~ ^\$(y|gy|7|2b|2y|2a|6)\$ ]]; then
|
|
|
|
|
users+=("$user")
|
|
|
|
|
fi
|
|
|
|
|
done </etc/shadow
|
|
|
|
|
|
|
|
|
|
if (( "''${#users[@]}" )); then
|
|
|
|
|
echo "
|
|
|
|
|
WARNING: The following user accounts rely on password hashes that will
|
|
|
|
|
be removed in NixOS 23.05. They should be renewed as soon as possible."
|
|
|
|
|
printf ' - %s\n' "''${users[@]}"
|
|
|
|
|
fi
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
2020-04-24 23:36:52 +00:00
|
|
|
|
# for backwards compatibility
|
|
|
|
|
system.activationScripts.groups = stringAfter [ "users" ] "";
|
|
|
|
|
|
|
|
|
|
# Install all the user shells
|
|
|
|
|
environment.systemPackages = systemShells;
|
|
|
|
|
|
2022-12-02 08:20:57 +00:00
|
|
|
|
environment.etc = mapAttrs' (_: { packages, name, ... }: {
|
2020-04-24 23:36:52 +00:00
|
|
|
|
name = "profiles/per-user/${name}";
|
|
|
|
|
value.source = pkgs.buildEnv {
|
|
|
|
|
name = "user-environment";
|
|
|
|
|
paths = packages;
|
|
|
|
|
inherit (config.environment) pathsToLink extraOutputsToInstall;
|
|
|
|
|
inherit (config.system.path) ignoreCollisions postBuild;
|
|
|
|
|
};
|
2022-12-02 08:20:57 +00:00
|
|
|
|
}) (filterAttrs (_: u: u.packages != []) cfg.users);
|
2020-04-24 23:36:52 +00:00
|
|
|
|
|
|
|
|
|
environment.profiles = [
|
|
|
|
|
"$HOME/.nix-profile"
|
|
|
|
|
"/etc/profiles/per-user/$USER"
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
assertions = [
|
|
|
|
|
{ assertion = !cfg.enforceIdUniqueness || (uidsAreUnique && gidsAreUnique);
|
|
|
|
|
message = "UIDs and GIDs must be unique!";
|
|
|
|
|
}
|
|
|
|
|
{ # If mutableUsers is false, to prevent users creating a
|
|
|
|
|
# configuration that locks them out of the system, ensure that
|
|
|
|
|
# there is at least one "privileged" account that has a
|
|
|
|
|
# password or an SSH authorized key. Privileged accounts are
|
|
|
|
|
# root and users in the wheel group.
|
2022-02-10 20:34:41 +00:00
|
|
|
|
# The check does not apply when users.disableLoginPossibilityAssertion
|
|
|
|
|
# The check does not apply when users.mutableUsers
|
|
|
|
|
assertion = !cfg.mutableUsers -> !cfg.allowNoPasswordLogin ->
|
|
|
|
|
any id (mapAttrsToList (name: cfg:
|
|
|
|
|
(name == "root"
|
2020-04-24 23:36:52 +00:00
|
|
|
|
|| cfg.group == "wheel"
|
|
|
|
|
|| elem "wheel" cfg.extraGroups)
|
|
|
|
|
&&
|
2020-07-18 16:06:22 +00:00
|
|
|
|
(allowsLogin cfg.hashedPassword
|
2020-04-24 23:36:52 +00:00
|
|
|
|
|| cfg.password != null
|
|
|
|
|
|| cfg.passwordFile != null
|
|
|
|
|
|| cfg.openssh.authorizedKeys.keys != []
|
|
|
|
|
|| cfg.openssh.authorizedKeys.keyFiles != [])
|
2022-02-10 20:34:41 +00:00
|
|
|
|
) cfg.users ++ [
|
2020-08-20 17:08:02 +00:00
|
|
|
|
config.security.googleOsLogin.enable
|
|
|
|
|
]);
|
2020-04-24 23:36:52 +00:00
|
|
|
|
message = ''
|
|
|
|
|
Neither the root account nor any wheel user has a password or SSH authorized key.
|
2022-02-10 20:34:41 +00:00
|
|
|
|
You must set one to prevent being locked out of your system.
|
|
|
|
|
If you really want to be locked out of your system, set users.allowNoPasswordLogin = true;
|
|
|
|
|
However you are most probably better off by setting users.mutableUsers = true; and
|
|
|
|
|
manually running passwd root to set the root password.
|
|
|
|
|
'';
|
2020-04-24 23:36:52 +00:00
|
|
|
|
}
|
2021-04-15 00:37:46 +00:00
|
|
|
|
] ++ flatten (flip mapAttrsToList cfg.users (name: user:
|
|
|
|
|
[
|
|
|
|
|
{
|
2020-07-18 16:06:22 +00:00
|
|
|
|
assertion = (user.hashedPassword != null)
|
2021-04-15 00:37:46 +00:00
|
|
|
|
-> (builtins.match ".*:.*" user.hashedPassword == null);
|
2020-07-18 16:06:22 +00:00
|
|
|
|
message = ''
|
2021-04-15 00:37:46 +00:00
|
|
|
|
The password hash of user "${user.name}" contains a ":" character.
|
|
|
|
|
This is invalid and would break the login system because the fields
|
|
|
|
|
of /etc/shadow (file where hashes are stored) are colon-separated.
|
|
|
|
|
Please check the value of option `users.users."${user.name}".hashedPassword`.'';
|
|
|
|
|
}
|
|
|
|
|
{
|
|
|
|
|
assertion = let
|
|
|
|
|
xor = a: b: a && !b || b && !a;
|
2023-02-02 18:25:31 +00:00
|
|
|
|
isEffectivelySystemUser = user.isSystemUser || (user.uid != null && user.uid < 1000);
|
2021-04-15 00:37:46 +00:00
|
|
|
|
in xor isEffectivelySystemUser user.isNormalUser;
|
|
|
|
|
message = ''
|
|
|
|
|
Exactly one of users.users.${user.name}.isSystemUser and users.users.${user.name}.isNormalUser must be set.
|
|
|
|
|
'';
|
|
|
|
|
}
|
2021-09-18 10:52:07 +00:00
|
|
|
|
{
|
|
|
|
|
assertion = user.group != "";
|
|
|
|
|
message = ''
|
|
|
|
|
users.users.${user.name}.group is unset. This used to default to
|
|
|
|
|
nogroup, but this is unsafe. For example you can create a group
|
|
|
|
|
for this user with:
|
|
|
|
|
users.users.${user.name}.group = "${user.name}";
|
|
|
|
|
users.groups.${user.name} = {};
|
|
|
|
|
'';
|
|
|
|
|
}
|
2021-04-15 00:37:46 +00:00
|
|
|
|
]
|
|
|
|
|
));
|
2020-04-24 23:36:52 +00:00
|
|
|
|
|
2020-06-18 07:06:33 +00:00
|
|
|
|
warnings =
|
|
|
|
|
builtins.filter (x: x != null) (
|
2021-04-05 15:23:46 +00:00
|
|
|
|
flip mapAttrsToList cfg.users (_: user:
|
2020-06-18 07:06:33 +00:00
|
|
|
|
# This regex matches a subset of the Modular Crypto Format (MCF)[1]
|
|
|
|
|
# informal standard. Since this depends largely on the OS or the
|
|
|
|
|
# specific implementation of crypt(3) we only support the (sane)
|
|
|
|
|
# schemes implemented by glibc and BSDs. In particular the original
|
|
|
|
|
# DES hash is excluded since, having no structure, it would validate
|
|
|
|
|
# common mistakes like typing the plaintext password.
|
|
|
|
|
#
|
|
|
|
|
# [1]: https://en.wikipedia.org/wiki/Crypt_(C)
|
|
|
|
|
let
|
|
|
|
|
sep = "\\$";
|
|
|
|
|
base64 = "[a-zA-Z0-9./]+";
|
|
|
|
|
id = "[a-z0-9-]+";
|
|
|
|
|
value = "[a-zA-Z0-9/+.-]+";
|
|
|
|
|
options = "${id}(=${value})?(,${id}=${value})*";
|
|
|
|
|
scheme = "${id}(${sep}${options})?";
|
2022-10-30 15:09:59 +00:00
|
|
|
|
content = "${base64}${sep}${base64}(${sep}${base64})?";
|
2020-06-18 07:06:33 +00:00
|
|
|
|
mcf = "^${sep}${scheme}${sep}${content}$";
|
|
|
|
|
in
|
2020-07-18 16:06:22 +00:00
|
|
|
|
if (allowsLogin user.hashedPassword
|
|
|
|
|
&& user.hashedPassword != "" # login without password
|
2020-06-18 07:06:33 +00:00
|
|
|
|
&& builtins.match mcf user.hashedPassword == null)
|
2020-07-18 16:06:22 +00:00
|
|
|
|
then ''
|
2021-04-05 15:23:46 +00:00
|
|
|
|
The password hash of user "${user.name}" may be invalid. You must set a
|
2020-07-18 16:06:22 +00:00
|
|
|
|
valid hash or the user will be locked out of their account. Please
|
2021-04-05 15:23:46 +00:00
|
|
|
|
check the value of option `users.users."${user.name}".hashedPassword`.''
|
2020-06-18 07:06:33 +00:00
|
|
|
|
else null
|
|
|
|
|
));
|
|
|
|
|
|
2020-04-24 23:36:52 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
}
|