← Back to team overview

cloud-init-dev team mailing list archive

[Merge] ~smoser/cloud-init:bug/1570325-chpasswd-hashed-passwds into cloud-init:master

 

Scott Moser has proposed merging ~smoser/cloud-init:bug/1570325-chpasswd-hashed-passwds into cloud-init:master.

Commit message:
Add support for setting hashed passwords

This change will add support for hashed passwords in cc_set_passwords.
It checks if a password is a hash with by checking that it matches
in fairly safe way, and also that the password does not have a ":" in it.

chpasswd needs to know if the password is hashed or not, so two lists
is created so chpasswd is feed with the correct one.

LP: #1570325

Requested reviews:
  cloud init development team (cloud-init-dev)
Related bugs:
  Bug #1570325 in cloud-init: "RFE: chpasswd in cloud-init should support hashed passwords"
  https://bugs.launchpad.net/cloud-init/+bug/1570325

For more details, see:
https://code.launchpad.net/~smoser/cloud-init/+git/cloud-init/+merge/321000
-- 
Your team cloud init development team is requested to review the proposed merge of ~smoser/cloud-init:bug/1570325-chpasswd-hashed-passwds into cloud-init:master.
diff --git a/cloudinit/config/cc_set_passwords.py b/cloudinit/config/cc_set_passwords.py
index 8440e59..eb0bdab 100755
--- a/cloudinit/config/cc_set_passwords.py
+++ b/cloudinit/config/cc_set_passwords.py
@@ -23,7 +23,8 @@ If the ``list`` key is provided, a list of
 ``username:password`` pairs can be specified. The usernames specified
 must already exist on the system, or have been created using the
 ``cc_users_groups`` module. A password can be randomly generated using
-``username:RANDOM`` or ``username:R``. Password ssh authentication can be
+``username:RANDOM`` or ``username:R``. A hashed password can be specified
+using ``username:$6$salt$hash``. Password ssh authentication can be
 enabled, disabled, or left to system defaults using ``ssh_pwauth``.
 
 .. note::
@@ -60,8 +61,10 @@ enabled, disabled, or left to system defaults using ``ssh_pwauth``.
             - user2:RANDOM
             - user3:password3
             - user4:R
