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.