diff --git a/modules/config/users-groups.nix b/modules/config/users-groups.nix
index 0e02822fb77..37e431330ce 100644
--- a/modules/config/users-groups.nix
+++ b/modules/config/users-groups.nix
@@ -5,43 +5,83 @@ with pkgs.lib;
let
ids = config.ids;
+ users = config.users;
+ userOpts = {name, config, ...}:
- # User accounts to be created/updated by NixOS.
- users =
- let
- defaultUsers =
- [ { name = "root";
- uid = ids.uids.root;
- description = "System administrator";
- home = "/root";
- shell = config.users.defaultUserShell;
- group = "root";
- }
- { name = "nobody";
- uid = ids.uids.nobody;
- description = "Unprivileged account (don't use!)";
- }
- ];
-
- # !!! Use NixOS module system to add default attributes.
- addAttrs =
- { name
- , description
- , uid ? ""
- , group ? "nogroup"
- , extraGroups ? []
- , home ? "/var/empty"
- , shell ? (if useDefaultShell then config.users.defaultUserShell else "/noshell")
- , createHome ? false
- , useDefaultShell ? false
- , password ? null
- , isSystemUser ? true
- }:
- { inherit name description uid group extraGroups home shell createHome password isSystemUser; };
-
- in map addAttrs (defaultUsers ++ config.users.extraUsers);
+ {
+ options = {
+ name = mkOption {
+ type = with types; uniq string;
+ description = "The name of the user account. If undefined, the name of the attribute set will be used.";
+ };
+ description = mkOption {
+ type = with types; uniq string;
+ default = "";
+ description = "A short description of the user account.";
+ };
+ uid = mkOption {
+ type = with types; uniq (nullOr int);
+ default = null;
+ description = "The account UID. If undefined, NixOS will select a UID.";
+ };
+ group = mkOption {
+ type = with types; uniq string;
+ default = "nogroup";
+ description = "The user's primary group.";
+ };
+ extraGroups = mkOption {
+ type = types.listOf types.string;
+ default = [];
+ description = "The user's auxiliary groups.";
+ };
+ home = mkOption {
+ type = with types; uniq string;
+ default = "/var/empty";
+ description = "The user's home directory.";
+ };
+ shell = mkOption {
+ type = with types; uniq string;
+ default = "/noshell";
+ description = "The path to the user's shell.";
+ };
+ createHome = mkOption {
+ type = types.bool;
+ default = false;
+ description = "If true, the home directory will be created automatically.";
+ };
+ useDefaultShell = mkOption {
+ type = types.bool;
+ default = false;
+ description = "If true, the user's shell will be set to users.defaultUserShell.";
+ };
+ password = mkOption {
+ type = with types; uniq (nullOr string);
+ default = null;
+ description = "The user's password. If undefined, no password is set for the user. Warning: do not set confidential information here because this data would be readable by all. This option should only be used for public account such as guest.";
+ };
+ isSystemUser = mkOption {
+ type = types.bool;
+ default = true;
+ description = "Indicates if the user is a system user or not.";
+ };
+ createUser = mkOption {
+ type = types.bool;
+ default = true;
+ description = "
+ Indicates if the user should be created automatically as a local user.
+ Set this to false if the user for instance is an LDAP user. NixOS will
+ then not modify any of the basic properties for the user account.
+ ";
+ };
+ };
+ config = {
+ name = mkDefault name;
+ uid = mkDefault (attrByPath [name] null ids.uids);
+ shell = mkIf config.useDefaultShell (mkDefault users.defaultUserShell);
+ };
+ };
# Groups to be created/updated by NixOS.
groups =
@@ -109,11 +149,14 @@ let
# Note: the 'X' in front of the password is to distinguish between
# having an empty password, and not having a password.
- serializedUser = u: "${u.name}\n${u.description}\n${toString u.uid}\n${u.group}\n${toString (concatStringsSep "," u.extraGroups)}\n${u.home}\n${u.shell}\n${toString u.createHome}\n${if u.password != null then "X" + u.password else ""}\n${toString u.isSystemUser}\n";
+ serializedUser = userName: let u = getAttr userName config.users.extraUsers; in "${u.name}\n${u.description}\n${if u.uid != null then toString u.uid else ""}\n${u.group}\n${toString (concatStringsSep "," u.extraGroups)}\n${u.home}\n${u.shell}\n${toString u.createHome}\n${if u.password != null then "X" + u.password else ""}\n${toString u.isSystemUser}\n${if u.createUser then "yes" else "no"}\n";
+
serializedGroup = g: "${g.name}\n${toString g.gid}";
# keep this extra file so that cat can be used to pass special chars such as "`" which is used in the avahi daemon
- usersFile = pkgs.writeText "users" (concatStrings (map serializedUser users));
+ usersFile = pkgs.writeText "users" (
+ concatMapStrings serializedUser (attrNames config.users.extraUsers)
+ );
in
@@ -124,22 +167,24 @@ in
options = {
users.extraUsers = mkOption {
- default = [];
- example =
- [ { name = "alice";
- uid = 1234;
- description = "Alice";
- home = "/home/alice";
- createHome = true;
- group = "users";
- extraGroups = ["wheel"];
- shell = "/bin/sh";
- password = "foobar";
- }
- ];
+ default = {};
+ type = types.loaOf types.optionSet;
+ example = {
+ alice = {
+ uid = 1234;
+ description = "Alice";
+ home = "/home/alice";
+ createHome = true;
+ group = "users";
+ extraGroups = ["wheel"];
+ shell = "/bin/sh";
+ password = "foobar";
+ };
+ };
description = ''
Additional user accounts to be created automatically by the system.
'';
+ options = [ userOpts ];
};
users.extraGroups = mkOption {
@@ -154,6 +199,15 @@ in
'';
};
+ user = mkOption {
+ default = {};
+ description = ''
+ This option defines settings for individual users on the system.
+ '';
+ type = types.loaOf types.optionSet;
+ options = [ ];
+ };
+
};
@@ -161,6 +215,18 @@ in
config = {
+ users.extraUsers = {
+ root = {
+ description = "System administrator";
+ home = "/root";
+ shell = config.users.defaultUserShell;
+ group = "root";
+ };
+ nobody = {
+ description = "Unprivileged account (don't use!)";
+ };
+ };
+
system.activationScripts.rootPasswd = stringAfter [ "etc" ]
''
# If there is no password file yet, create a root account with an
@@ -192,6 +258,11 @@ in
read createHome
read password
read isSystemUser
+ read createUser
+
+ if ! test "$createUser" = "yes"; then
+ continue
+ fi
if ! curEnt=$(getent passwd "$name"); then
useradd ''${isSystemUser:+--system} \
diff --git a/modules/services/networking/ssh/sshd.nix b/modules/services/networking/ssh/sshd.nix
index be1dac08022..a779580eaee 100644
--- a/modules/services/networking/ssh/sshd.nix
+++ b/modules/services/networking/ssh/sshd.nix
@@ -14,6 +14,98 @@ let
v == "forced-commands-only" ||
v == "no";
+ userOptions = {
+ openssh.authorizedKeys = {
+
+ preserveExistingKeys = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ If this option is enabled, the keys specified in
+ keys and/or keyFiles will be
+ placed in a special section of the user's authorized_keys file
+ and any existing keys will be preserved. That section will be
+ regenerated each time NixOS is activated. However, if
+ preserveExisting isn't enabled, the complete file
+ will be generated, and any user modifications will be wiped out.
+ '';
+ };
+
+ keys = mkOption {
+ type = types.listOf types.string;
+ default = [];
+ description = ''
+ A list of verbatim OpenSSH public keys that should be inserted into the
+ user's authorized_keys file. You can combine the keys and
+ keyFiles options.
+ '';
+ };
+
+ keyFiles = mkOption {
+ type = types.listOf types.string;
+ default = [];
+ description = ''
+ A list of files each containing one OpenSSH public keys that should be
+ inserted into the user's authorized_keys file. You can combine
+ the keyFiles and
+ keys options.
+ '';
+ };
+
+ };
+ };
+
+ mkAuthkeyScript =
+ let
+ marker1 = "### NixOS will regenerate this line and every line below it.";
+ marker2 = "### NixOS will regenerate this file. Do not edit!";
+ users = map (userName: getAttr userName config.users.extraUsers) (attrNames config.users.extraUsers);
+ usersWithKeys = flip filter users (u:
+ length u.openssh.authorizedKeys.keys != 0 || length u.openssh.authorizedKeys.keyFiles != 0
+ );
+ userLoop = flip concatMapStrings usersWithKeys (u:
+ let
+ authKeys = concatStringsSep "," u.openssh.authorizedKeys.keys;
+ authKeyFiles = concatStringsSep "," u.openssh.authorizedKeys.keyFiles;
+ preserveExisting = if u.openssh.authorizedKeys.preserveExistingKeys then "true" else "false";
+ in ''
+ mkAuthKeysFile "${u.name}" "${authKeys}" "${authKeyFiles}" "${preserveExisting}"
+ ''
+ );
+ in ''
+ mkAuthKeysFile() {
+ local userName="$1"
+ local authKeys="$2"
+ local authKeyFiles="$3"
+ local preserveExisting="$4"
+ IFS=","
+
+ for f in $authKeyFiles; do
+ if [ -f "$f" ]; then
+ authKeys="$(${pkgs.coreutils}/bin/cat "$f"),$authKeys"
+ fi
+ done
+
+ if [ -n "$authKeys" ]; then
+ eval authfile=~$userName/.ssh/authorized_keys
+ ${pkgs.coreutils}/bin/mkdir -p "$(dirname $authfile)"
+ ${pkgs.coreutils}/bin/touch "$authfile"
+ if [ "$preserveExisting" == "false" ]; then
+ rm -f "$authfile"
+ authKeys="${marker2},$authKeys"
+ else
+ ${pkgs.gnused}/bin/sed -i '/^### NixOS.*$/,$d' "$authfile"
+ authKeys="${marker1},$authKeys"
+ fi
+ for key in $authKeys; do ${pkgs.coreutils}/bin/echo "$key" >> "$authfile"; done
+ fi
+
+ unset IFS
+ }
+ ${userLoop}
+ '';
+
+
in
{
@@ -102,6 +194,10 @@ in
};
+ users.extraUsers = mkOption {
+ options = [ userOptions ];
+ };
+
};
@@ -135,6 +231,8 @@ in
preStart =
''
+ ${mkAuthkeyScript}
+
mkdir -m 0755 -p /etc/ssh
if ! test -f /etc/ssh/ssh_host_dsa_key; then