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