diff --git a/nixos/module-tests/mutable-users.nix b/nixos/module-tests/mutable-users.nix new file mode 100644 index 00000000000000..e8ba79d8af58b4 --- /dev/null +++ b/nixos/module-tests/mutable-users.nix @@ -0,0 +1,75 @@ +let + ensurePass = name: config: + builtins.seq + (builtins.unsafeDiscardOutputDependency + (import ../lib/eval-config.nix { + modules = [ + config + ({ + fileSystems."/".device = "/dev/bogus"; + boot.loader.grub.device = "/dev/bogus"; + }) + ]; + }).config.system.build.toplevel.drvPath + ) "ok" ; + + + ensureFail = name: config: + if (builtins.tryEval (ensurePass name config)).success == false + then "ok" + else throw "unexpected success in ${name}"; +in { + user-with-password = ensurePass "user-with-password" { + # services.openssh.enable = true; + users.mutableUsers = false; + users.users.foo = { + password = "foob"; + extraGroups = [ "wheel" ]; + }; + users.users.root.hashedPassword = null; + }; + + root-only-no-password = ensureFail "root-only-no-password" { + users.mutableUsers = false; + users.users.root.hashedPassword = null; + }; + + root-only-with-bang-password = ensureFail "root-only-with-bang-password" { + users.mutableUsers = false; + users.users.root.hashedPassword = "!"; + }; + + root-only-with-real-password = ensurePass "root-only-with-real-password" { + users.mutableUsers = false; + users.users.root.hashedPassword = "w00t"; + }; + + root-only-with-real-password-ssh-no-login = ensureFail "root-only-with-real-password-ssh-no-login" { + users.mutableUsers = false; + users.users.root.hashedPassword = "w00t"; + services.openssh.enable = true; + services.openssh.permitRootLogin = "no"; + }; + + root-only-with-real-password-ssh-without-pass-login = ensureFail "root-only-with-real-password-ssh-without-pass-login" { + users.mutableUsers = false; + users.users.root.hashedPassword = "w00t"; + services.openssh.enable = true; + services.openssh.permitRootLogin = "without-password"; + }; + + root-only-with-real-password-ssh-prohibit-password-login = ensureFail "root-only-with-real-password-ssh-prohibit-password-login" { + users.mutableUsers = false; + users.users.root.hashedPassword = "w00t"; + services.openssh.enable = true; + services.openssh.permitRootLogin = "prohibit-password"; + }; + + root-only-with-real-password-ssh-no-login-ssh-disabled = ensurePass "root-only-with-real-password-ssh-no-login-ssh-disabled" { + users.mutableUsers = false; + users.users.root.hashedPassword = "w00t"; + services.openssh.enable = false; + services.openssh.permitRootLogin = "no"; + }; + +} diff --git a/nixos/modules/config/users-groups.nix b/nixos/modules/config/users-groups.nix index a4715175cc952f..2dd15cc96e9598 100644 --- a/nixos/modules/config/users-groups.nix +++ b/nixos/modules/config/users-groups.nix @@ -557,18 +557,77 @@ in { # password or an SSH authorized key. Privileged accounts are # root and users in the wheel group. assertion = !cfg.mutableUsers -> - any id (mapAttrsToList (name: cfg: - (name == "root" - || cfg.group == "wheel" - || elem "wheel" cfg.extraGroups) - && - ((cfg.hashedPassword != null && cfg.hashedPassword != "!") - || cfg.password != null - || cfg.passwordFile != null - || cfg.openssh.authorizedKeys.keys != [] - || cfg.openssh.authorizedKeys.keyFiles != []) - ) cfg.users); + (let + userHasKeys = cfg: + ( + cfg.openssh.authorizedKeys.keys != [] + || cfg.openssh.authorizedKeys.keyFiles != [] + ); + + userHasPassword = cfg: + ( + (cfg.hashedPassword != null && cfg.hashedPassword != "!") + || cfg.password != null + || cfg.passwordFile != null + ); + + userHasSSHCreds = name: cfg: + let + ssh = config.services.openssh; + passwordWorks = if ssh.passwordAuthentication + then userHasPassword cfg + else false; + + keyWorks = userHasKeys cfg; + in if name == "root" then + ( + # Is root allowed explicitly to use a password? + if ssh.permitRootLogin == "yes" + then passwordWorks || keyWorks + else + # Is root allowed explicitly denied from using a password? + if (ssh.permitRootLogin == "without-password" + || ssh.permitRootLogin == "prohibit-password") + then keyWorks + else + # Forced commands don't count as being allowed in + # and no means no + if (ssh.permitRootLogin == "forced-command" + || ssh.permitRootLogin == "no") + then false + else + builtins.trace ("Cannot handle openssh.permitRootLogin" + + " = ${ssh.permitRootLogin} in determining if it" + + " can be used for SSH login. Assuming no, for" + + " safety.") false + ) + else passwordWorks || keyWorks; + + userIsWheel = cfg: + ( + cfg.group == "wheel" + || elem "wheel" cfg.extraGroups + ); + + userCanBecomeRoot = name: cfg: + if name == "root" + then true + else if config.security.sudo.enable == false + then false + else if config.security.sudo.wheelNeedsPassword + then (userIsWheel cfg && userHasPassword cfg) + else userIsWheel cfg; + + in any id (mapAttrsToList (name: cfg: + userCanBecomeRoot name cfg + && ( + if config.services.openssh.enable + then userHasSSHCreds name cfg + else userHasPassword cfg + ) + ) cfg.users)); message = '' + No account can log in at the console or over SSH. Please set Neither the root account nor any wheel user has a password or SSH authorized key. You must set one to prevent being locked out of your system.''; } diff --git a/nixos/modules/services/networking/ssh/sshd.nix b/nixos/modules/services/networking/ssh/sshd.nix index 8828429a8178b5..335f577bd21c1d 100644 --- a/nixos/modules/services/networking/ssh/sshd.nix +++ b/nixos/modules/services/networking/ssh/sshd.nix @@ -21,7 +21,7 @@ let daemon reads in addition to the the user's authorized_keys file. You can combine the keys and keyFiles options. - Warning: If you are using NixOps then don't use this + Warning: If you are using NixOps then don't use this option since it will replace the key required for deployment via ssh. ''; }; @@ -114,6 +114,8 @@ in permitRootLogin = mkOption { default = "prohibit-password"; + # When changing the allowed types, you must also update + # userHasSSHCreds in nixos/modules/config/users-groups.nix! type = types.enum ["yes" "without-password" "prohibit-password" "forced-commands-only" "no"]; description = '' Whether the root user can login using ssh.