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