diff --git a/nixos/modules/config/users-groups.nix b/nixos/modules/config/users-groups.nix index 714de646eb7a..97bf67262795 100644 --- a/nixos/modules/config/users-groups.nix +++ b/nixos/modules/config/users-groups.nix @@ -5,7 +5,7 @@ with pkgs.lib; let ids = config.ids; - users = config.users; + cfg = config.users; userOpts = { name, config, ... }: { @@ -28,9 +28,8 @@ let }; uid = mkOption { - type = with types; uniq (nullOr int); - default = null; - description = "The account UID. If undefined, NixOS will select a free UID."; + type = with types; uniq int; + description = "The account UID."; }; group = mkOption { @@ -60,13 +59,21 @@ let createHome = mkOption { type = types.bool; default = false; - description = "If true, the home directory will be created automatically."; + description = '' + If true, the home directory will be created automatically. If this + option is true and the home directory already exists but is not + owned by the user, directory owner and group will be changed to + match the user. + ''; }; useDefaultShell = mkOption { type = types.bool; default = false; - description = "If true, the user's shell will be set to <literal>users.defaultUserShell</literal>."; + description = '' + If true, the user's shell will be set to + <literal>cfg.defaultUserShell</literal>. + ''; }; password = mkOption { @@ -78,13 +85,29 @@ let because it is world-readable in the Nix store. This option should only be used for public accounts such as <literal>guest</literal>. + The option <literal>password</literal> overrides + <literal>passwordFile</literal>, if both are specified. + If none of the options <literal>password</literal> or + <literal>passwordFile</literal> are specified, the user account will + be locked for password logins. This is the default behavior except + for the root account, which has an empty password by default. If you + want to lock the root account for password logins, set + <literal>users.extraUsers.root.password</literal> to + <literal>null</literal>. ''; }; - isSystemUser = mkOption { - type = types.bool; - default = true; - description = "Indicates if the user is a system user or not."; + passwordFile = mkOption { + type = with types; uniq (nullOr string); + default = null; + description = '' + The 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 + that is suitable for the <literal>chpasswd -e</literal> command. + See the <literal>password</literal> for more details on how passwords + are assigned. + ''; }; createUser = mkOption { @@ -96,19 +119,11 @@ let then not modify any of the basic properties for the user account. ''; }; - - isAlias = mkOption { - type = types.bool; - default = false; - description = "If true, the UID of this user is not required to be unique and can thus alias another user."; - }; - }; config = { name = mkDefault name; - uid = mkDefault (attrByPath [name] null ids.uids); - shell = mkIf config.useDefaultShell (mkDefault users.defaultUserShell); + shell = mkIf config.useDefaultShell (mkDefault cfg.defaultUserShell); }; }; @@ -123,28 +138,100 @@ let }; gid = mkOption { - type = with types; uniq (nullOr int); - default = null; - description = "The GID of the group. If undefined, NixOS will select a free GID."; + type = with types; uniq int; + description = "The GID of the group."; + }; + + members = mkOption { + type = with types; listOf string; + default = []; + description = '' + ''; }; }; config = { name = mkDefault name; - gid = mkDefault (attrByPath [name] null ids.gids); }; }; - # 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${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${toString u.createUser}\n${toString u.isAlias}\n"; - - usersFile = pkgs.writeText "users" ( + getGroup = gname: let - p = partition (u: u.isAlias) (attrValues config.users.extraUsers); - in concatStrings (map serializedUser p.wrong ++ map serializedUser p.right)); + groups = mapAttrsToList (n: g: g) ( + filterAttrs (n: g: g.name == gname) cfg.extraGroups + ); + in + if length groups == 1 then head groups + else if groups == [] then throw "Group ${gname} not defined" + else throw "Group ${gname} has multiple definitions"; + + getUser = uname: + let + users = mapAttrsToList (n: u: u) ( + filterAttrs (n: u: u.name == uname) cfg.extraUsers + ); + in + if length users == 1 then head users + else if users == [] then throw "User ${uname} not defined" + else throw "User ${uname} has multiple definitions"; + + mkGroupEntry = gname: + let + g = getGroup gname; + users = mapAttrsToList (n: u: u.name) ( + filterAttrs (n: u: elem g.name u.extraGroups) cfg.extraUsers + ); + in concatStringsSep ":" [ + g.name "x" (toString g.gid) + (concatStringsSep "," (users ++ (filter (u: !(elem u users)) g.members))) + ]; + + mkPasswdEntry = uname: let u = getUser uname; in + concatStringsSep ":" [ + u.name "x" (toString u.uid) + (toString (getGroup u.group).gid) + u.description u.home u.shell + ]; + + sortOn = a: sort (as1: as2: lessThan (getAttr a as1) (getAttr a as2)); + + groupFile = pkgs.writeText "group" ( + concatStringsSep "\n" (map (g: mkGroupEntry g.name) ( + sortOn "gid" (attrValues cfg.extraGroups) + )) + ); + + passwdFile = pkgs.writeText "passwd" ( + concatStringsSep "\n" (map (u: mkPasswdEntry u.name) ( + sortOn "uid" (filter (u: u.createUser) (attrValues cfg.extraUsers)) + )) + ); + + # If mutableUsers is true, this script adds all users/groups defined in + # users.extra{Users,Groups} to /etc/{passwd,group} iff there isn't any + # existing user/group with the same name in those files. + # If mutableUsers is false, the /etc/{passwd,group} files will simply be + # replaced with the users/groups defined in the NixOS configuration. + # The merging procedure could certainly be improved, and instead of just + # keeping the lines as-is from /etc/{passwd,group} they could be combined + # in some way with the generated content from the NixOS configuration. + merger = src: pkgs.writeScript "merger" '' + #!${pkgs.bash}/bin/bash + + PATH=${pkgs.gawk}/bin:${pkgs.gnugrep}/bin:$PATH + + ${if !cfg.mutableUsers + then ''cp ${src} $1.tmp'' + else ''awk -F: '{ print "^"$1":.*" }' $1 | egrep -vf - ${src} | cat $1 - > $1.tmp'' + } + + # set mtime to +1, otherwise change might go unnoticed (vipw/vigr only looks at mtime) + touch -m -t $(date -d @$(($(stat -c %Y $1)+1)) +%Y%m%d%H%M.%S) $1.tmp + + mv -f $1.tmp $1 + ''; in @@ -154,6 +241,28 @@ in options = { + users.mutableUsers = mkOption { + type = types.bool; + default = true; + description = '' + If true, you are free to add new users and groups to the system + with the ordinary <literal>useradd</literal> and + <literal>groupadd</literal> commands. On system activation, the + existing contents of the <literal>/etc/passwd</literal> and + <literal>/etc/group</literal> files will be merged with the + contents generated from the <literal>users.extraUsers</literal> and + <literal>users.extraGroups</literal> options. If + <literal>mutableUsers</literal> is false, the contents of the user and + group files will simply be replaced on system activation. This also + holds for the user passwords; if this option is false, all changed + passwords will be reset according to the + <literal>users.extraUsers</literal> configuration on activation. If + this option is true, the initial password for a user will be set + according to <literal>users.extraUsers</literal>, but existing passwords + will not be changed. + ''; + }; + users.extraUsers = mkOption { default = {}; type = types.loaOf types.optionSet; @@ -188,20 +297,6 @@ in options = [ groupOpts ]; }; - security.initialRootPassword = mkOption { - type = types.str; - default = ""; - example = "!"; - description = '' - The (hashed) password for the root account set on initial - installation. The empty string denotes that root can login - locally without a password (but not via remote services such - as SSH, or indirectly via <command>su</command> or - <command>sudo</command>). The string <literal>!</literal> - prevents root from logging in using a password. - ''; - }; - }; @@ -211,144 +306,88 @@ in users.extraUsers = { root = { + uid = ids.uids.root; description = "System administrator"; home = "/root"; - shell = config.users.defaultUserShell; + shell = cfg.defaultUserShell; group = "root"; + password = mkDefault ""; }; nobody = { + uid = ids.uids.nobody; description = "Unprivileged account (don't use!)"; + group = "nogroup"; }; }; users.extraGroups = { - root = { }; - wheel = { }; - disk = { }; - kmem = { }; - tty = { }; - floppy = { }; - uucp = { }; - lp = { }; - cdrom = { }; - tape = { }; - audio = { }; - video = { }; - dialout = { }; - nogroup = { }; - users = { }; - nixbld = { }; - utmp = { }; - adm = { }; # expected by journald + 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; }; - system.activationScripts.rootPasswd = stringAfter [ "etc" ] - '' - # If there is no password file yet, create a root account with an - # empty password. - if ! test -e /etc/passwd; then - rootHome=/root - touch /etc/passwd; chmod 0644 /etc/passwd - touch /etc/group; chmod 0644 /etc/group - touch /etc/shadow; chmod 0600 /etc/shadow - # Can't use useradd, since it complains that it doesn't know us - # (bootstrap problem!). - echo "root:x:0:0:System administrator:$rootHome:${config.users.defaultUserShell}" >> /etc/passwd - echo "root:${config.security.initialRootPassword}:::::::" >> /etc/shadow - fi + system.activationScripts.users = + let + mkhomeUsers = filterAttrs (n: u: u.createHome) cfg.extraUsers; + setpwUsers = filterAttrs (n: u: u.createUser) cfg.extraUsers; + setpw = n: u: '' + setpw=yes + ${optionalString cfg.mutableUsers '' + test "$(getent shadow '${u.name}' | cut -d: -f2)" != "x" && setpw=no + ''} + if [ "$setpw" == "yes" ]; then + ${if u.password == "" + then "passwd -d '${u.name}' &>/dev/null" + else if (isNull u.password && isNull u.passwordFile) + then "passwd -l '${u.name}' &>/dev/null" + else if !(isNull u.password) + then '' + echo "${u.name}:${u.password}" | ${pkgs.shadow}/sbin/chpasswd'' + else '' + echo -n "${u.name}:" | cat - "${u.passwordFile}" | \ + ${pkgs.shadow}/sbin/chpasswd -e + '' + } + fi + ''; + mkhome = n: u: + let + uid = toString u.uid; + gid = toString ((getGroup u.group).gid); + h = u.home; + in '' + test -a "${h}" || mkdir -p "${h}" || true + test "$(stat -c %u "${h}")" = ${uid} || chown ${uid} "${h}" || true + test "$(stat -c %g "${h}")" = ${gid} || chgrp ${gid} "${h}" || true + ''; + in stringAfter [ "etc" ] '' + touch /etc/group + touch /etc/passwd + VISUAL=${merger groupFile} ${pkgs.shadow}/sbin/vigr &>/dev/null + VISUAL=${merger passwdFile} ${pkgs.shadow}/sbin/vipw &>/dev/null + ${pkgs.shadow}/sbin/grpconv + ${pkgs.shadow}/sbin/pwconv + ${concatStrings (mapAttrsToList mkhome mkhomeUsers)} + ${concatStrings (mapAttrsToList setpw setpwUsers)} ''; - # Print a reminder for users to set a root password. - environment.interactiveShellInit = - '' - if [ "$UID" = 0 ]; then - read _l < /etc/shadow - if [ "''${_l:0:6}" = root:: ]; then - cat >&2 <<EOF - [1;31mWarning:[0m Your root account has a null password, allowing local users - to login as root. Please set a non-null password using \`passwd', or - disable password-based root logins using \`passwd -l'. - EOF - fi - unset _l - fi - ''; - - system.activationScripts.users = stringAfter [ "groups" ] - '' - echo "updating users..." - - cat ${usersFile} | while true; do - read name || break - read description - read uid - read group - read extraGroups - read home - read shell - read createHome - read password - read isSystemUser - read createUser - read isAlias - - if [ -z "$createUser" ]; then - continue - fi - - if ! curEnt=$(getent passwd "$name"); then - useradd ''${isSystemUser:+--system} \ - --comment "$description" \ - ''${uid:+--uid $uid} \ - --gid "$group" \ - --groups "$extraGroups" \ - --home "$home" \ - --shell "$shell" \ - ''${createHome:+--create-home} \ - ''${isAlias:+--non-unique} \ - "$name" - if test "''${password:0:1}" = 'X'; then - (echo "''${password:1}"; echo "''${password:1}") | ${pkgs.shadow}/bin/passwd "$name" - fi - else - #echo "updating user $name..." - oldIFS="$IFS"; IFS=:; set -- $curEnt; IFS="$oldIFS" - prevUid=$3 - prevHome=$6 - # Don't change the home directory if it's the same to prevent - # unnecessary warnings about logged in users. - if test "$prevHome" = "$home"; then unset home; fi - usermod \ - --comment "$description" \ - --gid "$group" \ - --groups "$extraGroups" \ - ''${home:+--home "$home"} \ - --shell "$shell" \ - "$name" - fi - - done - ''; - - system.activationScripts.groups = stringAfter [ "rootPasswd" "binsh" "etc" "var" ] - '' - echo "updating groups..." - - createGroup() { - name="$1" - gid="$2" - - if ! curEnt=$(getent group "$name"); then - groupadd --system \ - ''${gid:+--gid $gid} \ - "$name" - fi - } - - ${flip concatMapStrings (attrValues config.users.extraGroups) (g: '' - createGroup '${g.name}' '${toString g.gid}' - '')} - ''; + # for backwards compatibility + system.activationScripts.groups = stringAfter [ "users" ] ""; }; diff --git a/nixos/modules/programs/shadow.nix b/nixos/modules/programs/shadow.nix index 9e46ab8b298f..fdc80331a84e 100644 --- a/nixos/modules/programs/shadow.nix +++ b/nixos/modules/programs/shadow.nix @@ -94,6 +94,8 @@ in groupmems = { rootOK = true; }; groupdel = { rootOK = true; }; login = { startSession = true; allowNullPassword = true; showMotd = true; updateWtmp = true; }; + chpasswd = { rootOK = true; }; + chgpasswd = { rootOK = true; }; }; security.setuidPrograms = [ "passwd" "chfn" "su" "newgrp" ]; diff --git a/nixos/modules/virtualisation/amazon-image.nix b/nixos/modules/virtualisation/amazon-image.nix index abd2a1084bd9..701e95af7d3f 100644 --- a/nixos/modules/virtualisation/amazon-image.nix +++ b/nixos/modules/virtualisation/amazon-image.nix @@ -164,5 +164,5 @@ with pkgs.lib; # Prevent logging in as root without a password. This doesn't really matter, # since the only PAM services that allow logging in with a null # password are local ones that are inaccessible on EC2 machines. - security.initialRootPassword = "!"; + users.extraUsers.root.password = null; } diff --git a/nixos/modules/virtualisation/virtualbox-image.nix b/nixos/modules/virtualisation/virtualbox-image.nix index 71bdf31a98d2..a89c8264a33f 100644 --- a/nixos/modules/virtualisation/virtualbox-image.nix +++ b/nixos/modules/virtualisation/virtualbox-image.nix @@ -111,5 +111,5 @@ with pkgs.lib; # Prevent logging in as root without a password. For NixOps, we # don't need this because the user can login via SSH, and for the # demo images, there is a demo user account that can sudo to root. - security.initialRootPassword = "!"; + users.extraUsers.root.password = null; }