+            - user4:$6$rL..$ej...
 """
 
+import re
 import sys
 
 from cloudinit.distros import ug_util
@@ -112,24 +115,43 @@ def handle(_name, cfg, cloud, log, args):
     errors = []
     if plist:
         plist_in = []
+        hashed_plist_in = []
+        hashed_users = []
         randlist = []
         users = []
+        prog = re.compile(r'\$[1,2a,2y,5,6](\$.+){2}')
         for line in plist:
             u, p = line.split(':', 1)
-            if p == "R" or p == "RANDOM":
-                p = rand_user_password()
-                randlist.append("%s:%s" % (u, p))
-            plist_in.append("%s:%s" % (u, p))
-            users.append(u)
+            if prog.match(p) is not None and ":" not in p:
+                hashed_plist_in.append("%s:%s" % (u, p))
+                hashed_users.append(u)
+            else:
+                if p == "R" or p == "RANDOM":
+                    p = rand_user_password()
+                    randlist.append("%s:%s" % (u, p))
+                plist_in.append("%s:%s" % (u, p))
+                users.append(u)
 
         ch_in = '\n'.join(plist_in) + '\n'
-        try:
-            log.debug("Changing password for %s:", users)
-            util.subp(['chpasswd'], ch_in)
-        except Exception as e:
-            errors.append(e)
-            util.logexc(log, "Failed to set passwords with chpasswd for %s",
-                        users)
+        if users:
+            try:
+                log.debug("Changing password for %s:", users)
+                util.subp(['chpasswd'], ch_in)
+            except Exception as e:
+                errors.append(e)
+                util.logexc(
+                    log, "Failed to set passwords with chpasswd for %s", users)
+
+        hashed_ch_in = '\n'.join(hashed_plist_in) + '\n'
+        if hashed_users:
+            try:
+                log.debug("Setting hashed password for %s:", hashed_users)
+                util.subp(['chpasswd', '-e'], hashed_ch_in)
+            except Exception as e:
+                errors.append(e)
+                util.logexc(
+                    log, "Failed to set hashed passwords with chpasswd for %s",
+                    hashed_users)
 
         if len(randlist):
             blurb = ("Set the following 'random' passwords\n",
diff --git a/doc/examples/cloud-config.txt b/doc/examples/cloud-config.txt
index c03f102..bd84c64 100644
--- a/doc/examples/cloud-config.txt
+++ b/doc/examples/cloud-config.txt
@@ -426,14 +426,21 @@ syslog_fix_perms: syslog:root
 #
 # there is also an option to set multiple users passwords, using 'chpasswd'
 # That looks like the following, with 'expire' set to 'True' by default.
-# to not expire users passwords, set 'expire' to 'False':
+# to not expire users passwords, set 'expire' to 'False'. Also possible
+# to set hashed password, here account 'user3' has a password it set to
+# 'cloud-init', hashed with SHA-256:
 # chpasswd:
 #  list: |
 #    user1:password1
 #    user2:RANDOM
+#    user3:$5$eriogqzq$Dg7PxHsKGzziuEGkZgkLvacjuEFeljJ.rLf.hZqKQLA
 #  expire: True
 # ssh_pwauth: [ True, False, "" or "unchanged" ]
 #
+# Hashed passwords can be generated in multiple ways, example with python3:
+# python3 -c 'import crypt,getpass; print(crypt.crypt(getpass.getpass(), crypt.mksalt(crypt.METHOD_SHA512)))'
+# Newer versions of 'mkpasswd' will also work: mkpasswd -m sha-512 password
+#
 # So, a simple working example to allow login via ssh, and not expire
 # for the default user would look like:
 password: passw0rd
diff --git a/tests/cloud_tests/configs/modules/set_password_list.yaml b/tests/cloud_tests/configs/modules/set_password_list.yaml
index a1eadd7..a2a89c9 100644
--- a/tests/cloud_tests/configs/modules/set_password_list.yaml
+++ b/tests/cloud_tests/configs/modules/set_password_list.yaml
@@ -21,11 +21,14 @@ cloud_config: |
       # sha256 gojanego
       passwd: "$5$iW$XsxmWCdpwIW8Yhv.Jn/R3uk6A4UaicfW5Xp7C9p9pg."
       lock_passwd: false
+    - name: "mikey"
+      lock_passwd: false
   chpasswd:
     list:
       - tom:mypassword123!
       - dick:RANDOM
       - harry:RANDOM
+      - mikey:$5$xZ$B2YGGEx2AOf4PeW48KC6.QyT1W2B4rZ9Qbltudtha89
 collect_scripts:
   shadow: |
     #!/bin/bash
diff --git a/tests/cloud_tests/configs/modules/set_password_list_string.yaml b/tests/cloud_tests/configs/modules/set_password_list_string.yaml
index cbb71be..c2a0f63 100644
--- a/tests/cloud_tests/configs/modules/set_password_list_string.yaml
+++ b/tests/cloud_tests/configs/modules/set_password_list_string.yaml
@@ -21,11 +21,14 @@ cloud_config: |
       # sha256 gojanego
       passwd: "$5$iW$XsxmWCdpwIW8Yhv.Jn/R3uk6A4UaicfW5Xp7C9p9pg."
       lock_passwd: false
+    - name: "mikey"
+      lock_passwd: false
   chpasswd:
     list: |
       tom:mypassword123!
       dick:RANDOM
       harry:RANDOM
+      mikey:$5$xZ$B2YGGEx2AOf4PeW48KC6.QyT1W2B4rZ9Qbltudtha89
 collect_scripts:
   shadow: |
     #!/bin/bash
diff --git a/tests/cloud_tests/testcases/base.py b/tests/cloud_tests/testcases/base.py
index 51ce2b4..64d5507 100644
--- a/tests/cloud_tests/testcases/base.py
+++ b/tests/cloud_tests/testcases/base.py
@@ -98,6 +98,9 @@ class PasswordListTest(CloudTestCase):
         self.assertEqual([], dupes)
         self.assertEqual(jane_enc, users['jane'])
 
+        mikey_enc = "$5$xZ$B2YGGEx2AOf4PeW48KC6.QyT1W2B4rZ9Qbltudtha89"
+        self.assertEqual(mikey_enc, users['mikey'])
+
         # shadow entry is $N$salt$, so we encrypt with the same format
         # and salt and expect the result.
         tom = "mypassword123!"
@@ -124,6 +127,7 @@ class PasswordListTest(CloudTestCase):
         self.assertIn('dick:', out)
         self.assertIn('harry:', out)
         self.assertIn('jane:', out)
+        self.assertIn('mikey:', out)
 
     def test_sshd_config(self):
         """Test sshd config allows passwords"""

References