Blob Blame History Raw
From 857009723f14e9ad2f5f4c8614d72982b00ec27d Mon Sep 17 00:00:00 2001
From: Emanuele Giuseppe Esposito <eesposit@redhat.com>
Date: Mon, 12 Jul 2021 21:47:37 +0200
Subject: [PATCH 2/2] ssh-util: allow cloudinit to merge all ssh keys into a
 custom user file, defined in AuthorizedKeysFile (#937)

RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
RH-MergeRequest: 5: ssh-util: allow cloudinit to merge all ssh keys into a custom user file, defined in AuthorizedKeysFile (#937)
RH-Commit: [1/1] 3ed352e47c34e2ed2a1f9f5d68bc8b8f9a1365a6 (eesposit/cloud-init-centos-)
RH-Bugzilla: 1979099
RH-Acked-by: Miroslav Rezanina <mrezanin@redhat.com>
RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>

Conflicts: upstream patch modifies tests/integration_tests/util.py, that is
not present in RHEL.

commit 9b52405c6f0de5e00d5ee9c1d13540425d8f6bf5
Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
Date:   Mon Jul 12 20:21:02 2021 +0200

    ssh-util: allow cloudinit to merge all ssh keys into a custom user file, defined in AuthorizedKeysFile (#937)

    This patch aims to fix LP1911680, by analyzing the files provided
    in sshd_config and merge all keys into an user-specific file. Also
    introduces additional tests to cover this specific case.

    The file is picked by analyzing the path given in AuthorizedKeysFile.

    If it points inside the current user folder (path is /home/user/*), it
    means it is an user-specific file, so we can copy all user-keys there.
    If it contains a %u or %h, it means that there will be a specific
    authorized_keys file for each user, so we can copy all user-keys there.
    If no path points to an user-specific file, for example when only
    /etc/ssh/authorized_keys is given, default to ~/.ssh/authorized_keys.
    Note that if there are more than a single user-specific file, the last
    one will be picked.

    Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
    Co-authored-by: James Falcon <therealfalcon@gmail.com>

    LP: #1911680
    RHBZ:1862967

Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com>
---
 cloudinit/ssh_util.py                         |  22 +-
 .../assets/keys/id_rsa.test1                  |  38 +++
 .../assets/keys/id_rsa.test1.pub              |   1 +
 .../assets/keys/id_rsa.test2                  |  38 +++
 .../assets/keys/id_rsa.test2.pub              |   1 +
 .../assets/keys/id_rsa.test3                  |  38 +++
 .../assets/keys/id_rsa.test3.pub              |   1 +
 .../modules/test_ssh_keysfile.py              |  85 ++++++
 tests/unittests/test_sshutil.py               | 246 +++++++++++++++++-
 9 files changed, 456 insertions(+), 14 deletions(-)
 create mode 100644 tests/integration_tests/assets/keys/id_rsa.test1
 create mode 100644 tests/integration_tests/assets/keys/id_rsa.test1.pub
 create mode 100644 tests/integration_tests/assets/keys/id_rsa.test2
 create mode 100644 tests/integration_tests/assets/keys/id_rsa.test2.pub
 create mode 100644 tests/integration_tests/assets/keys/id_rsa.test3
 create mode 100644 tests/integration_tests/assets/keys/id_rsa.test3.pub
 create mode 100644 tests/integration_tests/modules/test_ssh_keysfile.py

diff --git a/cloudinit/ssh_util.py b/cloudinit/ssh_util.py
index c08042d6..89057262 100644
--- a/cloudinit/ssh_util.py
+++ b/cloudinit/ssh_util.py
@@ -252,13 +252,15 @@ def render_authorizedkeysfile_paths(value, homedir, username):
 def extract_authorized_keys(username, sshd_cfg_file=DEF_SSHD_CFG):
     (ssh_dir, pw_ent) = users_ssh_info(username)
     default_authorizedkeys_file = os.path.join(ssh_dir, 'authorized_keys')
+    user_authorizedkeys_file = default_authorizedkeys_file
     auth_key_fns = []
     with util.SeLinuxGuard(ssh_dir, recursive=True):
         try:
             ssh_cfg = parse_ssh_config_map(sshd_cfg_file)
+            key_paths = ssh_cfg.get("authorizedkeysfile",
+                                    "%h/.ssh/authorized_keys")
             auth_key_fns = render_authorizedkeysfile_paths(
-                ssh_cfg.get("authorizedkeysfile", "%h/.ssh/authorized_keys"),
-                pw_ent.pw_dir, username)
+                key_paths, pw_ent.pw_dir, username)
 
         except (IOError, OSError):
             # Give up and use a default key filename
@@ -267,8 +269,22 @@ def extract_authorized_keys(username, sshd_cfg_file=DEF_SSHD_CFG):
                         "config from %r, using 'AuthorizedKeysFile' file "
                         "%r instead", DEF_SSHD_CFG, auth_key_fns[0])
 
+    # check if one of the keys is the user's one
+    for key_path, auth_key_fn in zip(key_paths.split(), auth_key_fns):
+        if any([
+            '%u' in key_path,
+            '%h' in key_path,
+            auth_key_fn.startswith('{}/'.format(pw_ent.pw_dir))
+        ]):
+            user_authorizedkeys_file = auth_key_fn
+
+    if user_authorizedkeys_file != default_authorizedkeys_file:
+        LOG.debug(
+            "AuthorizedKeysFile has an user-specific authorized_keys, "
+            "using %s", user_authorizedkeys_file)
+
     # always store all the keys in the user's private file
-    return (default_authorizedkeys_file, parse_authorized_keys(auth_key_fns))
+    return (user_authorizedkeys_file, parse_authorized_keys(auth_key_fns))
 
 
 def setup_user_keys(keys, username, options=None):
diff --git a/tests/integration_tests/assets/keys/id_rsa.test1 b/tests/integration_tests/assets/keys/id_rsa.test1
new file mode 100644
index 00000000..bd4c822e
--- /dev/null
+++ b/tests/integration_tests/assets/keys/id_rsa.test1
@@ -0,0 +1,38 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+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
+CHbTWbsFrHR1qFAAAACmphbWVzQG5ld3Q=
+-----END OPENSSH PRIVATE KEY-----
diff --git a/tests/integration_tests/assets/keys/id_rsa.test1.pub b/tests/integration_tests/assets/keys/id_rsa.test1.pub
new file mode 100644
index 00000000..3d2e26e1
--- /dev/null
+++ b/tests/integration_tests/assets/keys/id_rsa.test1.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC1GUb3ponbdRG8CA79sGy4uX6Wq5zfnRpLBX3z+Ly+W86fkZVwfJ63+tdCDiVnizSjAbeRbJAVjye9OT4b5Q1Tx83RqcDMEOL7gzOx6OQX2xyNMwWZdWsikgKpXdthA2M+/qv65tB2QPTV27d4H23n/OXziC2yGnSEF2h/LPy7X8Dbmzt9VTrsbIjs90q9pL7K8lIidmdvZ71VDN8kL6oCc3uBRlq0EqN2BS0l6JRR4NpWdpLXtvRqhP2IE7SwhxC05MGViUKMW3VMAwtnrgePdJ8ZkHtiPRn4vMUHYhZfXPOyMjrxiRg8o5Iv81iFBT17nmQlDtaeeA0dj/af8Dt00pIUOaAzlz0haB5nt+Omx8hgCgn8IDEUg0P48Qd3WAJLrGgSFpRXKHlYf9eTAq/WfHgCJnYEUIT7vmTJJEtlQn7nLSvcNTmS0xd5RHNUg/um7RHwMK6X4ZT4z/5MfJa0AQ6B5o0np9imjaKkOFuZNi3eSTaUwhqlHEYEWz7fux0= test1@host
diff --git a/tests/integration_tests/assets/keys/id_rsa.test2 b/tests/integration_tests/assets/keys/id_rsa.test2
new file mode 100644
index 00000000..5854d901
--- /dev/null
+++ b/tests/integration_tests/assets/keys/id_rsa.test2
@@ -0,0 +1,38 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+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
+tZRSm2GEgnDGAvAAAACmphbWVzQG5ld3Q=
+-----END OPENSSH PRIVATE KEY-----
diff --git a/tests/integration_tests/assets/keys/id_rsa.test2.pub b/tests/integration_tests/assets/keys/id_rsa.test2.pub
new file mode 100644
index 00000000..f3831a57
--- /dev/null
+++ b/tests/integration_tests/assets/keys/id_rsa.test2.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC8rnQPY9Y5ziKTIdVElLq0OGrOMvlwqKK8gPinVfwFgJXDzdcAQY4uci1TJVcc0AOWHp+lWrU1joDYlW3KCg8XpkXHymHshYyaeEN2fEsvIaxuF3UzW2JckP9H7dacYdEng8qtBq8wuCodGuJ5XdBVV+MVJ6jqNf/hOu4/pma8hMxlYmtdoamHEn+k/KQR2Q6LwCYpT0U2+KiPI9Lac22P0H/rfkh2Ba+t3tV/lhyzD0xHnktZKJ3BrznpEfrZiXoukQZMebWUmIyTW7zwk9Kq+iGFpSQsqQWlxDBwHSbvpbUo7KWUmyZfxs1euVmwj5aKJgjteXm9CbbXsMS415q+08C0MzG7weZODZQnnk1rF6Ft97aXHaTmRgYbDeLU7U5U3alnb84HvUu5xh3/mrE9rPTfCzBwY4lgY+Q1zjS90RL9Jxzti3weydm7OqTI6DOfQeJQLOhhRgthOkt/7IabDFLIARjTnpo5+QKxugoc4qI6aUnE1plkVCRfV696ngM= test2@host
diff --git a/tests/integration_tests/assets/keys/id_rsa.test3 b/tests/integration_tests/assets/keys/id_rsa.test3
new file mode 100644
index 00000000..2596c762
--- /dev/null
+++ b/tests/integration_tests/assets/keys/id_rsa.test3
@@ -0,0 +1,38 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+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
+eauhOY52PS/BcAAAAKamFtZXNAbmV3dAE=
+-----END OPENSSH PRIVATE KEY-----
diff --git a/tests/integration_tests/assets/keys/id_rsa.test3.pub b/tests/integration_tests/assets/keys/id_rsa.test3.pub
new file mode 100644
index 00000000..057db632
--- /dev/null
+++ b/tests/integration_tests/assets/keys/id_rsa.test3.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCk8bgx2RhAoPnv+qt4WuH0ZELbbLrqpk5ZlZGMLzzuutu8HM7r2FXImlMheV4kS+laIdhyMxHkbo00Wyc21nh/EOqZJzi9wWPncmwT9c2osqrqmxdp0Jvm+Q2kZvED5nlsmXMn3r0+AkggRuBzzqx6LiSBHYy49aq1ltjkaSk0JOpa4th4Ur7XigWu4DafJYf0w4hEMcr93n3umwfL4tMy3KY7tk+E7nvVpsrDqO+/CJJ4PFUT2RXsoBAi4z39LeB+2+BBCuifcYvfimCYhpt1IZ1t8eoYOiivImGVuq4lkQsE3bfoqBE1MICNW9TOONCvV5E/fGywbBU7oQIEYJAuX9piiACC3CfCP2Jeq1+IsrcTCWCOq5Tj4lDQJW0uEbeAhkH6qgxOFyjSmYdOuO19ALCvO2wEEW6dJrb8Ky25XWZDk7lmtnoAKj3I9EE2Sezr4KIp20y0K3OLeTZEPP2OliY9xqTTcuvsiR0LVOnr6MD7//I8dI7WjgKpbz0hDM0= test3@host
diff --git a/tests/integration_tests/modules/test_ssh_keysfile.py b/tests/integration_tests/modules/test_ssh_keysfile.py
new file mode 100644
index 00000000..f82d7649
--- /dev/null
+++ b/tests/integration_tests/modules/test_ssh_keysfile.py
@@ -0,0 +1,85 @@
+import paramiko
+import pytest
+from io import StringIO
+from paramiko.ssh_exception import SSHException
+
+from tests.integration_tests.instances import IntegrationInstance
+from tests.integration_tests.util import get_test_rsa_keypair
+
+TEST_USER1_KEYS = get_test_rsa_keypair('test1')
+TEST_USER2_KEYS = get_test_rsa_keypair('test2')
+TEST_DEFAULT_KEYS = get_test_rsa_keypair('test3')
+
+USERDATA = """\
+#cloud-config
+bootcmd:
+ - sed -i 's;#AuthorizedKeysFile.*;AuthorizedKeysFile /etc/ssh/authorized_keys %h/.ssh/authorized_keys2;' /etc/ssh/sshd_config
+ssh_authorized_keys:
+ - {default}
+users:
+- default
+- name: test_user1
+  ssh_authorized_keys:
+    - {user1}
+- name: test_user2
+  ssh_authorized_keys:
+    - {user2}
+""".format(  # noqa: E501
+    default=TEST_DEFAULT_KEYS.public_key,
+    user1=TEST_USER1_KEYS.public_key,
+    user2=TEST_USER2_KEYS.public_key,
+)
+
+
+@pytest.mark.ubuntu
+@pytest.mark.user_data(USERDATA)
+def test_authorized_keys(client: IntegrationInstance):
+    expected_keys = [
+        ('test_user1', '/home/test_user1/.ssh/authorized_keys2',
+         TEST_USER1_KEYS),
+        ('test_user2', '/home/test_user2/.ssh/authorized_keys2',
+         TEST_USER2_KEYS),
+        ('ubuntu', '/home/ubuntu/.ssh/authorized_keys2',
+         TEST_DEFAULT_KEYS),
+        ('root', '/root/.ssh/authorized_keys2', TEST_DEFAULT_KEYS),
+    ]
+
+    for user, filename, keys in expected_keys:
+        contents = client.read_from_file(filename)
+        if user in ['ubuntu', 'root']:
+            # Our personal public key gets added by pycloudlib
+            lines = contents.split('\n')
+            assert len(lines) == 2
+            assert keys.public_key.strip() in contents
+        else:
+            assert contents.strip() == keys.public_key.strip()
+
+        # Ensure we can actually connect
+        ssh = paramiko.SSHClient()
+        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+        paramiko_key = paramiko.RSAKey.from_private_key(StringIO(
+            keys.private_key))
+
+        # Will fail with AuthenticationException if
+        # we cannot connect
+        ssh.connect(
+            client.instance.ip,
+            username=user,
+            pkey=paramiko_key,
+            look_for_keys=False,
+            allow_agent=False,
+        )
+
+        # Ensure other uses can't connect using our key
+        other_users = [u[0] for u in expected_keys if u[2] != keys]
+        for other_user in other_users:
+            with pytest.raises(SSHException):
+                print('trying to connect as {} with key from {}'.format(
+                    other_user, user))
+                ssh.connect(
+                    client.instance.ip,
+                    username=other_user,
+                    pkey=paramiko_key,
+                    look_for_keys=False,
+                    allow_agent=False,
+                )
diff --git a/tests/unittests/test_sshutil.py b/tests/unittests/test_sshutil.py
index fd1d1bac..bcb8044f 100644
--- a/tests/unittests/test_sshutil.py
+++ b/tests/unittests/test_sshutil.py
@@ -570,20 +570,33 @@ class TestBasicAuthorizedKeyParse(test_helpers.CiTestCase):
             ssh_util.render_authorizedkeysfile_paths(
                 "%h/.keys", "/homedirs/bobby", "bobby"))
 
+    def test_all(self):
+        self.assertEqual(
+            ["/homedirs/bobby/.keys", "/homedirs/bobby/.secret/keys",
+             "/keys/path1", "/opt/bobby/keys"],
+            ssh_util.render_authorizedkeysfile_paths(
+                "%h/.keys .secret/keys /keys/path1 /opt/%u/keys",
+                "/homedirs/bobby", "bobby"))
+
 
 class TestMultipleSshAuthorizedKeysFile(test_helpers.CiTestCase):
 
     @patch("cloudinit.ssh_util.pwd.getpwnam")
     def test_multiple_authorizedkeys_file_order1(self, m_getpwnam):
-        fpw = FakePwEnt(pw_name='bobby', pw_dir='/home2/bobby')
+        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby')
         m_getpwnam.return_value = fpw
-        authorized_keys = self.tmp_path('authorized_keys')
+        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
+
+        # /tmp/home2/bobby/.ssh/authorized_keys = rsa
+        authorized_keys = self.tmp_path('authorized_keys', dir=user_ssh_folder)
         util.write_file(authorized_keys, VALID_CONTENT['rsa'])
 
-        user_keys = self.tmp_path('user_keys')
+        # /tmp/home2/bobby/.ssh/user_keys = dsa
+        user_keys = self.tmp_path('user_keys', dir=user_ssh_folder)
         util.write_file(user_keys, VALID_CONTENT['dsa'])
 
-        sshd_config = self.tmp_path('sshd_config')
+        # /tmp/sshd_config
+        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
         util.write_file(
             sshd_config,
             "AuthorizedKeysFile %s %s" % (authorized_keys, user_keys)
@@ -593,33 +606,244 @@ class TestMultipleSshAuthorizedKeysFile(test_helpers.CiTestCase):
             fpw.pw_name, sshd_config)
         content = ssh_util.update_authorized_keys(auth_key_entries, [])
 
-        self.assertEqual("%s/.ssh/authorized_keys" % fpw.pw_dir, auth_key_fn)
+        self.assertEqual(user_keys, auth_key_fn)
         self.assertTrue(VALID_CONTENT['rsa'] in content)
         self.assertTrue(VALID_CONTENT['dsa'] in content)
 
     @patch("cloudinit.ssh_util.pwd.getpwnam")
     def test_multiple_authorizedkeys_file_order2(self, m_getpwnam):
-        fpw = FakePwEnt(pw_name='suzie', pw_dir='/home/suzie')
+        fpw = FakePwEnt(pw_name='suzie', pw_dir='/tmp/home/suzie')
         m_getpwnam.return_value = fpw
-        authorized_keys = self.tmp_path('authorized_keys')
+        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
+
+        # /tmp/home/suzie/.ssh/authorized_keys = rsa
+        authorized_keys = self.tmp_path('authorized_keys', dir=user_ssh_folder)
         util.write_file(authorized_keys, VALID_CONTENT['rsa'])
 
-        user_keys = self.tmp_path('user_keys')
+        # /tmp/home/suzie/.ssh/user_keys = dsa
+        user_keys = self.tmp_path('user_keys', dir=user_ssh_folder)
         util.write_file(user_keys, VALID_CONTENT['dsa'])
 
-        sshd_config = self.tmp_path('sshd_config')
+        # /tmp/sshd_config
+        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
         util.write_file(
             sshd_config,
-            "AuthorizedKeysFile %s %s" % (authorized_keys, user_keys)
+            "AuthorizedKeysFile %s %s" % (user_keys, authorized_keys)
         )
 
         (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
-            fpw.pw_name, sshd_config
+            fpw.pw_name, sshd_config)
+        content = ssh_util.update_authorized_keys(auth_key_entries, [])
+
+        self.assertEqual(authorized_keys, auth_key_fn)
+        self.assertTrue(VALID_CONTENT['rsa'] in content)
+        self.assertTrue(VALID_CONTENT['dsa'] in content)
+
+    @patch("cloudinit.ssh_util.pwd.getpwnam")
+    def test_multiple_authorizedkeys_file_local_global(self, m_getpwnam):
+        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby')
+        m_getpwnam.return_value = fpw
+        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
+
+        # /tmp/home2/bobby/.ssh/authorized_keys = rsa
+        authorized_keys = self.tmp_path('authorized_keys', dir=user_ssh_folder)
+        util.write_file(authorized_keys, VALID_CONTENT['rsa'])
+
+        # /tmp/home2/bobby/.ssh/user_keys = dsa
+        user_keys = self.tmp_path('user_keys', dir=user_ssh_folder)
+        util.write_file(user_keys, VALID_CONTENT['dsa'])
+
+        # /tmp/etc/ssh/authorized_keys = ecdsa
+        authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys',
+                                               dir="/tmp")
+        util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa'])
+
+        # /tmp/sshd_config
+        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
+        util.write_file(
+            sshd_config,
+            "AuthorizedKeysFile %s %s %s" % (authorized_keys_global,
+                                             user_keys, authorized_keys)
+        )
+
+        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
+            fpw.pw_name, sshd_config)
+        content = ssh_util.update_authorized_keys(auth_key_entries, [])
+
+        self.assertEqual(authorized_keys, auth_key_fn)
+        self.assertTrue(VALID_CONTENT['rsa'] in content)
+        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
+        self.assertTrue(VALID_CONTENT['dsa'] in content)
+
+    @patch("cloudinit.ssh_util.pwd.getpwnam")
+    def test_multiple_authorizedkeys_file_local_global2(self, m_getpwnam):
+        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby')
+        m_getpwnam.return_value = fpw
+        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
+
+        # /tmp/home2/bobby/.ssh/authorized_keys2 = rsa
+        authorized_keys = self.tmp_path('authorized_keys2',
+                                        dir=user_ssh_folder)
+        util.write_file(authorized_keys, VALID_CONTENT['rsa'])
+
+        # /tmp/home2/bobby/.ssh/user_keys3 = dsa
+        user_keys = self.tmp_path('user_keys3', dir=user_ssh_folder)
+        util.write_file(user_keys, VALID_CONTENT['dsa'])
+
+        # /tmp/etc/ssh/authorized_keys = ecdsa
+        authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys',
+                                               dir="/tmp")
+        util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa'])
+
+        # /tmp/sshd_config
+        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
+        util.write_file(
+            sshd_config,
+            "AuthorizedKeysFile %s %s %s" % (authorized_keys_global,
+                                             authorized_keys, user_keys)
+        )
+
+        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
+            fpw.pw_name, sshd_config)
+        content = ssh_util.update_authorized_keys(auth_key_entries, [])
+
+        self.assertEqual(user_keys, auth_key_fn)
+        self.assertTrue(VALID_CONTENT['rsa'] in content)
+        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
+        self.assertTrue(VALID_CONTENT['dsa'] in content)
+
+    @patch("cloudinit.ssh_util.pwd.getpwnam")
+    def test_multiple_authorizedkeys_file_global(self, m_getpwnam):
+        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby')
+        m_getpwnam.return_value = fpw
+
+        # /tmp/etc/ssh/authorized_keys = rsa
+        authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys',
+                                               dir="/tmp")
+        util.write_file(authorized_keys_global, VALID_CONTENT['rsa'])
+
+        # /tmp/sshd_config
+        sshd_config = self.tmp_path('sshd_config')
+        util.write_file(
+            sshd_config,
+            "AuthorizedKeysFile %s" % (authorized_keys_global)
         )
+
+        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
+            fpw.pw_name, sshd_config)
         content = ssh_util.update_authorized_keys(auth_key_entries, [])
 
         self.assertEqual("%s/.ssh/authorized_keys" % fpw.pw_dir, auth_key_fn)
         self.assertTrue(VALID_CONTENT['rsa'] in content)
+
+    @patch("cloudinit.ssh_util.pwd.getpwnam")
+    def test_multiple_authorizedkeys_file_multiuser(self, m_getpwnam):
+        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby')
+        m_getpwnam.return_value = fpw
+        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
+        # /tmp/home2/bobby/.ssh/authorized_keys2 = rsa
+        authorized_keys = self.tmp_path('authorized_keys2',
+                                        dir=user_ssh_folder)
+        util.write_file(authorized_keys, VALID_CONTENT['rsa'])
+        # /tmp/home2/bobby/.ssh/user_keys3 = dsa
+        user_keys = self.tmp_path('user_keys3', dir=user_ssh_folder)
+        util.write_file(user_keys, VALID_CONTENT['dsa'])
+
+        fpw2 = FakePwEnt(pw_name='suzie', pw_dir='/tmp/home/suzie')
+        user_ssh_folder = "%s/.ssh" % fpw2.pw_dir
+        # /tmp/home/suzie/.ssh/authorized_keys2 = ssh-xmss@openssh.com
+        authorized_keys2 = self.tmp_path('authorized_keys2',
+                                         dir=user_ssh_folder)
+        util.write_file(authorized_keys2,
+                        VALID_CONTENT['ssh-xmss@openssh.com'])
+
+        # /tmp/etc/ssh/authorized_keys = ecdsa
+        authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys2',
+                                               dir="/tmp")
+        util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa'])
+
+        # /tmp/sshd_config
+        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
+        util.write_file(
+            sshd_config,
+            "AuthorizedKeysFile %s %%h/.ssh/authorized_keys2 %s" %
+            (authorized_keys_global, user_keys)
+        )
+
+        # process first user
+        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
+            fpw.pw_name, sshd_config)
+        content = ssh_util.update_authorized_keys(auth_key_entries, [])
+
+        self.assertEqual(user_keys, auth_key_fn)
+        self.assertTrue(VALID_CONTENT['rsa'] in content)
+        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
+        self.assertTrue(VALID_CONTENT['dsa'] in content)
+        self.assertFalse(VALID_CONTENT['ssh-xmss@openssh.com'] in content)
+
+        m_getpwnam.return_value = fpw2
+        # process second user
+        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
+            fpw2.pw_name, sshd_config)
+        content = ssh_util.update_authorized_keys(auth_key_entries, [])
+
+        self.assertEqual(authorized_keys2, auth_key_fn)
+        self.assertTrue(VALID_CONTENT['ssh-xmss@openssh.com'] in content)
+        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
+        self.assertTrue(VALID_CONTENT['dsa'] in content)
+        self.assertFalse(VALID_CONTENT['rsa'] in content)
+
+    @patch("cloudinit.ssh_util.pwd.getpwnam")
+    def test_multiple_authorizedkeys_file_multiuser2(self, m_getpwnam):
+        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home/bobby')
+        m_getpwnam.return_value = fpw
+        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
+        # /tmp/home/bobby/.ssh/authorized_keys2 = rsa
+        authorized_keys = self.tmp_path('authorized_keys2',
+                                        dir=user_ssh_folder)
+        util.write_file(authorized_keys, VALID_CONTENT['rsa'])
+        # /tmp/home/bobby/.ssh/user_keys3 = dsa
+        user_keys = self.tmp_path('user_keys3', dir=user_ssh_folder)
+        util.write_file(user_keys, VALID_CONTENT['dsa'])
+
+        fpw2 = FakePwEnt(pw_name='badguy', pw_dir='/tmp/home/badguy')
+        user_ssh_folder = "%s/.ssh" % fpw2.pw_dir
+        # /tmp/home/badguy/home/bobby = ""
+        authorized_keys2 = self.tmp_path('home/bobby', dir="/tmp/home/badguy")
+
+        # /tmp/etc/ssh/authorized_keys = ecdsa
+        authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys2',
+                                               dir="/tmp")
+        util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa'])
+
+        # /tmp/sshd_config
+        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
+        util.write_file(
+            sshd_config,
+            "AuthorizedKeysFile %s %%h/.ssh/authorized_keys2 %s %s" %
+            (authorized_keys_global, user_keys, authorized_keys2)
+        )
+
+        # process first user
+        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
+            fpw.pw_name, sshd_config)
+        content = ssh_util.update_authorized_keys(auth_key_entries, [])
+
+        self.assertEqual(user_keys, auth_key_fn)
+        self.assertTrue(VALID_CONTENT['rsa'] in content)
+        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
+        self.assertTrue(VALID_CONTENT['dsa'] in content)
+
+        m_getpwnam.return_value = fpw2
+        # process second user
+        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
+            fpw2.pw_name, sshd_config)
+        content = ssh_util.update_authorized_keys(auth_key_entries, [])
+
+        # badguy should not take the key from the other user!
+        self.assertEqual(authorized_keys2, auth_key_fn)
+        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
         self.assertTrue(VALID_CONTENT['dsa'] in content)
+        self.assertFalse(VALID_CONTENT['rsa'] in content)
 
 # vi: ts=4 expandtab
-- 
2.27.0