summary refs log tree commit diff
path: root/nixos/modules/config/users-groups.nix
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/modules/config/users-groups.nix')
-rw-r--r--nixos/modules/config/users-groups.nix113
1 files changed, 77 insertions, 36 deletions
diff --git a/nixos/modules/config/users-groups.nix b/nixos/modules/config/users-groups.nix
index 141e43fec39..56b7af98b61 100644
--- a/nixos/modules/config/users-groups.nix
+++ b/nixos/modules/config/users-groups.nix
@@ -6,6 +6,16 @@ let
   ids = config.ids;
   cfg = config.users;
 
+  # Check whether a password hash will allow login.
+  allowsLogin = hash:
+    hash == "" # login without password
+    || !(lib.elem hash
+      [ null   # password login disabled
+        "!"    # password login disabled
+        "!!"   # a variant of "!"
+        "*"    # password unset
+      ]);
+
   passwordDescription = ''
     The options <option>hashedPassword</option>,
     <option>password</option> and <option>passwordFile</option>
@@ -25,8 +35,19 @@ let
   '';
 
   hashedPasswordDescription = ''
-    To generate hashed password install <literal>mkpasswd</literal>
+    To generate a hashed password install the <literal>mkpasswd</literal>
     package and run <literal>mkpasswd -m sha-512</literal>.
+
+    If set to an empty string (<literal>""</literal>), this user will
+    be able to log in without being asked for a password (but not via remote
+    services such as SSH, or indirectly via <command>su</command> or
+    <command>sudo</command>). This should only be used for e.g. bootable
+    live systems. Note: this is different from setting an empty password,
+    which ca be achieved using <option>users.users.&lt;name?&gt;.password</option>.
+
+    If set to <literal>null</literal> (default) this user will not
+    be able to log in using a password (i.e. via <command>login</command>
+    command).
   '';
 
   userOpts = { name, config, ... }: {
@@ -354,18 +375,6 @@ let
     };
   };
 
-  mkSubuidEntry = user: concatStrings (
-    map (range: "${user.name}:${toString range.startUid}:${toString range.count}\n")
-      user.subUidRanges);
-
-  subuidFile = concatStrings (map mkSubuidEntry (attrValues cfg.users));
-
-  mkSubgidEntry = user: concatStrings (
-    map (range: "${user.name}:${toString range.startGid}:${toString range.count}\n")
-        user.subGidRanges);
-
-  subgidFile = concatStrings (map mkSubgidEntry (attrValues cfg.users));
-
   idsAreUnique = set: idAttr: !(fold (name: args@{ dup, acc }:
     let
       id = builtins.toString (builtins.getAttr idAttr (builtins.getAttr name set));
@@ -385,6 +394,7 @@ let
       { inherit (u)
           name uid group description home createHome isSystemUser
           password passwordFile hashedPassword
+          isNormalUser subUidRanges subGidRanges
           initialPassword initialHashedPassword;
         shell = utils.toShellPath u.shell;
       }) cfg.users;
@@ -406,6 +416,12 @@ in {
   imports = [
     (mkAliasOptionModule [ "users" "extraUsers" ] [ "users" "users" ])
     (mkAliasOptionModule [ "users" "extraGroups" ] [ "users" "groups" ])
+    (mkChangedOptionModule
+      [ "security" "initialRootPassword" ]
+      [ "users" "users" "root" "initialHashedPassword" ]
+      (cfg: if cfg.security.initialRootPassword == "!"
+            then null
+            else cfg.security.initialRootPassword))
   ];
 
   ###### interface
@@ -477,14 +493,6 @@ in {
       '';
     };
 
-    # FIXME: obsolete - will remove.
-    security.initialRootPassword = mkOption {
-      type = types.str;
-      default = "!";
-      example = "";
-      visible = false;
-    };
-
   };
 
 
@@ -499,7 +507,6 @@ in {
         home = "/root";
         shell = mkDefault cfg.defaultUserShell;
         group = "root";
-        initialHashedPassword = mkDefault config.security.initialRootPassword;
       };
       nobody = {
         uid = ids.uids.nobody;
@@ -549,16 +556,7 @@ in {
     # Install all the user shells
     environment.systemPackages = systemShells;
 
-    environment.etc = {
-      subuid = {
-        text = subuidFile;
-        mode = "0644";
-      };
-      subgid = {
-        text = subgidFile;
-        mode = "0644";
-      };
-    } // (mapAttrs' (name: { packages, ... }: {
+    environment.etc = (mapAttrs' (name: { packages, ... }: {
       name = "profiles/per-user/${name}";
       value.source = pkgs.buildEnv {
         name = "user-environment";
@@ -583,22 +581,65 @@ 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:
+          any id ((mapAttrsToList (name: cfg:
             (name == "root"
              || cfg.group == "wheel"
              || elem "wheel" cfg.extraGroups)
             &&
-            ((cfg.hashedPassword != null && cfg.hashedPassword != "!")
+            (allowsLogin cfg.hashedPassword
              || cfg.password != null
              || cfg.passwordFile != null
              || cfg.openssh.authorizedKeys.keys != []
              || cfg.openssh.authorizedKeys.keyFiles != [])
-          ) cfg.users);
+          ) cfg.users) ++ [
+            config.security.googleOsLogin.enable
+          ]);
         message = ''
           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.'';
       }
-    ];
+    ] ++ flip mapAttrsToList cfg.users (name: user:
+      {
+        assertion = (user.hashedPassword != null)
+                    -> (builtins.match ".*:.*" user.hashedPassword == null);
+        message = ''
+          The password hash of user "${name}" contains a ":" character.
+          This is invalid and would break the login system because the fields
+          of /etc/shadow (file where hashes are stored) are colon-separated.
+          Please check the value of option `users.users."${name}".hashedPassword`.'';
+      }
+    );
+
+    warnings =
+      builtins.filter (x: x != null) (
+        flip mapAttrsToList cfg.users (name: user:
+        # This regex matches a subset of the Modular Crypto Format (MCF)[1]
+        # informal standard. Since this depends largely on the OS or the
+        # specific implementation of crypt(3) we only support the (sane)
+        # schemes implemented by glibc and BSDs. In particular the original
+        # DES hash is excluded since, having no structure, it would validate
+        # common mistakes like typing the plaintext password.
+        #
+        # [1]: https://en.wikipedia.org/wiki/Crypt_(C)
+        let
+          sep = "\\$";
+          base64 = "[a-zA-Z0-9./]+";
+          id = "[a-z0-9-]+";
+          value = "[a-zA-Z0-9/+.-]+";
+          options = "${id}(=${value})?(,${id}=${value})*";
+          scheme  = "${id}(${sep}${options})?";
+          content = "${base64}${sep}${base64}";
+          mcf = "^${sep}${scheme}${sep}${content}$";
+        in
+        if (allowsLogin user.hashedPassword
+            && user.hashedPassword != ""  # login without password
+            && builtins.match mcf user.hashedPassword == null)
+        then ''
+          The password hash of user "${name}" may be invalid. You must set a
+          valid hash or the user will be locked out of their account. Please
+          check the value of option `users.users."${name}".hashedPassword`.''
+        else null
+      ));
 
   };