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