c36ff1
From 857009723f14e9ad2f5f4c8614d72982b00ec27d Mon Sep 17 00:00:00 2001
c36ff1
From: Emanuele Giuseppe Esposito <eesposit@redhat.com>
c36ff1
Date: Mon, 12 Jul 2021 21:47:37 +0200
c36ff1
Subject: [PATCH 2/2] ssh-util: allow cloudinit to merge all ssh keys into a
c36ff1
 custom user file, defined in AuthorizedKeysFile (#937)
c36ff1
c36ff1
RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
c36ff1
RH-MergeRequest: 5: ssh-util: allow cloudinit to merge all ssh keys into a custom user file, defined in AuthorizedKeysFile (#937)
c36ff1
RH-Commit: [1/1] 3ed352e47c34e2ed2a1f9f5d68bc8b8f9a1365a6 (eesposit/cloud-init-centos-)
c36ff1
RH-Bugzilla: 1979099
c36ff1
RH-Acked-by: Miroslav Rezanina <mrezanin@redhat.com>
c36ff1
RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>
c36ff1
c36ff1
Conflicts: upstream patch modifies tests/integration_tests/util.py, that is
c36ff1
not present in RHEL.
c36ff1
c36ff1
commit 9b52405c6f0de5e00d5ee9c1d13540425d8f6bf5
c36ff1
Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
c36ff1
Date:   Mon Jul 12 20:21:02 2021 +0200
c36ff1
c36ff1
    ssh-util: allow cloudinit to merge all ssh keys into a custom user file, defined in AuthorizedKeysFile (#937)
c36ff1
c36ff1
    This patch aims to fix LP1911680, by analyzing the files provided
c36ff1
    in sshd_config and merge all keys into an user-specific file. Also
c36ff1
    introduces additional tests to cover this specific case.
c36ff1
c36ff1
    The file is picked by analyzing the path given in AuthorizedKeysFile.
c36ff1
c36ff1
    If it points inside the current user folder (path is /home/user/*), it
c36ff1
    means it is an user-specific file, so we can copy all user-keys there.
c36ff1
    If it contains a %u or %h, it means that there will be a specific
c36ff1
    authorized_keys file for each user, so we can copy all user-keys there.
c36ff1
    If no path points to an user-specific file, for example when only
c36ff1
    /etc/ssh/authorized_keys is given, default to ~/.ssh/authorized_keys.
c36ff1
    Note that if there are more than a single user-specific file, the last
c36ff1
    one will be picked.
c36ff1
c36ff1
    Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
c36ff1
    Co-authored-by: James Falcon <therealfalcon@gmail.com>
c36ff1
c36ff1
    LP: #1911680
c36ff1
    RHBZ:1862967
c36ff1
c36ff1
Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
c36ff1
Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com>
c36ff1
---
c36ff1
 cloudinit/ssh_util.py                         |  22 +-
c36ff1
 .../assets/keys/id_rsa.test1                  |  38 +++
c36ff1
 .../assets/keys/id_rsa.test1.pub              |   1 +
c36ff1
 .../assets/keys/id_rsa.test2                  |  38 +++
c36ff1
 .../assets/keys/id_rsa.test2.pub              |   1 +
c36ff1
 .../assets/keys/id_rsa.test3                  |  38 +++
c36ff1
 .../assets/keys/id_rsa.test3.pub              |   1 +
c36ff1
 .../modules/test_ssh_keysfile.py              |  85 ++++++
c36ff1
 tests/unittests/test_sshutil.py               | 246 +++++++++++++++++-
c36ff1
 9 files changed, 456 insertions(+), 14 deletions(-)
c36ff1
 create mode 100644 tests/integration_tests/assets/keys/id_rsa.test1
c36ff1
 create mode 100644 tests/integration_tests/assets/keys/id_rsa.test1.pub
c36ff1
 create mode 100644 tests/integration_tests/assets/keys/id_rsa.test2
c36ff1
 create mode 100644 tests/integration_tests/assets/keys/id_rsa.test2.pub
c36ff1
 create mode 100644 tests/integration_tests/assets/keys/id_rsa.test3
c36ff1
 create mode 100644 tests/integration_tests/assets/keys/id_rsa.test3.pub
c36ff1
 create mode 100644 tests/integration_tests/modules/test_ssh_keysfile.py
c36ff1
c36ff1
diff --git a/cloudinit/ssh_util.py b/cloudinit/ssh_util.py
c36ff1
index c08042d6..89057262 100644
c36ff1
--- a/cloudinit/ssh_util.py
c36ff1
+++ b/cloudinit/ssh_util.py
c36ff1
@@ -252,13 +252,15 @@ def render_authorizedkeysfile_paths(value, homedir, username):
c36ff1
 def extract_authorized_keys(username, sshd_cfg_file=DEF_SSHD_CFG):
c36ff1
     (ssh_dir, pw_ent) = users_ssh_info(username)
c36ff1
     default_authorizedkeys_file = os.path.join(ssh_dir, 'authorized_keys')
c36ff1
+    user_authorizedkeys_file = default_authorizedkeys_file
c36ff1
     auth_key_fns = []
c36ff1
     with util.SeLinuxGuard(ssh_dir, recursive=True):
c36ff1
         try:
c36ff1
             ssh_cfg = parse_ssh_config_map(sshd_cfg_file)
c36ff1
+            key_paths = ssh_cfg.get("authorizedkeysfile",
c36ff1
+                                    "%h/.ssh/authorized_keys")
c36ff1
             auth_key_fns = render_authorizedkeysfile_paths(
c36ff1
-                ssh_cfg.get("authorizedkeysfile", "%h/.ssh/authorized_keys"),
c36ff1
-                pw_ent.pw_dir, username)
c36ff1
+                key_paths, pw_ent.pw_dir, username)
c36ff1
 
c36ff1
         except (IOError, OSError):
c36ff1
             # Give up and use a default key filename
c36ff1
@@ -267,8 +269,22 @@ def extract_authorized_keys(username, sshd_cfg_file=DEF_SSHD_CFG):
c36ff1
                         "config from %r, using 'AuthorizedKeysFile' file "
c36ff1
                         "%r instead", DEF_SSHD_CFG, auth_key_fns[0])
c36ff1
 
c36ff1
+    # check if one of the keys is the user's one
c36ff1
+    for key_path, auth_key_fn in zip(key_paths.split(), auth_key_fns):
c36ff1
+        if any([
c36ff1
+            '%u' in key_path,
c36ff1
+            '%h' in key_path,
c36ff1
+            auth_key_fn.startswith('{}/'.format(pw_ent.pw_dir))
c36ff1
+        ]):
c36ff1
+            user_authorizedkeys_file = auth_key_fn
c36ff1
+
c36ff1
+    if user_authorizedkeys_file != default_authorizedkeys_file:
c36ff1
+        LOG.debug(
c36ff1
+            "AuthorizedKeysFile has an user-specific authorized_keys, "
c36ff1
+            "using %s", user_authorizedkeys_file)
c36ff1
+
c36ff1
     # always store all the keys in the user's private file
c36ff1
-    return (default_authorizedkeys_file, parse_authorized_keys(auth_key_fns))
c36ff1
+    return (user_authorizedkeys_file, parse_authorized_keys(auth_key_fns))
c36ff1
 
c36ff1
 
c36ff1
 def setup_user_keys(keys, username, options=None):
c36ff1
diff --git a/tests/integration_tests/assets/keys/id_rsa.test1 b/tests/integration_tests/assets/keys/id_rsa.test1
c36ff1
new file mode 100644
c36ff1
index 00000000..bd4c822e
c36ff1
--- /dev/null
c36ff1
+++ b/tests/integration_tests/assets/keys/id_rsa.test1
c36ff1
@@ -0,0 +1,38 @@
c36ff1
+-----BEGIN OPENSSH PRIVATE KEY-----
c36ff1
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
c36ff1
+NhAAAAAwEAAQAAAYEAtRlG96aJ23URvAgO/bBsuLl+lquc350aSwV98/i8vlvOn5GVcHye
c36ff1
+t/rXQg4lZ4s0owG3kWyQFY8nvTk+G+UNU8fN0anAzBDi+4MzsejkF9scjTMFmXVrIpICqV
c36ff1
+3bYQNjPv6r+ubQdkD01du3eB9t5/zl84gtshp0hBdofyz8u1/A25s7fVU67GyI7PdKvaS+
c36ff1
+yvJSInZnb2e9VQzfJC+qAnN7gUZatBKjdgUtJeiUUeDaVnaS17b0aoT9iBO0sIcQtOTBlY
c36ff1
+lCjFt1TAMLZ64Hj3SfGZB7Yj0Z+LzFB2IWX1zzsjI68YkYPKOSL/NYhQU9e55kJQ7WnngN
c36ff1
+HY/2n/A7dNKSFDmgM5c9IWgeZ7fjpsfIYAoJ/CAxFIND+PEHd1gCS6xoEhaUVyh5WH/Xkw
c36ff1
+Kv1nx4AiZ2BFCE+75kySRLZUJ+5y0r3DU5ktMXeURzVIP7pu0R8DCul+GU+M/+THyWtAEO
c36ff1
+geaNJ6fYpo2ipDhbmTYt3kk2lMIapRxGBFs+37sdAAAFgGGJssNhibLDAAAAB3NzaC1yc2
c36ff1
+EAAAGBALUZRvemidt1EbwIDv2wbLi5fparnN+dGksFffP4vL5bzp+RlXB8nrf610IOJWeL
c36ff1
+NKMBt5FskBWPJ705PhvlDVPHzdGpwMwQ4vuDM7Ho5BfbHI0zBZl1ayKSAqld22EDYz7+q/
c36ff1
+rm0HZA9NXbt3gfbef85fOILbIadIQXaH8s/LtfwNubO31VOuxsiOz3Sr2kvsryUiJ2Z29n
c36ff1
+vVUM3yQvqgJze4FGWrQSo3YFLSXolFHg2lZ2kte29GqE/YgTtLCHELTkwZWJQoxbdUwDC2
c36ff1
+euB490nxmQe2I9Gfi8xQdiFl9c87IyOvGJGDyjki/zWIUFPXueZCUO1p54DR2P9p/wO3TS
c36ff1
+khQ5oDOXPSFoHme346bHyGAKCfwgMRSDQ/jxB3dYAkusaBIWlFcoeVh/15MCr9Z8eAImdg
c36ff1
+RQhPu+ZMkkS2VCfuctK9w1OZLTF3lEc1SD+6btEfAwrpfhlPjP/kx8lrQBDoHmjSen2KaN
c36ff1
+oqQ4W5k2Ld5JNpTCGqUcRgRbPt+7HQAAAAMBAAEAAAGBAJJCTOd70AC2ptEGbR0EHHqADT
c36ff1
+Wgefy7A94tHFEqxTy0JscGq/uCGimaY7kMdbcPXT59B4VieWeAC2cuUPP0ZHQSfS5ke7oT
c36ff1
+tU3N47U+0uBVbNS4rUAH7bOo2o9wptnOA5x/z+O+AARRZ6tEXQOd1oSy4gByLf2Wkh2QTi
c36ff1
+vP6Hln1vlFgKEzcXg6G8fN3MYWxKRhWmZM3DLERMvorlqqSBLcs5VvfZfLKcsKWTExioAq
c36ff1
+KgwEjYm8T9+rcpsw1xBus3j9k7wCI1Sus6PCDjq0pcYKLMYM7p8ygnU2tRYrOztdIxgWRA
c36ff1
+w/1oenm1Mqq2tV5xJcBCwCLOGe6SFwkIRywOYc57j5McH98Xhhg9cViyyBdXy/baF0mro+
c36ff1
+qPhOsWDxqwD4VKZ9UmQ6O8kPNKcc7QcIpFJhcO0g9zbp/MT0KueaWYrTKs8y4lUkTT7Xz6
c36ff1
++MzlR122/JwlAbBo6Y2kWtB+y+XwBZ0BfyJsm2czDhKm7OI5KfuBNhq0tFfKwOlYBq4QAA
c36ff1
+AMAyvUof1R8LLISkdO3EFTKn5RGNkPPoBJmGs6LwvU7NSjjLj/wPQe4jsIBc585tvbrddp
c36ff1
+60h72HgkZ5tqOfdeBYOKqX0qQQBHUEvI6M+NeQTQRev8bCHMLXQ21vzpClnrwNzlja359E
c36ff1
+uTRfiPRwIlyPLhOUiClBDSAnBI9h82Hkk3zzsQ/xGfsPB7iOjRbW69bMRSVCRpeweCVmWC
c36ff1
+77DTsEOq69V2TdljhQNIXE5OcOWonIlfgPiI74cdd+dLhzc/AAAADBAO1/JXd2kYiRyNkZ
c36ff1
+aXTLcwiSgBQIYbobqVP3OEtTclr0P1JAvby3Y4cCaEhkenx+fBqgXAku5lKM+U1Q9AEsMk
c36ff1
+cjIhaDpb43rU7GPjMn4zHwgGsEKd5pC1yIQ2PlK+cHanAdsDjIg+6RR+fuvid/mBeBOYXb
c36ff1
+Py0sa3HyekLJmCdx4UEyNASoiNaGFLQVAqo+RACsXy6VMxFH5dqDYlvwrfUQLwxJmse9Vb
c36ff1
+GEuuPAsklNugZqssC2XOIujFVUpslduQAAAMEAwzVHQVtsc3icCSzEAARpDTUdTbI29OhB
c36ff1
+/FMBnjzS9/3SWfLuBOSm9heNCHs2jdGNb8cPdKZuY7S9Fx6KuVUPyTbSSYkjj0F4fTeC9g
c36ff1
+0ym4p4UWYdF67WSWwLORkaG8K0d+G/CXkz8hvKUg6gcZWKBHAE1ROrHu1nsc8v7mkiKq4I
c36ff1
+bnTw5Q9TgjbWcQWtgPq0wXyyl/K8S1SFdkMCTOHDD0RQ+jTV2WNGVwFTodIRHenX+Rw2g4
c36ff1
+CHbTWbsFrHR1qFAAAACmphbWVzQG5ld3Q=
c36ff1
+-----END OPENSSH PRIVATE KEY-----
c36ff1
diff --git a/tests/integration_tests/assets/keys/id_rsa.test1.pub b/tests/integration_tests/assets/keys/id_rsa.test1.pub
c36ff1
new file mode 100644
c36ff1
index 00000000..3d2e26e1
c36ff1
--- /dev/null
c36ff1
+++ b/tests/integration_tests/assets/keys/id_rsa.test1.pub
c36ff1
@@ -0,0 +1 @@
c36ff1
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC1GUb3ponbdRG8CA79sGy4uX6Wq5zfnRpLBX3z+Ly+W86fkZVwfJ63+tdCDiVnizSjAbeRbJAVjye9OT4b5Q1Tx83RqcDMEOL7gzOx6OQX2xyNMwWZdWsikgKpXdthA2M+/qv65tB2QPTV27d4H23n/OXziC2yGnSEF2h/LPy7X8Dbmzt9VTrsbIjs90q9pL7K8lIidmdvZ71VDN8kL6oCc3uBRlq0EqN2BS0l6JRR4NpWdpLXtvRqhP2IE7SwhxC05MGViUKMW3VMAwtnrgePdJ8ZkHtiPRn4vMUHYhZfXPOyMjrxiRg8o5Iv81iFBT17nmQlDtaeeA0dj/af8Dt00pIUOaAzlz0haB5nt+Omx8hgCgn8IDEUg0P48Qd3WAJLrGgSFpRXKHlYf9eTAq/WfHgCJnYEUIT7vmTJJEtlQn7nLSvcNTmS0xd5RHNUg/um7RHwMK6X4ZT4z/5MfJa0AQ6B5o0np9imjaKkOFuZNi3eSTaUwhqlHEYEWz7fux0= test1@host
c36ff1
diff --git a/tests/integration_tests/assets/keys/id_rsa.test2 b/tests/integration_tests/assets/keys/id_rsa.test2
c36ff1
new file mode 100644
c36ff1
index 00000000..5854d901
c36ff1
--- /dev/null
c36ff1
+++ b/tests/integration_tests/assets/keys/id_rsa.test2
c36ff1
@@ -0,0 +1,38 @@
c36ff1
+-----BEGIN OPENSSH PRIVATE KEY-----
c36ff1
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
c36ff1
+NhAAAAAwEAAQAAAYEAvK50D2PWOc4ikyHVRJS6tDhqzjL5cKiivID4p1X8BYCVw83XAEGO
c36ff1
+LnItUyVXHNADlh6fpVq1NY6A2JVtygoPF6ZFx8ph7IWMmnhDdnxLLyGsbhd1M1tiXJD/R+
c36ff1
+3WnGHRJ4PKrQavMLgqHRrieV3QVVfjFSeo6jX/4TruP6ZmvITMZWJrXaGphxJ/pPykEdkO
c36ff1
+i8AmKU9FNviojyPS2nNtj9B/635IdgWvrd7Vf5Ycsw9MR55LWSidwa856RH62Yl6LpEGTH
c36ff1
+m1lJiMk1u88JPSqvohhaUkLKkFpcQwcB0m76W1KOyllJsmX8bNXrlZsI+WiiYI7Xl5vQm2
c36ff1
+17DEuNeavtPAtDMxu8HmTg2UJ55Naxehbfe2lx2k5kYGGw3i1O1OVN2pZ2/OB71LucYd/5
c36ff1
+qxPaz03wswcGOJYGPkNc40vdES/Scc7Yt8HsnZuzqkyOgzn0HiUCzoYUYLYTpLf+yGmwxS
c36ff1
+yAEY056aOfkCsboKHOKiOmlJxNaZZFQkX1evep4DAAAFgC7HMbUuxzG1AAAAB3NzaC1yc2
c36ff1
+EAAAGBALyudA9j1jnOIpMh1USUurQ4as4y+XCooryA+KdV/AWAlcPN1wBBji5yLVMlVxzQ
c36ff1
+A5Yen6VatTWOgNiVbcoKDxemRcfKYeyFjJp4Q3Z8Sy8hrG4XdTNbYlyQ/0ft1pxh0SeDyq
c36ff1
+0GrzC4Kh0a4nld0FVX4xUnqOo1/+E67j+mZryEzGVia12hqYcSf6T8pBHZDovAJilPRTb4
c36ff1
+qI8j0tpzbY/Qf+t+SHYFr63e1X+WHLMPTEeeS1koncGvOekR+tmJei6RBkx5tZSYjJNbvP
c36ff1
+CT0qr6IYWlJCypBaXEMHAdJu+ltSjspZSbJl/GzV65WbCPloomCO15eb0JttewxLjXmr7T
c36ff1
+wLQzMbvB5k4NlCeeTWsXoW33tpcdpOZGBhsN4tTtTlTdqWdvzge9S7nGHf+asT2s9N8LMH
c36ff1
+BjiWBj5DXONL3REv0nHO2LfB7J2bs6pMjoM59B4lAs6GFGC2E6S3/shpsMUsgBGNOemjn5
c36ff1
+ArG6ChziojppScTWmWRUJF9Xr3qeAwAAAAMBAAEAAAGASj/kkEHbhbfmxzujL2/P4Sfqb+
c36ff1
+aDXqAeGkwujbs6h/fH99vC5ejmSMTJrVSeaUo6fxLiBDIj6UWA0rpLEBzRP59BCpRL4MXV
c36ff1
+RNxav/+9nniD4Hb+ug0WMhMlQmsH71ZW9lPYqCpfOq7ec8GmqdgPKeaCCEspH7HMVhfYtd
c36ff1
+eHylwAC02lrpz1l5/h900sS5G9NaWR3uPA+xbzThDs4uZVkSidjlCNt1QZhDSSk7jA5n34
c36ff1
+qJ5UTGu9WQDZqyxWKND+RIyQuFAPGQyoyCC1FayHO2sEhT5qHuumL14Mn81XpzoXFoKyql
c36ff1
+rhBDe+pHhKArBYt92Evch0k1ABKblFxtxLXcvk4Fs7pHi+8k4+Cnazej2kcsu1kURlMZJB
c36ff1
+w2QT/8BV4uImbH05LtyscQuwGzpIoxqrnHrvg5VbohStmhoOjYybzqqW3/M0qhkn5JgTiy
c36ff1
+dJcHRJisRnAcmbmEchYtLDi6RW1e022H4I9AFXQqyr5HylBq6ugtWcFCsrcX8ibZ8xAAAA
c36ff1
+wQCAOPgwae6yZLkrYzRfbxZtGKNmhpI0EtNSDCHYuQQapFZJe7EFENs/VAaIiiut0yajGj
c36ff1
+c3aoKcwGIoT8TUM8E3GSNW6+WidUOC7H6W+/6N2OYZHRBACGz820xO+UBCl2oSk+dLBlfr
c36ff1
+IQzBGUWn5uVYCs0/2nxfCdFyHtMK8dMF/ypbdG+o1rXz5y9b7PVG6Mn+o1Rjsdkq7VERmy
c36ff1
+Pukd8hwATOIJqoKl3TuFyBeYFLqe+0e7uTeswQFw17PF31VjAAAADBAOpJRQb8c6qWqsvv
c36ff1
+vkve0uMuL0DfWW0G6+SxjPLcV6aTWL5xu0Grd8uBxDkkHU/CDrAwpchXyuLsvbw21Eje/u
c36ff1
+U5k9nLEscWZwcX7odxlK+EfAY2Bf5+Hd9bH5HMzTRJH8KkWK1EppOLPyiDxz4LZGzPLVyv
c36ff1
+/1PgSuvXkSWk1KIE4SvSemyxGX2tPVI6uO+URqevfnPOS1tMB7BMQlgkR6eh4bugx9UYx9
c36ff1
+mwlXonNa4dN0iQxZ7N4rKFBbT/uyB2bQAAAMEAzisnkD8k9Tn8uyhxpWLHwb03X4ZUUHDV
c36ff1
+zu15e4a8dZ+mM8nHO986913Xz5JujlJKkGwFTvgWkIiR2zqTEauZHARH7gANpaweTm6lPd
c36ff1
+E4p2S0M3ulY7xtp9lCFIrDhMPPkGq8SFZB6qhgucHcZSRLq6ZDou3S2IdNOzDTpBtkhRCS
c36ff1
+0zFcdTLh3zZweoy8HGbW36bwB6s1CIL76Pd4F64i0Ms9CCCU6b+E5ArFhYQIsXiDbgHWbD
c36ff1
+tZRSm2GEgnDGAvAAAACmphbWVzQG5ld3Q=
c36ff1
+-----END OPENSSH PRIVATE KEY-----
c36ff1
diff --git a/tests/integration_tests/assets/keys/id_rsa.test2.pub b/tests/integration_tests/assets/keys/id_rsa.test2.pub
c36ff1
new file mode 100644
c36ff1
index 00000000..f3831a57
c36ff1
--- /dev/null
c36ff1
+++ b/tests/integration_tests/assets/keys/id_rsa.test2.pub
c36ff1
@@ -0,0 +1 @@
c36ff1
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC8rnQPY9Y5ziKTIdVElLq0OGrOMvlwqKK8gPinVfwFgJXDzdcAQY4uci1TJVcc0AOWHp+lWrU1joDYlW3KCg8XpkXHymHshYyaeEN2fEsvIaxuF3UzW2JckP9H7dacYdEng8qtBq8wuCodGuJ5XdBVV+MVJ6jqNf/hOu4/pma8hMxlYmtdoamHEn+k/KQR2Q6LwCYpT0U2+KiPI9Lac22P0H/rfkh2Ba+t3tV/lhyzD0xHnktZKJ3BrznpEfrZiXoukQZMebWUmIyTW7zwk9Kq+iGFpSQsqQWlxDBwHSbvpbUo7KWUmyZfxs1euVmwj5aKJgjteXm9CbbXsMS415q+08C0MzG7weZODZQnnk1rF6Ft97aXHaTmRgYbDeLU7U5U3alnb84HvUu5xh3/mrE9rPTfCzBwY4lgY+Q1zjS90RL9Jxzti3weydm7OqTI6DOfQeJQLOhhRgthOkt/7IabDFLIARjTnpo5+QKxugoc4qI6aUnE1plkVCRfV696ngM= test2@host
c36ff1
diff --git a/tests/integration_tests/assets/keys/id_rsa.test3 b/tests/integration_tests/assets/keys/id_rsa.test3
c36ff1
new file mode 100644
c36ff1
index 00000000..2596c762
c36ff1
--- /dev/null
c36ff1
+++ b/tests/integration_tests/assets/keys/id_rsa.test3
c36ff1
@@ -0,0 +1,38 @@
c36ff1
+-----BEGIN OPENSSH PRIVATE KEY-----
c36ff1
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
c36ff1
+NhAAAAAwEAAQAAAYEApPG4MdkYQKD57/qreFrh9GRC22y66qZOWZWRjC887rrbvBzO69hV
c36ff1
+yJpTIXleJEvpWiHYcjMR5G6NNFsnNtZ4fxDqmSc4vcFj53JsE/XNqLKq6psXadCb5vkNpG
c36ff1
+bxA+Z5bJlzJ969PgJIIEbgc86sei4kgR2MuPWqtZbY5GkpNCTqWuLYeFK+14oFruA2nyWH
c36ff1
+9MOIRDHK/d597psHy+LTMtymO7ZPhO571abKw6jvvwiSeDxVE9kV7KAQIuM9/S3gftvgQQ
c36ff1
+ron3GL34pgmIabdSGdbfHqGDooryJhlbquJZELBN236KgRNTCAjVvUzjjQr1eRP3xssGwV
c36ff1
+O6ECBGCQLl/aYogAgtwnwj9iXqtfiLK3EwlgjquU4+JQ0CVtLhG3gIZB+qoMThco0pmHTr
c36ff1
+jtfQCwrztsBBFunSa2/CstuV1mQ5O5ZrZ6ACo9yPRBNkns6+CiKdtMtCtzi3k2RDz9jpYm
c36ff1
+Pcak03Lr7IkdC1Tp6+jA+//yPHSO1o4CqW89IQzNAAAFgEUd7lZFHe5WAAAAB3NzaC1yc2
c36ff1
+EAAAGBAKTxuDHZGECg+e/6q3ha4fRkQttsuuqmTlmVkYwvPO6627wczuvYVciaUyF5XiRL
c36ff1
+6Voh2HIzEeRujTRbJzbWeH8Q6pknOL3BY+dybBP1zaiyquqbF2nQm+b5DaRm8QPmeWyZcy
c36ff1
+fevT4CSCBG4HPOrHouJIEdjLj1qrWW2ORpKTQk6lri2HhSvteKBa7gNp8lh/TDiEQxyv3e
c36ff1
+fe6bB8vi0zLcpju2T4Tue9WmysOo778Ikng8VRPZFeygECLjPf0t4H7b4EEK6J9xi9+KYJ
c36ff1
+iGm3UhnW3x6hg6KK8iYZW6riWRCwTdt+ioETUwgI1b1M440K9XkT98bLBsFTuhAgRgkC5f
c36ff1
+2mKIAILcJ8I/Yl6rX4iytxMJYI6rlOPiUNAlbS4Rt4CGQfqqDE4XKNKZh0647X0AsK87bA
c36ff1
+QRbp0mtvwrLbldZkOTuWa2egAqPcj0QTZJ7OvgoinbTLQrc4t5NkQ8/Y6WJj3GpNNy6+yJ
c36ff1
+HQtU6evowPv/8jx0jtaOAqlvPSEMzQAAAAMBAAEAAAGAGaqbdPZJNdVWzyb8g6/wtSzc0n
c36ff1
+Qq6dSTIJGLonq/So69HpqFAGIbhymsger24UMGvsXBfpO/1wH06w68HWZmPa+OMeLOi4iK
c36ff1
+WTuO4dQ/+l5DBlq32/lgKSLcIpb6LhcxEdsW9j9Mx1dnjc45owun/yMq/wRwH1/q/nLIsV
c36ff1
+JD3R9ZcGcYNDD8DWIm3D17gmw+qbG7hJES+0oh4n0xS2KyZpm7LFOEMDVEA8z+hE/HbryQ
c36ff1
+vjD1NC91n+qQWD1wKfN3WZDRwip3z1I5VHMpvXrA/spHpa9gzHK5qXNmZSz3/dfA1zHjCR
c36ff1
+2dHjJnrIUH8nyPfw8t+COC+sQBL3Nr0KUWEFPRM08cOcQm4ctzg17aDIZBONjlZGKlReR8
c36ff1
+1zfAw84Q70q2spLWLBLXSFblHkaOfijEbejIbaz2UUEQT27WD7RHAORdQlkx7eitk66T9d
c36ff1
+DzIq/cpYhm5Fs8KZsh3PLldp9nsHbD2Oa9J9LJyI4ryuIW0mVwRdvPSiiYi3K+mDCpAAAA
c36ff1
+wBe+ugEEJ+V7orb1f4Zez0Bd4FNkEc52WZL4CWbaCtM+ZBg5KnQ6xW14JdC8IS9cNi/I5P
c36ff1
+yLsBvG4bWPLGgQruuKY6oLueD6BFnKjqF6ACUCiSQldh4BAW1nYc2U48+FFvo3ZQyudFSy
c36ff1
+QEFlhHmcaNMDo0AIJY5Xnq2BG3nEX7AqdtZ8hhenHwLCRQJatDwSYBHDpSDdh9vpTnGp/2
c36ff1
+0jBz25Ko4UANzvSAc3sA4yN3jfpoM366TgdNf8x3g1v7yljQAAAMEA0HSQjzH5nhEwB58k
c36ff1
+mYYxnBYp1wb86zIuVhAyjZaeinvBQSTmLow8sXIHcCVuD3CgBezlU2SX5d9YuvRU9rcthi
c36ff1
+uzn4wWnbnzYy4SwzkMJXchUAkumFVD8Hq5TNPh2Z+033rLLE08EhYypSeVpuzdpFoStaS9
c36ff1
+3DUZA2bR/zLZI9MOVZRUcYImNegqIjOYHY8Sbj3/0QPV6+WpUJFMPvvedWhfaOsRMTA6nr
c36ff1
+VLG4pxkrieVl0UtuRGbzD/exXhXVi7AAAAwQDKkJj4ez/+KZFYlZQKiV0BrfUFcgS6ElFM
c36ff1
+2CZIEagCtu8eedrwkNqx2FUX33uxdvUTr4c9I3NvWeEEGTB9pgD4lh1x/nxfuhyGXtimFM
c36ff1
+GnznGV9oyz0DmKlKiKSEGwWf5G+/NiiCwwVJ7wsQQm7TqNtkQ9b8MhWWXC7xlXKUs7dmTa
c36ff1
+e8AqAndCCMEnbS1UQFO/R5PNcZXkFWDggLQ/eWRYKlrXgdnUgH6h0saOcViKpNJBUXb3+x
c36ff1
+eauhOY52PS/BcAAAAKamFtZXNAbmV3dAE=
c36ff1
+-----END OPENSSH PRIVATE KEY-----
c36ff1
diff --git a/tests/integration_tests/assets/keys/id_rsa.test3.pub b/tests/integration_tests/assets/keys/id_rsa.test3.pub
c36ff1
new file mode 100644
c36ff1
index 00000000..057db632
c36ff1
--- /dev/null
c36ff1
+++ b/tests/integration_tests/assets/keys/id_rsa.test3.pub
c36ff1
@@ -0,0 +1 @@
c36ff1
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCk8bgx2RhAoPnv+qt4WuH0ZELbbLrqpk5ZlZGMLzzuutu8HM7r2FXImlMheV4kS+laIdhyMxHkbo00Wyc21nh/EOqZJzi9wWPncmwT9c2osqrqmxdp0Jvm+Q2kZvED5nlsmXMn3r0+AkggRuBzzqx6LiSBHYy49aq1ltjkaSk0JOpa4th4Ur7XigWu4DafJYf0w4hEMcr93n3umwfL4tMy3KY7tk+E7nvVpsrDqO+/CJJ4PFUT2RXsoBAi4z39LeB+2+BBCuifcYvfimCYhpt1IZ1t8eoYOiivImGVuq4lkQsE3bfoqBE1MICNW9TOONCvV5E/fGywbBU7oQIEYJAuX9piiACC3CfCP2Jeq1+IsrcTCWCOq5Tj4lDQJW0uEbeAhkH6qgxOFyjSmYdOuO19ALCvO2wEEW6dJrb8Ky25XWZDk7lmtnoAKj3I9EE2Sezr4KIp20y0K3OLeTZEPP2OliY9xqTTcuvsiR0LVOnr6MD7//I8dI7WjgKpbz0hDM0= test3@host
c36ff1
diff --git a/tests/integration_tests/modules/test_ssh_keysfile.py b/tests/integration_tests/modules/test_ssh_keysfile.py
c36ff1
new file mode 100644
c36ff1
index 00000000..f82d7649
c36ff1
--- /dev/null
c36ff1
+++ b/tests/integration_tests/modules/test_ssh_keysfile.py
c36ff1
@@ -0,0 +1,85 @@
c36ff1
+import paramiko
c36ff1
+import pytest
c36ff1
+from io import StringIO
c36ff1
+from paramiko.ssh_exception import SSHException
c36ff1
+
c36ff1
+from tests.integration_tests.instances import IntegrationInstance
c36ff1
+from tests.integration_tests.util import get_test_rsa_keypair
c36ff1
+
c36ff1
+TEST_USER1_KEYS = get_test_rsa_keypair('test1')
c36ff1
+TEST_USER2_KEYS = get_test_rsa_keypair('test2')
c36ff1
+TEST_DEFAULT_KEYS = get_test_rsa_keypair('test3')
c36ff1
+
c36ff1
+USERDATA = """\
c36ff1
+#cloud-config
c36ff1
+bootcmd:
c36ff1
+ - sed -i 's;#AuthorizedKeysFile.*;AuthorizedKeysFile /etc/ssh/authorized_keys %h/.ssh/authorized_keys2;' /etc/ssh/sshd_config
c36ff1
+ssh_authorized_keys:
c36ff1
+ - {default}
c36ff1
+users:
c36ff1
+- default
c36ff1
+- name: test_user1
c36ff1
+  ssh_authorized_keys:
c36ff1
+    - {user1}
c36ff1
+- name: test_user2
c36ff1
+  ssh_authorized_keys:
c36ff1
+    - {user2}
c36ff1
+""".format(  # noqa: E501
c36ff1
+    default=TEST_DEFAULT_KEYS.public_key,
c36ff1
+    user1=TEST_USER1_KEYS.public_key,
c36ff1
+    user2=TEST_USER2_KEYS.public_key,
c36ff1
+)
c36ff1
+
c36ff1
+
c36ff1
+@pytest.mark.ubuntu
c36ff1
+@pytest.mark.user_data(USERDATA)
c36ff1
+def test_authorized_keys(client: IntegrationInstance):
c36ff1
+    expected_keys = [
c36ff1
+        ('test_user1', '/home/test_user1/.ssh/authorized_keys2',
c36ff1
+         TEST_USER1_KEYS),
c36ff1
+        ('test_user2', '/home/test_user2/.ssh/authorized_keys2',
c36ff1
+         TEST_USER2_KEYS),
c36ff1
+        ('ubuntu', '/home/ubuntu/.ssh/authorized_keys2',
c36ff1
+         TEST_DEFAULT_KEYS),
c36ff1
+        ('root', '/root/.ssh/authorized_keys2', TEST_DEFAULT_KEYS),
c36ff1
+    ]
c36ff1
+
c36ff1
+    for user, filename, keys in expected_keys:
c36ff1
+        contents = client.read_from_file(filename)
c36ff1
+        if user in ['ubuntu', 'root']:
c36ff1
+            # Our personal public key gets added by pycloudlib
c36ff1
+            lines = contents.split('\n')
c36ff1
+            assert len(lines) == 2
c36ff1
+            assert keys.public_key.strip() in contents
c36ff1
+        else:
c36ff1
+            assert contents.strip() == keys.public_key.strip()
c36ff1
+
c36ff1
+        # Ensure we can actually connect
c36ff1
+        ssh = paramiko.SSHClient()
c36ff1
+        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
c36ff1
+        paramiko_key = paramiko.RSAKey.from_private_key(StringIO(
c36ff1
+            keys.private_key))
c36ff1
+
c36ff1
+        # Will fail with AuthenticationException if
c36ff1
+        # we cannot connect
c36ff1
+        ssh.connect(
c36ff1
+            client.instance.ip,
c36ff1
+            username=user,
c36ff1
+            pkey=paramiko_key,
c36ff1
+            look_for_keys=False,
c36ff1
+            allow_agent=False,
c36ff1
+        )
c36ff1
+
c36ff1
+        # Ensure other uses can't connect using our key
c36ff1
+        other_users = [u[0] for u in expected_keys if u[2] != keys]
c36ff1
+        for other_user in other_users:
c36ff1
+            with pytest.raises(SSHException):
c36ff1
+                print('trying to connect as {} with key from {}'.format(
c36ff1
+                    other_user, user))
c36ff1
+                ssh.connect(
c36ff1
+                    client.instance.ip,
c36ff1
+                    username=other_user,
c36ff1
+                    pkey=paramiko_key,
c36ff1
+                    look_for_keys=False,
c36ff1
+                    allow_agent=False,
c36ff1
+                )
c36ff1
diff --git a/tests/unittests/test_sshutil.py b/tests/unittests/test_sshutil.py
c36ff1
index fd1d1bac..bcb8044f 100644
c36ff1
--- a/tests/unittests/test_sshutil.py
c36ff1
+++ b/tests/unittests/test_sshutil.py
c36ff1
@@ -570,20 +570,33 @@ class TestBasicAuthorizedKeyParse(test_helpers.CiTestCase):
c36ff1
             ssh_util.render_authorizedkeysfile_paths(
c36ff1
                 "%h/.keys", "/homedirs/bobby", "bobby"))
c36ff1
 
c36ff1
+    def test_all(self):
c36ff1
+        self.assertEqual(
c36ff1
+            ["/homedirs/bobby/.keys", "/homedirs/bobby/.secret/keys",
c36ff1
+             "/keys/path1", "/opt/bobby/keys"],
c36ff1
+            ssh_util.render_authorizedkeysfile_paths(
c36ff1
+                "%h/.keys .secret/keys /keys/path1 /opt/%u/keys",
c36ff1
+                "/homedirs/bobby", "bobby"))
c36ff1
+
c36ff1
 
c36ff1
 class TestMultipleSshAuthorizedKeysFile(test_helpers.CiTestCase):
c36ff1
 
c36ff1
     @patch("cloudinit.ssh_util.pwd.getpwnam")
c36ff1
     def test_multiple_authorizedkeys_file_order1(self, m_getpwnam):
c36ff1
-        fpw = FakePwEnt(pw_name='bobby', pw_dir='/home2/bobby')
c36ff1
+        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby')
c36ff1
         m_getpwnam.return_value = fpw
c36ff1
-        authorized_keys = self.tmp_path('authorized_keys')
c36ff1
+        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
c36ff1
+
c36ff1
+        # /tmp/home2/bobby/.ssh/authorized_keys = rsa
c36ff1
+        authorized_keys = self.tmp_path('authorized_keys', dir=user_ssh_folder)
c36ff1
         util.write_file(authorized_keys, VALID_CONTENT['rsa'])
c36ff1
 
c36ff1
-        user_keys = self.tmp_path('user_keys')
c36ff1
+        # /tmp/home2/bobby/.ssh/user_keys = dsa
c36ff1
+        user_keys = self.tmp_path('user_keys', dir=user_ssh_folder)
c36ff1
         util.write_file(user_keys, VALID_CONTENT['dsa'])
c36ff1
 
c36ff1
-        sshd_config = self.tmp_path('sshd_config')
c36ff1
+        # /tmp/sshd_config
c36ff1
+        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
c36ff1
         util.write_file(
c36ff1
             sshd_config,
c36ff1
             "AuthorizedKeysFile %s %s" % (authorized_keys, user_keys)
c36ff1
@@ -593,33 +606,244 @@ class TestMultipleSshAuthorizedKeysFile(test_helpers.CiTestCase):
c36ff1
             fpw.pw_name, sshd_config)
c36ff1
         content = ssh_util.update_authorized_keys(auth_key_entries, [])
c36ff1
 
c36ff1
-        self.assertEqual("%s/.ssh/authorized_keys" % fpw.pw_dir, auth_key_fn)
c36ff1
+        self.assertEqual(user_keys, auth_key_fn)
c36ff1
         self.assertTrue(VALID_CONTENT['rsa'] in content)
c36ff1
         self.assertTrue(VALID_CONTENT['dsa'] in content)
c36ff1
 
c36ff1
     @patch("cloudinit.ssh_util.pwd.getpwnam")
c36ff1
     def test_multiple_authorizedkeys_file_order2(self, m_getpwnam):
c36ff1
-        fpw = FakePwEnt(pw_name='suzie', pw_dir='/home/suzie')
c36ff1
+        fpw = FakePwEnt(pw_name='suzie', pw_dir='/tmp/home/suzie')
c36ff1
         m_getpwnam.return_value = fpw
c36ff1
-        authorized_keys = self.tmp_path('authorized_keys')
c36ff1
+        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
c36ff1
+
c36ff1
+        # /tmp/home/suzie/.ssh/authorized_keys = rsa
c36ff1
+        authorized_keys = self.tmp_path('authorized_keys', dir=user_ssh_folder)
c36ff1
         util.write_file(authorized_keys, VALID_CONTENT['rsa'])
c36ff1
 
c36ff1
-        user_keys = self.tmp_path('user_keys')
c36ff1
+        # /tmp/home/suzie/.ssh/user_keys = dsa
c36ff1
+        user_keys = self.tmp_path('user_keys', dir=user_ssh_folder)
c36ff1
         util.write_file(user_keys, VALID_CONTENT['dsa'])
c36ff1
 
c36ff1
-        sshd_config = self.tmp_path('sshd_config')
c36ff1
+        # /tmp/sshd_config
c36ff1
+        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
c36ff1
         util.write_file(
c36ff1
             sshd_config,
c36ff1
-            "AuthorizedKeysFile %s %s" % (authorized_keys, user_keys)
c36ff1
+            "AuthorizedKeysFile %s %s" % (user_keys, authorized_keys)
c36ff1
         )
c36ff1
 
c36ff1
         (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
c36ff1
-            fpw.pw_name, sshd_config
c36ff1
+            fpw.pw_name, sshd_config)
c36ff1
+        content = ssh_util.update_authorized_keys(auth_key_entries, [])
c36ff1
+
c36ff1
+        self.assertEqual(authorized_keys, auth_key_fn)
c36ff1
+        self.assertTrue(VALID_CONTENT['rsa'] in content)
c36ff1
+        self.assertTrue(VALID_CONTENT['dsa'] in content)
c36ff1
+
c36ff1
+    @patch("cloudinit.ssh_util.pwd.getpwnam")
c36ff1
+    def test_multiple_authorizedkeys_file_local_global(self, m_getpwnam):
c36ff1
+        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby')
c36ff1
+        m_getpwnam.return_value = fpw
c36ff1
+        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
c36ff1
+
c36ff1
+        # /tmp/home2/bobby/.ssh/authorized_keys = rsa
c36ff1
+        authorized_keys = self.tmp_path('authorized_keys', dir=user_ssh_folder)
c36ff1
+        util.write_file(authorized_keys, VALID_CONTENT['rsa'])
c36ff1
+
c36ff1
+        # /tmp/home2/bobby/.ssh/user_keys = dsa
c36ff1
+        user_keys = self.tmp_path('user_keys', dir=user_ssh_folder)
c36ff1
+        util.write_file(user_keys, VALID_CONTENT['dsa'])
c36ff1
+
c36ff1
+        # /tmp/etc/ssh/authorized_keys = ecdsa
c36ff1
+        authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys',
c36ff1
+                                               dir="/tmp")
c36ff1
+        util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa'])
c36ff1
+
c36ff1
+        # /tmp/sshd_config
c36ff1
+        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
c36ff1
+        util.write_file(
c36ff1
+            sshd_config,
c36ff1
+            "AuthorizedKeysFile %s %s %s" % (authorized_keys_global,
c36ff1
+                                             user_keys, authorized_keys)
c36ff1
+        )
c36ff1
+
c36ff1
+        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
c36ff1
+            fpw.pw_name, sshd_config)
c36ff1
+        content = ssh_util.update_authorized_keys(auth_key_entries, [])
c36ff1
+
c36ff1
+        self.assertEqual(authorized_keys, auth_key_fn)
c36ff1
+        self.assertTrue(VALID_CONTENT['rsa'] in content)
c36ff1
+        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
c36ff1
+        self.assertTrue(VALID_CONTENT['dsa'] in content)
c36ff1
+
c36ff1
+    @patch("cloudinit.ssh_util.pwd.getpwnam")
c36ff1
+    def test_multiple_authorizedkeys_file_local_global2(self, m_getpwnam):
c36ff1
+        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby')
c36ff1
+        m_getpwnam.return_value = fpw
c36ff1
+        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
c36ff1
+
c36ff1
+        # /tmp/home2/bobby/.ssh/authorized_keys2 = rsa
c36ff1
+        authorized_keys = self.tmp_path('authorized_keys2',
c36ff1
+                                        dir=user_ssh_folder)
c36ff1
+        util.write_file(authorized_keys, VALID_CONTENT['rsa'])
c36ff1
+
c36ff1
+        # /tmp/home2/bobby/.ssh/user_keys3 = dsa
c36ff1
+        user_keys = self.tmp_path('user_keys3', dir=user_ssh_folder)
c36ff1
+        util.write_file(user_keys, VALID_CONTENT['dsa'])
c36ff1
+
c36ff1
+        # /tmp/etc/ssh/authorized_keys = ecdsa
c36ff1
+        authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys',
c36ff1
+                                               dir="/tmp")
c36ff1
+        util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa'])
c36ff1
+
c36ff1
+        # /tmp/sshd_config
c36ff1
+        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
c36ff1
+        util.write_file(
c36ff1
+            sshd_config,
c36ff1
+            "AuthorizedKeysFile %s %s %s" % (authorized_keys_global,
c36ff1
+                                             authorized_keys, user_keys)
c36ff1
+        )
c36ff1
+
c36ff1
+        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
c36ff1
+            fpw.pw_name, sshd_config)
c36ff1
+        content = ssh_util.update_authorized_keys(auth_key_entries, [])
c36ff1
+
c36ff1
+        self.assertEqual(user_keys, auth_key_fn)
c36ff1
+        self.assertTrue(VALID_CONTENT['rsa'] in content)
c36ff1
+        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
c36ff1
+        self.assertTrue(VALID_CONTENT['dsa'] in content)
c36ff1
+
c36ff1
+    @patch("cloudinit.ssh_util.pwd.getpwnam")
c36ff1
+    def test_multiple_authorizedkeys_file_global(self, m_getpwnam):
c36ff1
+        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby')
c36ff1
+        m_getpwnam.return_value = fpw
c36ff1
+
c36ff1
+        # /tmp/etc/ssh/authorized_keys = rsa
c36ff1
+        authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys',
c36ff1
+                                               dir="/tmp")
c36ff1
+        util.write_file(authorized_keys_global, VALID_CONTENT['rsa'])
c36ff1
+
c36ff1
+        # /tmp/sshd_config
c36ff1
+        sshd_config = self.tmp_path('sshd_config')
c36ff1
+        util.write_file(
c36ff1
+            sshd_config,
c36ff1
+            "AuthorizedKeysFile %s" % (authorized_keys_global)
c36ff1
         )
c36ff1
+
c36ff1
+        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
c36ff1
+            fpw.pw_name, sshd_config)
c36ff1
         content = ssh_util.update_authorized_keys(auth_key_entries, [])
c36ff1
 
c36ff1
         self.assertEqual("%s/.ssh/authorized_keys" % fpw.pw_dir, auth_key_fn)
c36ff1
         self.assertTrue(VALID_CONTENT['rsa'] in content)
c36ff1
+
c36ff1
+    @patch("cloudinit.ssh_util.pwd.getpwnam")
c36ff1
+    def test_multiple_authorizedkeys_file_multiuser(self, m_getpwnam):
c36ff1
+        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby')
c36ff1
+        m_getpwnam.return_value = fpw
c36ff1
+        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
c36ff1
+        # /tmp/home2/bobby/.ssh/authorized_keys2 = rsa
c36ff1
+        authorized_keys = self.tmp_path('authorized_keys2',
c36ff1
+                                        dir=user_ssh_folder)
c36ff1
+        util.write_file(authorized_keys, VALID_CONTENT['rsa'])
c36ff1
+        # /tmp/home2/bobby/.ssh/user_keys3 = dsa
c36ff1
+        user_keys = self.tmp_path('user_keys3', dir=user_ssh_folder)
c36ff1
+        util.write_file(user_keys, VALID_CONTENT['dsa'])
c36ff1
+
c36ff1
+        fpw2 = FakePwEnt(pw_name='suzie', pw_dir='/tmp/home/suzie')
c36ff1
+        user_ssh_folder = "%s/.ssh" % fpw2.pw_dir
c36ff1
+        # /tmp/home/suzie/.ssh/authorized_keys2 = ssh-xmss@openssh.com
c36ff1
+        authorized_keys2 = self.tmp_path('authorized_keys2',
c36ff1
+                                         dir=user_ssh_folder)
c36ff1
+        util.write_file(authorized_keys2,
c36ff1
+                        VALID_CONTENT['ssh-xmss@openssh.com'])
c36ff1
+
c36ff1
+        # /tmp/etc/ssh/authorized_keys = ecdsa
c36ff1
+        authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys2',
c36ff1
+                                               dir="/tmp")
c36ff1
+        util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa'])
c36ff1
+
c36ff1
+        # /tmp/sshd_config
c36ff1
+        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
c36ff1
+        util.write_file(
c36ff1
+            sshd_config,
c36ff1
+            "AuthorizedKeysFile %s %%h/.ssh/authorized_keys2 %s" %
c36ff1
+            (authorized_keys_global, user_keys)
c36ff1
+        )
c36ff1
+
c36ff1
+        # process first user
c36ff1
+        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
c36ff1
+            fpw.pw_name, sshd_config)
c36ff1
+        content = ssh_util.update_authorized_keys(auth_key_entries, [])
c36ff1
+
c36ff1
+        self.assertEqual(user_keys, auth_key_fn)
c36ff1
+        self.assertTrue(VALID_CONTENT['rsa'] in content)
c36ff1
+        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
c36ff1
+        self.assertTrue(VALID_CONTENT['dsa'] in content)
c36ff1
+        self.assertFalse(VALID_CONTENT['ssh-xmss@openssh.com'] in content)
c36ff1
+
c36ff1
+        m_getpwnam.return_value = fpw2
c36ff1
+        # process second user
c36ff1
+        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
c36ff1
+            fpw2.pw_name, sshd_config)
c36ff1
+        content = ssh_util.update_authorized_keys(auth_key_entries, [])
c36ff1
+
c36ff1
+        self.assertEqual(authorized_keys2, auth_key_fn)
c36ff1
+        self.assertTrue(VALID_CONTENT['ssh-xmss@openssh.com'] in content)
c36ff1
+        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
c36ff1
+        self.assertTrue(VALID_CONTENT['dsa'] in content)
c36ff1
+        self.assertFalse(VALID_CONTENT['rsa'] in content)
c36ff1
+
c36ff1
+    @patch("cloudinit.ssh_util.pwd.getpwnam")
c36ff1
+    def test_multiple_authorizedkeys_file_multiuser2(self, m_getpwnam):
c36ff1
+        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home/bobby')
c36ff1
+        m_getpwnam.return_value = fpw
c36ff1
+        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
c36ff1
+        # /tmp/home/bobby/.ssh/authorized_keys2 = rsa
c36ff1
+        authorized_keys = self.tmp_path('authorized_keys2',
c36ff1
+                                        dir=user_ssh_folder)
c36ff1
+        util.write_file(authorized_keys, VALID_CONTENT['rsa'])
c36ff1
+        # /tmp/home/bobby/.ssh/user_keys3 = dsa
c36ff1
+        user_keys = self.tmp_path('user_keys3', dir=user_ssh_folder)
c36ff1
+        util.write_file(user_keys, VALID_CONTENT['dsa'])
c36ff1
+
c36ff1
+        fpw2 = FakePwEnt(pw_name='badguy', pw_dir='/tmp/home/badguy')
c36ff1
+        user_ssh_folder = "%s/.ssh" % fpw2.pw_dir
c36ff1
+        # /tmp/home/badguy/home/bobby = ""
c36ff1
+        authorized_keys2 = self.tmp_path('home/bobby', dir="/tmp/home/badguy")
c36ff1
+
c36ff1
+        # /tmp/etc/ssh/authorized_keys = ecdsa
c36ff1
+        authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys2',
c36ff1
+                                               dir="/tmp")
c36ff1
+        util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa'])
c36ff1
+
c36ff1
+        # /tmp/sshd_config
c36ff1
+        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
c36ff1
+        util.write_file(
c36ff1
+            sshd_config,
c36ff1
+            "AuthorizedKeysFile %s %%h/.ssh/authorized_keys2 %s %s" %
c36ff1
+            (authorized_keys_global, user_keys, authorized_keys2)
c36ff1
+        )
c36ff1
+
c36ff1
+        # process first user
c36ff1
+        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
c36ff1
+            fpw.pw_name, sshd_config)
c36ff1
+        content = ssh_util.update_authorized_keys(auth_key_entries, [])
c36ff1
+
c36ff1
+        self.assertEqual(user_keys, auth_key_fn)
c36ff1
+        self.assertTrue(VALID_CONTENT['rsa'] in content)
c36ff1
+        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
c36ff1
+        self.assertTrue(VALID_CONTENT['dsa'] in content)
c36ff1
+
c36ff1
+        m_getpwnam.return_value = fpw2
c36ff1
+        # process second user
c36ff1
+        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
c36ff1
+            fpw2.pw_name, sshd_config)
c36ff1
+        content = ssh_util.update_authorized_keys(auth_key_entries, [])
c36ff1
+
c36ff1
+        # badguy should not take the key from the other user!
c36ff1
+        self.assertEqual(authorized_keys2, auth_key_fn)
c36ff1
+        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
c36ff1
         self.assertTrue(VALID_CONTENT['dsa'] in content)
c36ff1
+        self.assertFalse(VALID_CONTENT['rsa'] in content)
c36ff1
 
c36ff1
 # vi: ts=4 expandtab
c36ff1
-- 
c36ff1
2.27.0
c36ff1