diff --git a/SOURCES/ci-Stop-copying-ssh-system-keys-and-check-folder-permis.patch b/SOURCES/ci-Stop-copying-ssh-system-keys-and-check-folder-permis.patch new file mode 100644 index 0000000..e46b52b --- /dev/null +++ b/SOURCES/ci-Stop-copying-ssh-system-keys-and-check-folder-permis.patch @@ -0,0 +1,1385 @@ +From 3b68aff3b7b1dc567ef6721a269c2d4e054b729f Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito +Date: Mon, 9 Aug 2021 23:41:44 +0200 +Subject: [PATCH] Stop copying ssh system keys and check folder permissions + (#956) + +RH-Author: Emanuele Giuseppe Esposito +RH-MergeRequest: 28: Stop copying ssh system keys and check folder permissions (#956) +RH-Commit: [1/1] 7cada613be82f2f525ee56b86ef9f71edf40d2ef (eesposit/cloud-init) +RH-Bugzilla: 1862967 +RH-Acked-by: Miroslav Rezanina +RH-Acked-by: Eduardo Otubo + +TESTED: By me and QA +BREW: 38818284 + +This is a continuation of previous MR 25 and upstream PR #937. +There were still issues when using non-standard file paths like +/etc/ssh/userkeys/%u or /etc/ssh/authorized_keys, and the choice +of storing the keys of all authorized_keys files into a single +one was not ideal. This fix modifies cloudinit to support +all different cases of authorized_keys file locations, and +picks a user-specific file where to copy the new keys that +complies with ssh permissions. + +commit 00dbaf1e9ab0e59d81662f0f3561897bef499a3f +Author: Emanuele Giuseppe Esposito +Date: Mon Aug 9 16:49:56 2021 +0200 + + Stop copying ssh system keys and check folder permissions (#956) + + In /etc/ssh/sshd_config, it is possible to define a custom + authorized_keys file that will contain the keys allowed to access the + machine via the AuthorizedKeysFile option. Cloudinit is able to add + user-specific keys to the existing ones, but we need to be careful on + which of the authorized_keys files listed to pick. + Chosing a file that is shared by all user will cause security + issues, because the owner of that key can then access also other users. + + We therefore pick an authorized_keys file only if it satisfies the + following conditions: + 1. it is not a "global" file, ie it must be defined in + AuthorizedKeysFile with %u, %h or be in /home/. This avoids + security issues. + 2. it must comply with ssh permission requirements, otherwise the ssh + agent won't use that file. + + If it doesn't meet either of those conditions, write to + ~/.ssh/authorized_keys + + We also need to consider the case when the chosen authorized_keys file + does not exist. In this case, the existing behavior of cloud-init is + to create the new file. We therefore need to be sure that the file + complies with ssh permissions too, by setting: + - the actual file to permission 600, and owned by the user + - the directories in the path that do not exist must be root owned and + with permission 755. + +Signed-off-by: Emanuele Giuseppe Esposito +--- + cloudinit/ssh_util.py | 133 ++++- + cloudinit/util.py | 51 +- + tests/unittests/test_sshutil.py | 952 +++++++++++++++++++++++++------- + 3 files changed, 920 insertions(+), 216 deletions(-) + +diff --git a/cloudinit/ssh_util.py b/cloudinit/ssh_util.py +index 89057262..b8a3c8f7 100644 +--- a/cloudinit/ssh_util.py ++++ b/cloudinit/ssh_util.py +@@ -249,6 +249,113 @@ def render_authorizedkeysfile_paths(value, homedir, username): + return rendered + + ++# Inspired from safe_path() in openssh source code (misc.c). ++def check_permissions(username, current_path, full_path, is_file, strictmodes): ++ """Check if the file/folder in @current_path has the right permissions. ++ ++ We need to check that: ++ 1. If StrictMode is enabled, the owner is either root or the user ++ 2. the user can access the file/folder, otherwise ssh won't use it ++ 3. If StrictMode is enabled, no write permission is given to group ++ and world users (022) ++ """ ++ ++ # group/world can only execute the folder (access) ++ minimal_permissions = 0o711 ++ if is_file: ++ # group/world can only read the file ++ minimal_permissions = 0o644 ++ ++ # 1. owner must be either root or the user itself ++ owner = util.get_owner(current_path) ++ if strictmodes and owner != username and owner != "root": ++ LOG.debug("Path %s in %s must be own by user %s or" ++ " by root, but instead is own by %s. Ignoring key.", ++ current_path, full_path, username, owner) ++ return False ++ ++ parent_permission = util.get_permissions(current_path) ++ # 2. the user can access the file/folder, otherwise ssh won't use it ++ if owner == username: ++ # need only the owner permissions ++ minimal_permissions &= 0o700 ++ else: ++ group_owner = util.get_group(current_path) ++ user_groups = util.get_user_groups(username) ++ ++ if group_owner in user_groups: ++ # need only the group permissions ++ minimal_permissions &= 0o070 ++ else: ++ # need only the world permissions ++ minimal_permissions &= 0o007 ++ ++ if parent_permission & minimal_permissions == 0: ++ LOG.debug("Path %s in %s must be accessible by user %s," ++ " check its permissions", ++ current_path, full_path, username) ++ return False ++ ++ # 3. no write permission (w) is given to group and world users (022) ++ # Group and world user can still have +rx. ++ if strictmodes and parent_permission & 0o022 != 0: ++ LOG.debug("Path %s in %s must not give write" ++ "permission to group or world users. Ignoring key.", ++ current_path, full_path) ++ return False ++ ++ return True ++ ++ ++def check_create_path(username, filename, strictmodes): ++ user_pwent = users_ssh_info(username)[1] ++ root_pwent = users_ssh_info("root")[1] ++ try: ++ # check the directories first ++ directories = filename.split("/")[1:-1] ++ ++ # scan in order, from root to file name ++ parent_folder = "" ++ # this is to comply also with unit tests, and ++ # strange home directories ++ home_folder = os.path.dirname(user_pwent.pw_dir) ++ for directory in directories: ++ parent_folder += "/" + directory ++ if home_folder.startswith(parent_folder): ++ continue ++ ++ if not os.path.isdir(parent_folder): ++ # directory does not exist, and permission so far are good: ++ # create the directory, and make it accessible by everyone ++ # but owned by root, as it might be used by many users. ++ with util.SeLinuxGuard(parent_folder): ++ os.makedirs(parent_folder, mode=0o755, exist_ok=True) ++ util.chownbyid(parent_folder, root_pwent.pw_uid, ++ root_pwent.pw_gid) ++ ++ permissions = check_permissions(username, parent_folder, ++ filename, False, strictmodes) ++ if not permissions: ++ return False ++ ++ # check the file ++ if not os.path.exists(filename): ++ # if file does not exist: we need to create it, since the ++ # folders at this point exist and have right permissions ++ util.write_file(filename, '', mode=0o600, ensure_dir_exists=True) ++ util.chownbyid(filename, user_pwent.pw_uid, user_pwent.pw_gid) ++ ++ permissions = check_permissions(username, filename, ++ filename, True, strictmodes) ++ if not permissions: ++ return False ++ except (IOError, OSError) as e: ++ util.logexc(LOG, str(e)) ++ return False ++ ++ return True ++ ++ + def extract_authorized_keys(username, sshd_cfg_file=DEF_SSHD_CFG): + (ssh_dir, pw_ent) = users_ssh_info(username) + default_authorizedkeys_file = os.path.join(ssh_dir, 'authorized_keys') +@@ -259,6 +366,7 @@ def extract_authorized_keys(username, sshd_cfg_file=DEF_SSHD_CFG): + ssh_cfg = parse_ssh_config_map(sshd_cfg_file) + key_paths = ssh_cfg.get("authorizedkeysfile", + "%h/.ssh/authorized_keys") ++ strictmodes = ssh_cfg.get("strictmodes", "yes") + auth_key_fns = render_authorizedkeysfile_paths( + key_paths, pw_ent.pw_dir, username) + +@@ -269,31 +377,31 @@ def extract_authorized_keys(username, sshd_cfg_file=DEF_SSHD_CFG): + "config from %r, using 'AuthorizedKeysFile' file " + "%r instead", DEF_SSHD_CFG, auth_key_fns[0]) + +- # check if one of the keys is the user's one ++ # check if one of the keys is the user's one and has the right permissions + for key_path, auth_key_fn in zip(key_paths.split(), auth_key_fns): + if any([ + '%u' in key_path, + '%h' in key_path, + auth_key_fn.startswith('{}/'.format(pw_ent.pw_dir)) + ]): +- user_authorizedkeys_file = auth_key_fn ++ permissions_ok = check_create_path(username, auth_key_fn, ++ strictmodes == "yes") ++ if permissions_ok: ++ user_authorizedkeys_file = auth_key_fn ++ break + + if user_authorizedkeys_file != default_authorizedkeys_file: + LOG.debug( + "AuthorizedKeysFile has an user-specific authorized_keys, " + "using %s", user_authorizedkeys_file) + +- # always store all the keys in the user's private file +- return (user_authorizedkeys_file, parse_authorized_keys(auth_key_fns)) ++ return ( ++ user_authorizedkeys_file, ++ parse_authorized_keys([user_authorizedkeys_file]) ++ ) + + + def setup_user_keys(keys, username, options=None): +- # Make sure the users .ssh dir is setup accordingly +- (ssh_dir, pwent) = users_ssh_info(username) +- if not os.path.isdir(ssh_dir): +- util.ensure_dir(ssh_dir, mode=0o700) +- util.chownbyid(ssh_dir, pwent.pw_uid, pwent.pw_gid) +- + # Turn the 'update' keys given into actual entries + parser = AuthKeyLineParser() + key_entries = [] +@@ -302,11 +410,10 @@ def setup_user_keys(keys, username, options=None): + + # Extract the old and make the new + (auth_key_fn, auth_key_entries) = extract_authorized_keys(username) ++ ssh_dir = os.path.dirname(auth_key_fn) + with util.SeLinuxGuard(ssh_dir, recursive=True): + content = update_authorized_keys(auth_key_entries, key_entries) +- util.ensure_dir(os.path.dirname(auth_key_fn), mode=0o700) +- util.write_file(auth_key_fn, content, mode=0o600) +- util.chownbyid(auth_key_fn, pwent.pw_uid, pwent.pw_gid) ++ util.write_file(auth_key_fn, content, preserve_mode=True) + + + class SshdConfigLine(object): +diff --git a/cloudinit/util.py b/cloudinit/util.py +index 4e0a72db..343976ad 100644 +--- a/cloudinit/util.py ++++ b/cloudinit/util.py +@@ -35,6 +35,7 @@ from base64 import b64decode, b64encode + from errno import ENOENT + from functools import lru_cache + from urllib import parse ++from typing import List + + from cloudinit import importer + from cloudinit import log as logging +@@ -1830,6 +1831,53 @@ def chmod(path, mode): + os.chmod(path, real_mode) + + ++def get_permissions(path: str) -> int: ++ """ ++ Returns the octal permissions of the file/folder pointed by the path, ++ encoded as an int. ++ ++ @param path: The full path of the file/folder. ++ """ ++ ++ return stat.S_IMODE(os.stat(path).st_mode) ++ ++ ++def get_owner(path: str) -> str: ++ """ ++ Returns the owner of the file/folder pointed by the path. ++ ++ @param path: The full path of the file/folder. ++ """ ++ st = os.stat(path) ++ return pwd.getpwuid(st.st_uid).pw_name ++ ++ ++def get_group(path: str) -> str: ++ """ ++ Returns the group of the file/folder pointed by the path. ++ ++ @param path: The full path of the file/folder. ++ """ ++ st = os.stat(path) ++ return grp.getgrgid(st.st_gid).gr_name ++ ++ ++def get_user_groups(username: str) -> List[str]: ++ """ ++ Returns a list of all groups to which the user belongs ++ ++ @param username: the user we want to check ++ """ ++ groups = [] ++ for group in grp.getgrall(): ++ if username in group.gr_mem: ++ groups.append(group.gr_name) ++ ++ gid = pwd.getpwnam(username).pw_gid ++ groups.append(grp.getgrgid(gid).gr_name) ++ return groups ++ ++ + def write_file( + filename, + content, +@@ -1856,8 +1904,7 @@ def write_file( + + if preserve_mode: + try: +- file_stat = os.stat(filename) +- mode = stat.S_IMODE(file_stat.st_mode) ++ mode = get_permissions(filename) + except OSError: + pass + +diff --git a/tests/unittests/test_sshutil.py b/tests/unittests/test_sshutil.py +index bcb8044f..a66788bf 100644 +--- a/tests/unittests/test_sshutil.py ++++ b/tests/unittests/test_sshutil.py +@@ -1,6 +1,9 @@ + # This file is part of cloud-init. See LICENSE file for license information. + ++import os ++ + from collections import namedtuple ++from functools import partial + from unittest.mock import patch + + from cloudinit import ssh_util +@@ -8,13 +11,48 @@ from cloudinit.tests import helpers as test_helpers + from cloudinit import util + + # https://stackoverflow.com/questions/11351032/ +-FakePwEnt = namedtuple( +- 'FakePwEnt', +- ['pw_dir', 'pw_gecos', 'pw_name', 'pw_passwd', 'pw_shell', 'pwd_uid']) ++FakePwEnt = namedtuple('FakePwEnt', [ ++ 'pw_name', ++ 'pw_passwd', ++ 'pw_uid', ++ 'pw_gid', ++ 'pw_gecos', ++ 'pw_dir', ++ 'pw_shell', ++]) + FakePwEnt.__new__.__defaults__ = tuple( + "UNSET_%s" % n for n in FakePwEnt._fields) + + ++def mock_get_owner(updated_permissions, value): ++ try: ++ return updated_permissions[value][0] ++ except ValueError: ++ return util.get_owner(value) ++ ++ ++def mock_get_group(updated_permissions, value): ++ try: ++ return updated_permissions[value][1] ++ except ValueError: ++ return util.get_group(value) ++ ++ ++def mock_get_user_groups(username): ++ return username ++ ++ ++def mock_get_permissions(updated_permissions, value): ++ try: ++ return updated_permissions[value][2] ++ except ValueError: ++ return util.get_permissions(value) ++ ++ ++def mock_getpwnam(users, username): ++ return users[username] ++ ++ + # Do not use these public keys, most of them are fetched from + # the testdata for OpenSSH, and their private keys are available + # https://github.com/openssh/openssh-portable/tree/master/regress/unittests/sshkey/testdata +@@ -552,12 +590,30 @@ class TestBasicAuthorizedKeyParse(test_helpers.CiTestCase): + ssh_util.render_authorizedkeysfile_paths( + "/opt/%u/keys", "/home/bobby", "bobby")) + ++ def test_user_file(self): ++ self.assertEqual( ++ ["/opt/bobby"], ++ ssh_util.render_authorizedkeysfile_paths( ++ "/opt/%u", "/home/bobby", "bobby")) ++ ++ def test_user_file2(self): ++ self.assertEqual( ++ ["/opt/bobby/bobby"], ++ ssh_util.render_authorizedkeysfile_paths( ++ "/opt/%u/%u", "/home/bobby", "bobby")) ++ + def test_multiple(self): + self.assertEqual( + ["/keys/path1", "/keys/path2"], + ssh_util.render_authorizedkeysfile_paths( + "/keys/path1 /keys/path2", "/home/bobby", "bobby")) + ++ def test_multiple2(self): ++ self.assertEqual( ++ ["/keys/path1", "/keys/bobby"], ++ ssh_util.render_authorizedkeysfile_paths( ++ "/keys/path1 /keys/%u", "/home/bobby", "bobby")) ++ + def test_relative(self): + self.assertEqual( + ["/home/bobby/.secret/keys"], +@@ -581,269 +637,763 @@ class TestBasicAuthorizedKeyParse(test_helpers.CiTestCase): + + class TestMultipleSshAuthorizedKeysFile(test_helpers.CiTestCase): + +- @patch("cloudinit.ssh_util.pwd.getpwnam") +- def test_multiple_authorizedkeys_file_order1(self, m_getpwnam): +- fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby') +- m_getpwnam.return_value = fpw +- user_ssh_folder = "%s/.ssh" % fpw.pw_dir +- +- # /tmp/home2/bobby/.ssh/authorized_keys = rsa +- authorized_keys = self.tmp_path('authorized_keys', dir=user_ssh_folder) +- util.write_file(authorized_keys, VALID_CONTENT['rsa']) +- +- # /tmp/home2/bobby/.ssh/user_keys = dsa +- user_keys = self.tmp_path('user_keys', dir=user_ssh_folder) +- util.write_file(user_keys, VALID_CONTENT['dsa']) +- +- # /tmp/sshd_config ++ def create_fake_users(self, names, mock_permissions, ++ m_get_group, m_get_owner, m_get_permissions, ++ m_getpwnam, users): ++ homes = [] ++ ++ root = '/tmp/root' ++ fpw = FakePwEnt(pw_name="root", pw_dir=root) ++ users["root"] = fpw ++ ++ for name in names: ++ home = '/tmp/home/' + name ++ fpw = FakePwEnt(pw_name=name, pw_dir=home) ++ users[name] = fpw ++ homes.append(home) ++ ++ m_get_permissions.side_effect = partial( ++ mock_get_permissions, mock_permissions) ++ m_get_owner.side_effect = partial(mock_get_owner, mock_permissions) ++ m_get_group.side_effect = partial(mock_get_group, mock_permissions) ++ m_getpwnam.side_effect = partial(mock_getpwnam, users) ++ return homes ++ ++ def create_user_authorized_file(self, home, filename, content_key, keys): ++ user_ssh_folder = "%s/.ssh" % home ++ # /tmp/home//.ssh/authorized_keys = content_key ++ authorized_keys = self.tmp_path(filename, dir=user_ssh_folder) ++ util.write_file(authorized_keys, VALID_CONTENT[content_key]) ++ keys[authorized_keys] = content_key ++ return authorized_keys ++ ++ def create_global_authorized_file(self, filename, content_key, keys): ++ authorized_keys = self.tmp_path(filename, dir='/tmp') ++ util.write_file(authorized_keys, VALID_CONTENT[content_key]) ++ keys[authorized_keys] = content_key ++ return authorized_keys ++ ++ def create_sshd_config(self, authorized_keys_files): + sshd_config = self.tmp_path('sshd_config', dir="/tmp") + util.write_file( + sshd_config, +- "AuthorizedKeysFile %s %s" % (authorized_keys, user_keys) ++ "AuthorizedKeysFile " + authorized_keys_files + ) ++ return sshd_config + ++ def execute_and_check(self, user, sshd_config, solution, keys, ++ delete_keys=True): + (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( +- fpw.pw_name, sshd_config) ++ user, sshd_config) + content = ssh_util.update_authorized_keys(auth_key_entries, []) + +- self.assertEqual(user_keys, auth_key_fn) +- self.assertTrue(VALID_CONTENT['rsa'] in content) +- self.assertTrue(VALID_CONTENT['dsa'] in content) ++ self.assertEqual(auth_key_fn, solution) ++ for path, key in keys.items(): ++ if path == solution: ++ self.assertTrue(VALID_CONTENT[key] in content) ++ else: ++ self.assertFalse(VALID_CONTENT[key] in content) ++ ++ if delete_keys and os.path.isdir("/tmp/home/"): ++ util.delete_dir_contents("/tmp/home/") + + @patch("cloudinit.ssh_util.pwd.getpwnam") +- def test_multiple_authorizedkeys_file_order2(self, m_getpwnam): +- fpw = FakePwEnt(pw_name='suzie', pw_dir='/tmp/home/suzie') +- m_getpwnam.return_value = fpw +- user_ssh_folder = "%s/.ssh" % fpw.pw_dir ++ @patch("cloudinit.util.get_permissions") ++ @patch("cloudinit.util.get_owner") ++ @patch("cloudinit.util.get_group") ++ def test_single_user_two_local_files( ++ self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam ++ ): ++ user_bobby = 'bobby' ++ keys = {} ++ users = {} ++ mock_permissions = { ++ '/tmp/home/bobby': ('bobby', 'bobby', 0o700), ++ '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700), ++ '/tmp/home/bobby/.ssh/user_keys': ('bobby', 'bobby', 0o600), ++ '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600), ++ } ++ ++ homes = self.create_fake_users( ++ [user_bobby], mock_permissions, m_get_group, m_get_owner, ++ m_get_permissions, m_getpwnam, users ++ ) ++ home = homes[0] + +- # /tmp/home/suzie/.ssh/authorized_keys = rsa +- authorized_keys = self.tmp_path('authorized_keys', dir=user_ssh_folder) +- util.write_file(authorized_keys, VALID_CONTENT['rsa']) ++ # /tmp/home/bobby/.ssh/authorized_keys = rsa ++ authorized_keys = self.create_user_authorized_file( ++ home, 'authorized_keys', 'rsa', keys ++ ) + +- # /tmp/home/suzie/.ssh/user_keys = dsa +- user_keys = self.tmp_path('user_keys', dir=user_ssh_folder) +- util.write_file(user_keys, VALID_CONTENT['dsa']) ++ # /tmp/home/bobby/.ssh/user_keys = dsa ++ user_keys = self.create_user_authorized_file( ++ home, 'user_keys', 'dsa', keys ++ ) + + # /tmp/sshd_config +- sshd_config = self.tmp_path('sshd_config', dir="/tmp") +- util.write_file( +- sshd_config, +- "AuthorizedKeysFile %s %s" % (user_keys, authorized_keys) ++ options = "%s %s" % (authorized_keys, user_keys) ++ sshd_config = self.create_sshd_config(options) ++ ++ self.execute_and_check(user_bobby, sshd_config, authorized_keys, keys) ++ ++ @patch("cloudinit.ssh_util.pwd.getpwnam") ++ @patch("cloudinit.util.get_permissions") ++ @patch("cloudinit.util.get_owner") ++ @patch("cloudinit.util.get_group") ++ def test_single_user_two_local_files_inverted( ++ self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam ++ ): ++ user_bobby = 'bobby' ++ keys = {} ++ users = {} ++ mock_permissions = { ++ '/tmp/home/bobby': ('bobby', 'bobby', 0o700), ++ '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700), ++ '/tmp/home/bobby/.ssh/user_keys': ('bobby', 'bobby', 0o600), ++ '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600), ++ } ++ ++ homes = self.create_fake_users( ++ [user_bobby], mock_permissions, m_get_group, m_get_owner, ++ m_get_permissions, m_getpwnam, users + ) ++ home = homes[0] + +- (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( +- fpw.pw_name, sshd_config) +- content = ssh_util.update_authorized_keys(auth_key_entries, []) ++ # /tmp/home/bobby/.ssh/authorized_keys = rsa ++ authorized_keys = self.create_user_authorized_file( ++ home, 'authorized_keys', 'rsa', keys ++ ) + +- self.assertEqual(authorized_keys, auth_key_fn) +- self.assertTrue(VALID_CONTENT['rsa'] in content) +- self.assertTrue(VALID_CONTENT['dsa'] in content) ++ # /tmp/home/bobby/.ssh/user_keys = dsa ++ user_keys = self.create_user_authorized_file( ++ home, 'user_keys', 'dsa', keys ++ ) + +- @patch("cloudinit.ssh_util.pwd.getpwnam") +- def test_multiple_authorizedkeys_file_local_global(self, m_getpwnam): +- fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby') +- m_getpwnam.return_value = fpw +- user_ssh_folder = "%s/.ssh" % fpw.pw_dir ++ # /tmp/sshd_config ++ options = "%s %s" % (user_keys, authorized_keys) ++ sshd_config = self.create_sshd_config(options) + +- # /tmp/home2/bobby/.ssh/authorized_keys = rsa +- authorized_keys = self.tmp_path('authorized_keys', dir=user_ssh_folder) +- util.write_file(authorized_keys, VALID_CONTENT['rsa']) ++ self.execute_and_check(user_bobby, sshd_config, user_keys, keys) + +- # /tmp/home2/bobby/.ssh/user_keys = dsa +- user_keys = self.tmp_path('user_keys', dir=user_ssh_folder) +- util.write_file(user_keys, VALID_CONTENT['dsa']) ++ @patch("cloudinit.ssh_util.pwd.getpwnam") ++ @patch("cloudinit.util.get_permissions") ++ @patch("cloudinit.util.get_owner") ++ @patch("cloudinit.util.get_group") ++ def test_single_user_local_global_files( ++ self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam ++ ): ++ user_bobby = 'bobby' ++ keys = {} ++ users = {} ++ mock_permissions = { ++ '/tmp/home/bobby': ('bobby', 'bobby', 0o700), ++ '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700), ++ '/tmp/home/bobby/.ssh/user_keys': ('bobby', 'bobby', 0o600), ++ '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600), ++ } ++ ++ homes = self.create_fake_users( ++ [user_bobby], mock_permissions, m_get_group, m_get_owner, ++ m_get_permissions, m_getpwnam, users ++ ) ++ home = homes[0] + +- # /tmp/etc/ssh/authorized_keys = ecdsa +- authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys', +- dir="/tmp") +- util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa']) ++ # /tmp/home/bobby/.ssh/authorized_keys = rsa ++ authorized_keys = self.create_user_authorized_file( ++ home, 'authorized_keys', 'rsa', keys ++ ) + +- # /tmp/sshd_config +- sshd_config = self.tmp_path('sshd_config', dir="/tmp") +- util.write_file( +- sshd_config, +- "AuthorizedKeysFile %s %s %s" % (authorized_keys_global, +- user_keys, authorized_keys) ++ # /tmp/home/bobby/.ssh/user_keys = dsa ++ user_keys = self.create_user_authorized_file( ++ home, 'user_keys', 'dsa', keys + ) + +- (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( +- fpw.pw_name, sshd_config) +- content = ssh_util.update_authorized_keys(auth_key_entries, []) ++ authorized_keys_global = self.create_global_authorized_file( ++ 'etc/ssh/authorized_keys', 'ecdsa', keys ++ ) + +- self.assertEqual(authorized_keys, auth_key_fn) +- self.assertTrue(VALID_CONTENT['rsa'] in content) +- self.assertTrue(VALID_CONTENT['ecdsa'] in content) +- self.assertTrue(VALID_CONTENT['dsa'] in content) ++ options = "%s %s %s" % (authorized_keys_global, user_keys, ++ authorized_keys) ++ sshd_config = self.create_sshd_config(options) + +- @patch("cloudinit.ssh_util.pwd.getpwnam") +- def test_multiple_authorizedkeys_file_local_global2(self, m_getpwnam): +- fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby') +- m_getpwnam.return_value = fpw +- user_ssh_folder = "%s/.ssh" % fpw.pw_dir ++ self.execute_and_check(user_bobby, sshd_config, user_keys, keys) + +- # /tmp/home2/bobby/.ssh/authorized_keys2 = rsa +- authorized_keys = self.tmp_path('authorized_keys2', +- dir=user_ssh_folder) +- util.write_file(authorized_keys, VALID_CONTENT['rsa']) ++ @patch("cloudinit.ssh_util.pwd.getpwnam") ++ @patch("cloudinit.util.get_permissions") ++ @patch("cloudinit.util.get_owner") ++ @patch("cloudinit.util.get_group") ++ def test_single_user_local_global_files_inverted( ++ self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam ++ ): ++ user_bobby = 'bobby' ++ keys = {} ++ users = {} ++ mock_permissions = { ++ '/tmp/home/bobby': ('bobby', 'bobby', 0o700), ++ '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700), ++ '/tmp/home/bobby/.ssh/user_keys3': ('bobby', 'bobby', 0o600), ++ '/tmp/home/bobby/.ssh/authorized_keys2': ('bobby', 'bobby', 0o600), ++ } ++ ++ homes = self.create_fake_users( ++ [user_bobby], mock_permissions, m_get_group, m_get_owner, ++ m_get_permissions, m_getpwnam, users ++ ) ++ home = homes[0] + +- # /tmp/home2/bobby/.ssh/user_keys3 = dsa +- user_keys = self.tmp_path('user_keys3', dir=user_ssh_folder) +- util.write_file(user_keys, VALID_CONTENT['dsa']) ++ # /tmp/home/bobby/.ssh/authorized_keys = rsa ++ authorized_keys = self.create_user_authorized_file( ++ home, 'authorized_keys2', 'rsa', keys ++ ) + +- # /tmp/etc/ssh/authorized_keys = ecdsa +- authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys', +- dir="/tmp") +- util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa']) ++ # /tmp/home/bobby/.ssh/user_keys = dsa ++ user_keys = self.create_user_authorized_file( ++ home, 'user_keys3', 'dsa', keys ++ ) + +- # /tmp/sshd_config +- sshd_config = self.tmp_path('sshd_config', dir="/tmp") +- util.write_file( +- sshd_config, +- "AuthorizedKeysFile %s %s %s" % (authorized_keys_global, +- authorized_keys, user_keys) ++ authorized_keys_global = self.create_global_authorized_file( ++ 'etc/ssh/authorized_keys', 'ecdsa', keys + ) + +- (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( +- fpw.pw_name, sshd_config) +- content = ssh_util.update_authorized_keys(auth_key_entries, []) ++ options = "%s %s %s" % (authorized_keys_global, authorized_keys, ++ user_keys) ++ sshd_config = self.create_sshd_config(options) + +- self.assertEqual(user_keys, auth_key_fn) +- self.assertTrue(VALID_CONTENT['rsa'] in content) +- self.assertTrue(VALID_CONTENT['ecdsa'] in content) +- self.assertTrue(VALID_CONTENT['dsa'] in content) ++ self.execute_and_check(user_bobby, sshd_config, authorized_keys, keys) + + @patch("cloudinit.ssh_util.pwd.getpwnam") +- def test_multiple_authorizedkeys_file_global(self, m_getpwnam): +- fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby') +- m_getpwnam.return_value = fpw ++ @patch("cloudinit.util.get_permissions") ++ @patch("cloudinit.util.get_owner") ++ @patch("cloudinit.util.get_group") ++ def test_single_user_global_file( ++ self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam ++ ): ++ user_bobby = 'bobby' ++ keys = {} ++ users = {} ++ mock_permissions = { ++ '/tmp/home/bobby': ('bobby', 'bobby', 0o700), ++ '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700), ++ '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600), ++ } ++ ++ homes = self.create_fake_users( ++ [user_bobby], mock_permissions, m_get_group, m_get_owner, ++ m_get_permissions, m_getpwnam, users ++ ) ++ home = homes[0] + + # /tmp/etc/ssh/authorized_keys = rsa +- authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys', +- dir="/tmp") +- util.write_file(authorized_keys_global, VALID_CONTENT['rsa']) ++ authorized_keys_global = self.create_global_authorized_file( ++ 'etc/ssh/authorized_keys', 'rsa', keys ++ ) + +- # /tmp/sshd_config +- sshd_config = self.tmp_path('sshd_config') +- util.write_file( +- sshd_config, +- "AuthorizedKeysFile %s" % (authorized_keys_global) ++ options = "%s" % authorized_keys_global ++ sshd_config = self.create_sshd_config(options) ++ ++ default = "%s/.ssh/authorized_keys" % home ++ self.execute_and_check(user_bobby, sshd_config, default, keys) ++ ++ @patch("cloudinit.ssh_util.pwd.getpwnam") ++ @patch("cloudinit.util.get_permissions") ++ @patch("cloudinit.util.get_owner") ++ @patch("cloudinit.util.get_group") ++ def test_two_users_local_file_standard( ++ self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam ++ ): ++ keys = {} ++ users = {} ++ mock_permissions = { ++ '/tmp/home/bobby': ('bobby', 'bobby', 0o700), ++ '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700), ++ '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600), ++ '/tmp/home/suzie': ('suzie', 'suzie', 0o700), ++ '/tmp/home/suzie/.ssh': ('suzie', 'suzie', 0o700), ++ '/tmp/home/suzie/.ssh/authorized_keys': ('suzie', 'suzie', 0o600), ++ } ++ ++ user_bobby = 'bobby' ++ user_suzie = 'suzie' ++ homes = self.create_fake_users( ++ [user_bobby, user_suzie], mock_permissions, m_get_group, ++ m_get_owner, m_get_permissions, m_getpwnam, users + ) ++ home_bobby = homes[0] ++ home_suzie = homes[1] + +- (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( +- fpw.pw_name, sshd_config) +- content = ssh_util.update_authorized_keys(auth_key_entries, []) ++ # /tmp/home/bobby/.ssh/authorized_keys = rsa ++ authorized_keys = self.create_user_authorized_file( ++ home_bobby, 'authorized_keys', 'rsa', keys ++ ) + +- self.assertEqual("%s/.ssh/authorized_keys" % fpw.pw_dir, auth_key_fn) +- self.assertTrue(VALID_CONTENT['rsa'] in content) ++ # /tmp/home/suzie/.ssh/authorized_keys = rsa ++ authorized_keys2 = self.create_user_authorized_file( ++ home_suzie, 'authorized_keys', 'ssh-xmss@openssh.com', keys ++ ) ++ ++ options = ".ssh/authorized_keys" ++ sshd_config = self.create_sshd_config(options) ++ ++ self.execute_and_check( ++ user_bobby, sshd_config, authorized_keys, keys, delete_keys=False ++ ) ++ self.execute_and_check(user_suzie, sshd_config, authorized_keys2, keys) + + @patch("cloudinit.ssh_util.pwd.getpwnam") +- def test_multiple_authorizedkeys_file_multiuser(self, m_getpwnam): +- fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby') +- m_getpwnam.return_value = fpw +- user_ssh_folder = "%s/.ssh" % fpw.pw_dir +- # /tmp/home2/bobby/.ssh/authorized_keys2 = rsa +- authorized_keys = self.tmp_path('authorized_keys2', +- dir=user_ssh_folder) +- util.write_file(authorized_keys, VALID_CONTENT['rsa']) +- # /tmp/home2/bobby/.ssh/user_keys3 = dsa +- user_keys = self.tmp_path('user_keys3', dir=user_ssh_folder) +- util.write_file(user_keys, VALID_CONTENT['dsa']) +- +- fpw2 = FakePwEnt(pw_name='suzie', pw_dir='/tmp/home/suzie') +- user_ssh_folder = "%s/.ssh" % fpw2.pw_dir +- # /tmp/home/suzie/.ssh/authorized_keys2 = ssh-xmss@openssh.com +- authorized_keys2 = self.tmp_path('authorized_keys2', +- dir=user_ssh_folder) +- util.write_file(authorized_keys2, +- VALID_CONTENT['ssh-xmss@openssh.com']) ++ @patch("cloudinit.util.get_permissions") ++ @patch("cloudinit.util.get_owner") ++ @patch("cloudinit.util.get_group") ++ def test_two_users_local_file_custom( ++ self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam ++ ): ++ keys = {} ++ users = {} ++ mock_permissions = { ++ '/tmp/home/bobby': ('bobby', 'bobby', 0o700), ++ '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700), ++ '/tmp/home/bobby/.ssh/authorized_keys2': ('bobby', 'bobby', 0o600), ++ '/tmp/home/suzie': ('suzie', 'suzie', 0o700), ++ '/tmp/home/suzie/.ssh': ('suzie', 'suzie', 0o700), ++ '/tmp/home/suzie/.ssh/authorized_keys2': ('suzie', 'suzie', 0o600), ++ } ++ ++ user_bobby = 'bobby' ++ user_suzie = 'suzie' ++ homes = self.create_fake_users( ++ [user_bobby, user_suzie], mock_permissions, m_get_group, ++ m_get_owner, m_get_permissions, m_getpwnam, users ++ ) ++ home_bobby = homes[0] ++ home_suzie = homes[1] + +- # /tmp/etc/ssh/authorized_keys = ecdsa +- authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys2', +- dir="/tmp") +- util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa']) ++ # /tmp/home/bobby/.ssh/authorized_keys2 = rsa ++ authorized_keys = self.create_user_authorized_file( ++ home_bobby, 'authorized_keys2', 'rsa', keys ++ ) + +- # /tmp/sshd_config +- sshd_config = self.tmp_path('sshd_config', dir="/tmp") +- util.write_file( +- sshd_config, +- "AuthorizedKeysFile %s %%h/.ssh/authorized_keys2 %s" % +- (authorized_keys_global, user_keys) ++ # /tmp/home/suzie/.ssh/authorized_keys2 = rsa ++ authorized_keys2 = self.create_user_authorized_file( ++ home_suzie, 'authorized_keys2', 'ssh-xmss@openssh.com', keys + ) + +- # process first user +- (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( +- fpw.pw_name, sshd_config) +- content = ssh_util.update_authorized_keys(auth_key_entries, []) ++ options = ".ssh/authorized_keys2" ++ sshd_config = self.create_sshd_config(options) ++ ++ self.execute_and_check( ++ user_bobby, sshd_config, authorized_keys, keys, delete_keys=False ++ ) ++ self.execute_and_check(user_suzie, sshd_config, authorized_keys2, keys) + +- self.assertEqual(user_keys, auth_key_fn) +- self.assertTrue(VALID_CONTENT['rsa'] in content) +- self.assertTrue(VALID_CONTENT['ecdsa'] in content) +- self.assertTrue(VALID_CONTENT['dsa'] in content) +- self.assertFalse(VALID_CONTENT['ssh-xmss@openssh.com'] in content) ++ @patch("cloudinit.ssh_util.pwd.getpwnam") ++ @patch("cloudinit.util.get_permissions") ++ @patch("cloudinit.util.get_owner") ++ @patch("cloudinit.util.get_group") ++ def test_two_users_local_global_files( ++ self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam ++ ): ++ keys = {} ++ users = {} ++ mock_permissions = { ++ '/tmp/home/bobby': ('bobby', 'bobby', 0o700), ++ '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700), ++ '/tmp/home/bobby/.ssh/authorized_keys2': ('bobby', 'bobby', 0o600), ++ '/tmp/home/bobby/.ssh/user_keys3': ('bobby', 'bobby', 0o600), ++ '/tmp/home/suzie': ('suzie', 'suzie', 0o700), ++ '/tmp/home/suzie/.ssh': ('suzie', 'suzie', 0o700), ++ '/tmp/home/suzie/.ssh/authorized_keys2': ('suzie', 'suzie', 0o600), ++ '/tmp/home/suzie/.ssh/user_keys3': ('suzie', 'suzie', 0o600), ++ } ++ ++ user_bobby = 'bobby' ++ user_suzie = 'suzie' ++ homes = self.create_fake_users( ++ [user_bobby, user_suzie], mock_permissions, m_get_group, ++ m_get_owner, m_get_permissions, m_getpwnam, users ++ ) ++ home_bobby = homes[0] ++ home_suzie = homes[1] + +- m_getpwnam.return_value = fpw2 +- # process second user +- (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( +- fpw2.pw_name, sshd_config) +- content = ssh_util.update_authorized_keys(auth_key_entries, []) ++ # /tmp/home/bobby/.ssh/authorized_keys2 = rsa ++ self.create_user_authorized_file( ++ home_bobby, 'authorized_keys2', 'rsa', keys ++ ) ++ # /tmp/home/bobby/.ssh/user_keys3 = dsa ++ user_keys = self.create_user_authorized_file( ++ home_bobby, 'user_keys3', 'dsa', keys ++ ) ++ ++ # /tmp/home/suzie/.ssh/authorized_keys2 = rsa ++ authorized_keys2 = self.create_user_authorized_file( ++ home_suzie, 'authorized_keys2', 'ssh-xmss@openssh.com', keys ++ ) ++ ++ # /tmp/etc/ssh/authorized_keys = ecdsa ++ authorized_keys_global = self.create_global_authorized_file( ++ 'etc/ssh/authorized_keys2', 'ecdsa', keys ++ ) ++ ++ options = "%s %s %%h/.ssh/authorized_keys2" % \ ++ (authorized_keys_global, user_keys) ++ sshd_config = self.create_sshd_config(options) + +- self.assertEqual(authorized_keys2, auth_key_fn) +- self.assertTrue(VALID_CONTENT['ssh-xmss@openssh.com'] in content) +- self.assertTrue(VALID_CONTENT['ecdsa'] in content) +- self.assertTrue(VALID_CONTENT['dsa'] in content) +- self.assertFalse(VALID_CONTENT['rsa'] in content) ++ self.execute_and_check( ++ user_bobby, sshd_config, user_keys, keys, delete_keys=False ++ ) ++ self.execute_and_check(user_suzie, sshd_config, authorized_keys2, keys) + ++ @patch("cloudinit.util.get_user_groups") + @patch("cloudinit.ssh_util.pwd.getpwnam") +- def test_multiple_authorizedkeys_file_multiuser2(self, m_getpwnam): +- fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home/bobby') +- m_getpwnam.return_value = fpw +- user_ssh_folder = "%s/.ssh" % fpw.pw_dir ++ @patch("cloudinit.util.get_permissions") ++ @patch("cloudinit.util.get_owner") ++ @patch("cloudinit.util.get_group") ++ def test_two_users_local_global_files_badguy( ++ self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam, ++ m_get_user_groups ++ ): ++ keys = {} ++ users = {} ++ mock_permissions = { ++ '/tmp/home/bobby': ('bobby', 'bobby', 0o700), ++ '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700), ++ '/tmp/home/bobby/.ssh/authorized_keys2': ('bobby', 'bobby', 0o600), ++ '/tmp/home/bobby/.ssh/user_keys3': ('bobby', 'bobby', 0o600), ++ '/tmp/home/badguy': ('root', 'root', 0o755), ++ '/tmp/home/badguy/home': ('root', 'root', 0o755), ++ '/tmp/home/badguy/home/bobby': ('root', 'root', 0o655), ++ } ++ ++ user_bobby = 'bobby' ++ user_badguy = 'badguy' ++ home_bobby, *_ = self.create_fake_users( ++ [user_bobby, user_badguy], mock_permissions, m_get_group, ++ m_get_owner, m_get_permissions, m_getpwnam, users ++ ) ++ m_get_user_groups.side_effect = mock_get_user_groups ++ + # /tmp/home/bobby/.ssh/authorized_keys2 = rsa +- authorized_keys = self.tmp_path('authorized_keys2', +- dir=user_ssh_folder) +- util.write_file(authorized_keys, VALID_CONTENT['rsa']) ++ authorized_keys = self.create_user_authorized_file( ++ home_bobby, 'authorized_keys2', 'rsa', keys ++ ) + # /tmp/home/bobby/.ssh/user_keys3 = dsa +- user_keys = self.tmp_path('user_keys3', dir=user_ssh_folder) +- util.write_file(user_keys, VALID_CONTENT['dsa']) ++ user_keys = self.create_user_authorized_file( ++ home_bobby, 'user_keys3', 'dsa', keys ++ ) + +- fpw2 = FakePwEnt(pw_name='badguy', pw_dir='/tmp/home/badguy') +- user_ssh_folder = "%s/.ssh" % fpw2.pw_dir + # /tmp/home/badguy/home/bobby = "" + authorized_keys2 = self.tmp_path('home/bobby', dir="/tmp/home/badguy") ++ util.write_file(authorized_keys2, '') + + # /tmp/etc/ssh/authorized_keys = ecdsa +- authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys2', +- dir="/tmp") +- util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa']) ++ authorized_keys_global = self.create_global_authorized_file( ++ 'etc/ssh/authorized_keys2', 'ecdsa', keys ++ ) + + # /tmp/sshd_config +- sshd_config = self.tmp_path('sshd_config', dir="/tmp") +- util.write_file( +- sshd_config, +- "AuthorizedKeysFile %s %%h/.ssh/authorized_keys2 %s %s" % +- (authorized_keys_global, user_keys, authorized_keys2) ++ options = "%s %%h/.ssh/authorized_keys2 %s %s" % \ ++ (authorized_keys2, authorized_keys_global, user_keys) ++ sshd_config = self.create_sshd_config(options) ++ ++ self.execute_and_check( ++ user_bobby, sshd_config, authorized_keys, keys, delete_keys=False ++ ) ++ self.execute_and_check( ++ user_badguy, sshd_config, authorized_keys2, keys + ) + +- # process first user +- (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( +- fpw.pw_name, sshd_config) +- content = ssh_util.update_authorized_keys(auth_key_entries, []) ++ @patch("cloudinit.util.get_user_groups") ++ @patch("cloudinit.ssh_util.pwd.getpwnam") ++ @patch("cloudinit.util.get_permissions") ++ @patch("cloudinit.util.get_owner") ++ @patch("cloudinit.util.get_group") ++ def test_two_users_unaccessible_file( ++ self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam, ++ m_get_user_groups ++ ): ++ keys = {} ++ users = {} ++ mock_permissions = { ++ '/tmp/home/bobby': ('bobby', 'bobby', 0o700), ++ '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700), ++ '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600), ++ ++ '/tmp/etc': ('root', 'root', 0o755), ++ '/tmp/etc/ssh': ('root', 'root', 0o755), ++ '/tmp/etc/ssh/userkeys': ('root', 'root', 0o700), ++ '/tmp/etc/ssh/userkeys/bobby': ('bobby', 'bobby', 0o600), ++ '/tmp/etc/ssh/userkeys/badguy': ('badguy', 'badguy', 0o600), ++ ++ '/tmp/home/badguy': ('badguy', 'badguy', 0o700), ++ '/tmp/home/badguy/.ssh': ('badguy', 'badguy', 0o700), ++ '/tmp/home/badguy/.ssh/authorized_keys': ++ ('badguy', 'badguy', 0o600), ++ } ++ ++ user_bobby = 'bobby' ++ user_badguy = 'badguy' ++ homes = self.create_fake_users( ++ [user_bobby, user_badguy], mock_permissions, m_get_group, ++ m_get_owner, m_get_permissions, m_getpwnam, users ++ ) ++ m_get_user_groups.side_effect = mock_get_user_groups ++ home_bobby = homes[0] ++ home_badguy = homes[1] + +- self.assertEqual(user_keys, auth_key_fn) +- self.assertTrue(VALID_CONTENT['rsa'] in content) +- self.assertTrue(VALID_CONTENT['ecdsa'] in content) +- self.assertTrue(VALID_CONTENT['dsa'] in content) ++ # /tmp/home/bobby/.ssh/authorized_keys = rsa ++ authorized_keys = self.create_user_authorized_file( ++ home_bobby, 'authorized_keys', 'rsa', keys ++ ) ++ # /tmp/etc/ssh/userkeys/bobby = dsa ++ # assume here that we can bypass userkeys, despite permissions ++ self.create_global_authorized_file( ++ 'etc/ssh/userkeys/bobby', 'dsa', keys ++ ) + +- m_getpwnam.return_value = fpw2 +- # process second user +- (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( +- fpw2.pw_name, sshd_config) +- content = ssh_util.update_authorized_keys(auth_key_entries, []) ++ # /tmp/home/badguy/.ssh/authorized_keys = ssh-xmss@openssh.com ++ authorized_keys2 = self.create_user_authorized_file( ++ home_badguy, 'authorized_keys', 'ssh-xmss@openssh.com', keys ++ ) + +- # badguy should not take the key from the other user! +- self.assertEqual(authorized_keys2, auth_key_fn) +- self.assertTrue(VALID_CONTENT['ecdsa'] in content) +- self.assertTrue(VALID_CONTENT['dsa'] in content) +- self.assertFalse(VALID_CONTENT['rsa'] in content) ++ # /tmp/etc/ssh/userkeys/badguy = ecdsa ++ self.create_global_authorized_file( ++ 'etc/ssh/userkeys/badguy', 'ecdsa', keys ++ ) ++ ++ # /tmp/sshd_config ++ options = "/tmp/etc/ssh/userkeys/%u .ssh/authorized_keys" ++ sshd_config = self.create_sshd_config(options) ++ ++ self.execute_and_check( ++ user_bobby, sshd_config, authorized_keys, keys, delete_keys=False ++ ) ++ self.execute_and_check( ++ user_badguy, sshd_config, authorized_keys2, keys ++ ) ++ ++ @patch("cloudinit.util.get_user_groups") ++ @patch("cloudinit.ssh_util.pwd.getpwnam") ++ @patch("cloudinit.util.get_permissions") ++ @patch("cloudinit.util.get_owner") ++ @patch("cloudinit.util.get_group") ++ def test_two_users_accessible_file( ++ self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam, ++ m_get_user_groups ++ ): ++ keys = {} ++ users = {} ++ mock_permissions = { ++ '/tmp/home/bobby': ('bobby', 'bobby', 0o700), ++ '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700), ++ '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600), ++ ++ '/tmp/etc': ('root', 'root', 0o755), ++ '/tmp/etc/ssh': ('root', 'root', 0o755), ++ '/tmp/etc/ssh/userkeys': ('root', 'root', 0o755), ++ '/tmp/etc/ssh/userkeys/bobby': ('bobby', 'bobby', 0o600), ++ '/tmp/etc/ssh/userkeys/badguy': ('badguy', 'badguy', 0o600), ++ ++ '/tmp/home/badguy': ('badguy', 'badguy', 0o700), ++ '/tmp/home/badguy/.ssh': ('badguy', 'badguy', 0o700), ++ '/tmp/home/badguy/.ssh/authorized_keys': ++ ('badguy', 'badguy', 0o600), ++ } ++ ++ user_bobby = 'bobby' ++ user_badguy = 'badguy' ++ homes = self.create_fake_users( ++ [user_bobby, user_badguy], mock_permissions, m_get_group, ++ m_get_owner, m_get_permissions, m_getpwnam, users ++ ) ++ m_get_user_groups.side_effect = mock_get_user_groups ++ home_bobby = homes[0] ++ home_badguy = homes[1] ++ ++ # /tmp/home/bobby/.ssh/authorized_keys = rsa ++ self.create_user_authorized_file( ++ home_bobby, 'authorized_keys', 'rsa', keys ++ ) ++ # /tmp/etc/ssh/userkeys/bobby = dsa ++ # assume here that we can bypass userkeys, despite permissions ++ authorized_keys = self.create_global_authorized_file( ++ 'etc/ssh/userkeys/bobby', 'dsa', keys ++ ) ++ ++ # /tmp/home/badguy/.ssh/authorized_keys = ssh-xmss@openssh.com ++ self.create_user_authorized_file( ++ home_badguy, 'authorized_keys', 'ssh-xmss@openssh.com', keys ++ ) ++ ++ # /tmp/etc/ssh/userkeys/badguy = ecdsa ++ authorized_keys2 = self.create_global_authorized_file( ++ 'etc/ssh/userkeys/badguy', 'ecdsa', keys ++ ) ++ ++ # /tmp/sshd_config ++ options = "/tmp/etc/ssh/userkeys/%u .ssh/authorized_keys" ++ sshd_config = self.create_sshd_config(options) ++ ++ self.execute_and_check( ++ user_bobby, sshd_config, authorized_keys, keys, delete_keys=False ++ ) ++ self.execute_and_check( ++ user_badguy, sshd_config, authorized_keys2, keys ++ ) ++ ++ @patch("cloudinit.util.get_user_groups") ++ @patch("cloudinit.ssh_util.pwd.getpwnam") ++ @patch("cloudinit.util.get_permissions") ++ @patch("cloudinit.util.get_owner") ++ @patch("cloudinit.util.get_group") ++ def test_two_users_hardcoded_single_user_file( ++ self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam, ++ m_get_user_groups ++ ): ++ keys = {} ++ users = {} ++ mock_permissions = { ++ '/tmp/home/bobby': ('bobby', 'bobby', 0o700), ++ '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700), ++ '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600), ++ ++ '/tmp/home/suzie': ('suzie', 'suzie', 0o700), ++ '/tmp/home/suzie/.ssh': ('suzie', 'suzie', 0o700), ++ '/tmp/home/suzie/.ssh/authorized_keys': ('suzie', 'suzie', 0o600), ++ } ++ ++ user_bobby = 'bobby' ++ user_suzie = 'suzie' ++ homes = self.create_fake_users( ++ [user_bobby, user_suzie], mock_permissions, m_get_group, ++ m_get_owner, m_get_permissions, m_getpwnam, users ++ ) ++ home_bobby = homes[0] ++ home_suzie = homes[1] ++ m_get_user_groups.side_effect = mock_get_user_groups ++ ++ # /tmp/home/bobby/.ssh/authorized_keys = rsa ++ authorized_keys = self.create_user_authorized_file( ++ home_bobby, 'authorized_keys', 'rsa', keys ++ ) ++ ++ # /tmp/home/suzie/.ssh/authorized_keys = ssh-xmss@openssh.com ++ self.create_user_authorized_file( ++ home_suzie, 'authorized_keys', 'ssh-xmss@openssh.com', keys ++ ) ++ ++ # /tmp/sshd_config ++ options = "%s" % (authorized_keys) ++ sshd_config = self.create_sshd_config(options) ++ ++ self.execute_and_check( ++ user_bobby, sshd_config, authorized_keys, keys, delete_keys=False ++ ) ++ default = "%s/.ssh/authorized_keys" % home_suzie ++ self.execute_and_check(user_suzie, sshd_config, default, keys) ++ ++ @patch("cloudinit.util.get_user_groups") ++ @patch("cloudinit.ssh_util.pwd.getpwnam") ++ @patch("cloudinit.util.get_permissions") ++ @patch("cloudinit.util.get_owner") ++ @patch("cloudinit.util.get_group") ++ def test_two_users_hardcoded_single_user_file_inverted( ++ self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam, ++ m_get_user_groups ++ ): ++ keys = {} ++ users = {} ++ mock_permissions = { ++ '/tmp/home/bobby': ('bobby', 'bobby', 0o700), ++ '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700), ++ '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600), ++ ++ '/tmp/home/suzie': ('suzie', 'suzie', 0o700), ++ '/tmp/home/suzie/.ssh': ('suzie', 'suzie', 0o700), ++ '/tmp/home/suzie/.ssh/authorized_keys': ('suzie', 'suzie', 0o600), ++ } ++ ++ user_bobby = 'bobby' ++ user_suzie = 'suzie' ++ homes = self.create_fake_users( ++ [user_bobby, user_suzie], mock_permissions, m_get_group, ++ m_get_owner, m_get_permissions, m_getpwnam, users ++ ) ++ home_bobby = homes[0] ++ home_suzie = homes[1] ++ m_get_user_groups.side_effect = mock_get_user_groups ++ ++ # /tmp/home/bobby/.ssh/authorized_keys = rsa ++ self.create_user_authorized_file( ++ home_bobby, 'authorized_keys', 'rsa', keys ++ ) ++ ++ # /tmp/home/suzie/.ssh/authorized_keys = ssh-xmss@openssh.com ++ authorized_keys2 = self.create_user_authorized_file( ++ home_suzie, 'authorized_keys', 'ssh-xmss@openssh.com', keys ++ ) ++ ++ # /tmp/sshd_config ++ options = "%s" % (authorized_keys2) ++ sshd_config = self.create_sshd_config(options) ++ ++ default = "%s/.ssh/authorized_keys" % home_bobby ++ self.execute_and_check( ++ user_bobby, sshd_config, default, keys, delete_keys=False ++ ) ++ self.execute_and_check(user_suzie, sshd_config, authorized_keys2, keys) ++ ++ @patch("cloudinit.util.get_user_groups") ++ @patch("cloudinit.ssh_util.pwd.getpwnam") ++ @patch("cloudinit.util.get_permissions") ++ @patch("cloudinit.util.get_owner") ++ @patch("cloudinit.util.get_group") ++ def test_two_users_hardcoded_user_files( ++ self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam, ++ m_get_user_groups ++ ): ++ keys = {} ++ users = {} ++ mock_permissions = { ++ '/tmp/home/bobby': ('bobby', 'bobby', 0o700), ++ '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700), ++ '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600), ++ ++ '/tmp/home/suzie': ('suzie', 'suzie', 0o700), ++ '/tmp/home/suzie/.ssh': ('suzie', 'suzie', 0o700), ++ '/tmp/home/suzie/.ssh/authorized_keys': ('suzie', 'suzie', 0o600), ++ } ++ ++ user_bobby = 'bobby' ++ user_suzie = 'suzie' ++ homes = self.create_fake_users( ++ [user_bobby, user_suzie], mock_permissions, m_get_group, ++ m_get_owner, m_get_permissions, m_getpwnam, users ++ ) ++ home_bobby = homes[0] ++ home_suzie = homes[1] ++ m_get_user_groups.side_effect = mock_get_user_groups ++ ++ # /tmp/home/bobby/.ssh/authorized_keys = rsa ++ authorized_keys = self.create_user_authorized_file( ++ home_bobby, 'authorized_keys', 'rsa', keys ++ ) ++ ++ # /tmp/home/suzie/.ssh/authorized_keys = ssh-xmss@openssh.com ++ authorized_keys2 = self.create_user_authorized_file( ++ home_suzie, 'authorized_keys', 'ssh-xmss@openssh.com', keys ++ ) ++ ++ # /tmp/etc/ssh/authorized_keys = ecdsa ++ authorized_keys_global = self.create_global_authorized_file( ++ 'etc/ssh/authorized_keys', 'ecdsa', keys ++ ) ++ ++ # /tmp/sshd_config ++ options = "%s %s %s" % \ ++ (authorized_keys_global, authorized_keys, authorized_keys2) ++ sshd_config = self.create_sshd_config(options) ++ ++ self.execute_and_check( ++ user_bobby, sshd_config, authorized_keys, keys, delete_keys=False ++ ) ++ self.execute_and_check(user_suzie, sshd_config, authorized_keys2, keys) + + # vi: ts=4 expandtab +-- +2.27.0 + diff --git a/SOURCES/ci-ssh-util-allow-cloudinit-to-merge-all-ssh-keys-into-.patch b/SOURCES/ci-ssh-util-allow-cloudinit-to-merge-all-ssh-keys-into-.patch new file mode 100644 index 0000000..bdec823 --- /dev/null +++ b/SOURCES/ci-ssh-util-allow-cloudinit-to-merge-all-ssh-keys-into-.patch @@ -0,0 +1,653 @@ +From aeab67600eb2d5e483812620b56ce5fb031a57d6 Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito +Date: Mon, 12 Jul 2021 21:47:37 +0200 +Subject: [PATCH] ssh-util: allow cloudinit to merge all ssh keys into a custom + user file, defined in AuthorizedKeysFile (#937) + +RH-Author: Emanuele Giuseppe Esposito +RH-MergeRequest: 25: ssh-util: allow cloudinit to merge all ssh keys into a custom user file, defined in AuthorizedKeysFile (#937) +RH-Commit: [1/1] 27bbe94f3b9dd8734865766bd30b06cff83383ab (eesposit/cloud-init) +RH-Bugzilla: 1862967 +RH-Acked-by: Vitaly Kuznetsov +RH-Acked-by: Mohamed Gamal Morsy + +TESTED: By me and QA +BREW: 38030830 + +Conflicts: upstream patch modifies tests/integration_tests/util.py, that is +not present in RHEL. + +commit 9b52405c6f0de5e00d5ee9c1d13540425d8f6bf5 +Author: Emanuele Giuseppe Esposito +Date: Mon Jul 12 20:21:02 2021 +0200 + + ssh-util: allow cloudinit to merge all ssh keys into a custom user file, defined in AuthorizedKeysFile (#937) + + This patch aims to fix LP1911680, by analyzing the files provided + in sshd_config and merge all keys into an user-specific file. Also + introduces additional tests to cover this specific case. + + The file is picked by analyzing the path given in AuthorizedKeysFile. + + If it points inside the current user folder (path is /home/user/*), it + means it is an user-specific file, so we can copy all user-keys there. + If it contains a %u or %h, it means that there will be a specific + authorized_keys file for each user, so we can copy all user-keys there. + If no path points to an user-specific file, for example when only + /etc/ssh/authorized_keys is given, default to ~/.ssh/authorized_keys. + Note that if there are more than a single user-specific file, the last + one will be picked. + + Signed-off-by: Emanuele Giuseppe Esposito + Co-authored-by: James Falcon + + LP: #1911680 + RHBZ:1862967 + +Signed-off-by: Emanuele Giuseppe Esposito +--- + cloudinit/ssh_util.py | 22 +- + .../assets/keys/id_rsa.test1 | 38 +++ + .../assets/keys/id_rsa.test1.pub | 1 + + .../assets/keys/id_rsa.test2 | 38 +++ + .../assets/keys/id_rsa.test2.pub | 1 + + .../assets/keys/id_rsa.test3 | 38 +++ + .../assets/keys/id_rsa.test3.pub | 1 + + .../modules/test_ssh_keysfile.py | 85 ++++++ + tests/unittests/test_sshutil.py | 246 +++++++++++++++++- + 9 files changed, 456 insertions(+), 14 deletions(-) + create mode 100644 tests/integration_tests/assets/keys/id_rsa.test1 + create mode 100644 tests/integration_tests/assets/keys/id_rsa.test1.pub + create mode 100644 tests/integration_tests/assets/keys/id_rsa.test2 + create mode 100644 tests/integration_tests/assets/keys/id_rsa.test2.pub + create mode 100644 tests/integration_tests/assets/keys/id_rsa.test3 + create mode 100644 tests/integration_tests/assets/keys/id_rsa.test3.pub + create mode 100644 tests/integration_tests/modules/test_ssh_keysfile.py + +diff --git a/cloudinit/ssh_util.py b/cloudinit/ssh_util.py +index c08042d6..89057262 100644 +--- a/cloudinit/ssh_util.py ++++ b/cloudinit/ssh_util.py +@@ -252,13 +252,15 @@ def render_authorizedkeysfile_paths(value, homedir, username): + def extract_authorized_keys(username, sshd_cfg_file=DEF_SSHD_CFG): + (ssh_dir, pw_ent) = users_ssh_info(username) + default_authorizedkeys_file = os.path.join(ssh_dir, 'authorized_keys') ++ user_authorizedkeys_file = default_authorizedkeys_file + auth_key_fns = [] + with util.SeLinuxGuard(ssh_dir, recursive=True): + try: + ssh_cfg = parse_ssh_config_map(sshd_cfg_file) ++ key_paths = ssh_cfg.get("authorizedkeysfile", ++ "%h/.ssh/authorized_keys") + auth_key_fns = render_authorizedkeysfile_paths( +- ssh_cfg.get("authorizedkeysfile", "%h/.ssh/authorized_keys"), +- pw_ent.pw_dir, username) ++ key_paths, pw_ent.pw_dir, username) + + except (IOError, OSError): + # Give up and use a default key filename +@@ -267,8 +269,22 @@ def extract_authorized_keys(username, sshd_cfg_file=DEF_SSHD_CFG): + "config from %r, using 'AuthorizedKeysFile' file " + "%r instead", DEF_SSHD_CFG, auth_key_fns[0]) + ++ # check if one of the keys is the user's one ++ for key_path, auth_key_fn in zip(key_paths.split(), auth_key_fns): ++ if any([ ++ '%u' in key_path, ++ '%h' in key_path, ++ auth_key_fn.startswith('{}/'.format(pw_ent.pw_dir)) ++ ]): ++ user_authorizedkeys_file = auth_key_fn ++ ++ if user_authorizedkeys_file != default_authorizedkeys_file: ++ LOG.debug( ++ "AuthorizedKeysFile has an user-specific authorized_keys, " ++ "using %s", user_authorizedkeys_file) ++ + # always store all the keys in the user's private file +- return (default_authorizedkeys_file, parse_authorized_keys(auth_key_fns)) ++ return (user_authorizedkeys_file, parse_authorized_keys(auth_key_fns)) + + + def setup_user_keys(keys, username, options=None): +diff --git a/tests/integration_tests/assets/keys/id_rsa.test1 b/tests/integration_tests/assets/keys/id_rsa.test1 +new file mode 100644 +index 00000000..bd4c822e +--- /dev/null ++++ b/tests/integration_tests/assets/keys/id_rsa.test1 +@@ -0,0 +1,38 @@ ++-----BEGIN OPENSSH PRIVATE KEY----- ++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 ++CHbTWbsFrHR1qFAAAACmphbWVzQG5ld3Q= ++-----END OPENSSH PRIVATE KEY----- +diff --git a/tests/integration_tests/assets/keys/id_rsa.test1.pub b/tests/integration_tests/assets/keys/id_rsa.test1.pub +new file mode 100644 +index 00000000..3d2e26e1 +--- /dev/null ++++ b/tests/integration_tests/assets/keys/id_rsa.test1.pub +@@ -0,0 +1 @@ ++ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC1GUb3ponbdRG8CA79sGy4uX6Wq5zfnRpLBX3z+Ly+W86fkZVwfJ63+tdCDiVnizSjAbeRbJAVjye9OT4b5Q1Tx83RqcDMEOL7gzOx6OQX2xyNMwWZdWsikgKpXdthA2M+/qv65tB2QPTV27d4H23n/OXziC2yGnSEF2h/LPy7X8Dbmzt9VTrsbIjs90q9pL7K8lIidmdvZ71VDN8kL6oCc3uBRlq0EqN2BS0l6JRR4NpWdpLXtvRqhP2IE7SwhxC05MGViUKMW3VMAwtnrgePdJ8ZkHtiPRn4vMUHYhZfXPOyMjrxiRg8o5Iv81iFBT17nmQlDtaeeA0dj/af8Dt00pIUOaAzlz0haB5nt+Omx8hgCgn8IDEUg0P48Qd3WAJLrGgSFpRXKHlYf9eTAq/WfHgCJnYEUIT7vmTJJEtlQn7nLSvcNTmS0xd5RHNUg/um7RHwMK6X4ZT4z/5MfJa0AQ6B5o0np9imjaKkOFuZNi3eSTaUwhqlHEYEWz7fux0= test1@host +diff --git a/tests/integration_tests/assets/keys/id_rsa.test2 b/tests/integration_tests/assets/keys/id_rsa.test2 +new file mode 100644 +index 00000000..5854d901 +--- /dev/null ++++ b/tests/integration_tests/assets/keys/id_rsa.test2 +@@ -0,0 +1,38 @@ ++-----BEGIN OPENSSH PRIVATE KEY----- ++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 ++tZRSm2GEgnDGAvAAAACmphbWVzQG5ld3Q= ++-----END OPENSSH PRIVATE KEY----- +diff --git a/tests/integration_tests/assets/keys/id_rsa.test2.pub b/tests/integration_tests/assets/keys/id_rsa.test2.pub +new file mode 100644 +index 00000000..f3831a57 +--- /dev/null ++++ b/tests/integration_tests/assets/keys/id_rsa.test2.pub +@@ -0,0 +1 @@ ++ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC8rnQPY9Y5ziKTIdVElLq0OGrOMvlwqKK8gPinVfwFgJXDzdcAQY4uci1TJVcc0AOWHp+lWrU1joDYlW3KCg8XpkXHymHshYyaeEN2fEsvIaxuF3UzW2JckP9H7dacYdEng8qtBq8wuCodGuJ5XdBVV+MVJ6jqNf/hOu4/pma8hMxlYmtdoamHEn+k/KQR2Q6LwCYpT0U2+KiPI9Lac22P0H/rfkh2Ba+t3tV/lhyzD0xHnktZKJ3BrznpEfrZiXoukQZMebWUmIyTW7zwk9Kq+iGFpSQsqQWlxDBwHSbvpbUo7KWUmyZfxs1euVmwj5aKJgjteXm9CbbXsMS415q+08C0MzG7weZODZQnnk1rF6Ft97aXHaTmRgYbDeLU7U5U3alnb84HvUu5xh3/mrE9rPTfCzBwY4lgY+Q1zjS90RL9Jxzti3weydm7OqTI6DOfQeJQLOhhRgthOkt/7IabDFLIARjTnpo5+QKxugoc4qI6aUnE1plkVCRfV696ngM= test2@host +diff --git a/tests/integration_tests/assets/keys/id_rsa.test3 b/tests/integration_tests/assets/keys/id_rsa.test3 +new file mode 100644 +index 00000000..2596c762 +--- /dev/null ++++ b/tests/integration_tests/assets/keys/id_rsa.test3 +@@ -0,0 +1,38 @@ ++-----BEGIN OPENSSH PRIVATE KEY----- ++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 ++eauhOY52PS/BcAAAAKamFtZXNAbmV3dAE= ++-----END OPENSSH PRIVATE KEY----- +diff --git a/tests/integration_tests/assets/keys/id_rsa.test3.pub b/tests/integration_tests/assets/keys/id_rsa.test3.pub +new file mode 100644 +index 00000000..057db632 +--- /dev/null ++++ b/tests/integration_tests/assets/keys/id_rsa.test3.pub +@@ -0,0 +1 @@ ++ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCk8bgx2RhAoPnv+qt4WuH0ZELbbLrqpk5ZlZGMLzzuutu8HM7r2FXImlMheV4kS+laIdhyMxHkbo00Wyc21nh/EOqZJzi9wWPncmwT9c2osqrqmxdp0Jvm+Q2kZvED5nlsmXMn3r0+AkggRuBzzqx6LiSBHYy49aq1ltjkaSk0JOpa4th4Ur7XigWu4DafJYf0w4hEMcr93n3umwfL4tMy3KY7tk+E7nvVpsrDqO+/CJJ4PFUT2RXsoBAi4z39LeB+2+BBCuifcYvfimCYhpt1IZ1t8eoYOiivImGVuq4lkQsE3bfoqBE1MICNW9TOONCvV5E/fGywbBU7oQIEYJAuX9piiACC3CfCP2Jeq1+IsrcTCWCOq5Tj4lDQJW0uEbeAhkH6qgxOFyjSmYdOuO19ALCvO2wEEW6dJrb8Ky25XWZDk7lmtnoAKj3I9EE2Sezr4KIp20y0K3OLeTZEPP2OliY9xqTTcuvsiR0LVOnr6MD7//I8dI7WjgKpbz0hDM0= test3@host +diff --git a/tests/integration_tests/modules/test_ssh_keysfile.py b/tests/integration_tests/modules/test_ssh_keysfile.py +new file mode 100644 +index 00000000..f82d7649 +--- /dev/null ++++ b/tests/integration_tests/modules/test_ssh_keysfile.py +@@ -0,0 +1,85 @@ ++import paramiko ++import pytest ++from io import StringIO ++from paramiko.ssh_exception import SSHException ++ ++from tests.integration_tests.instances import IntegrationInstance ++from tests.integration_tests.util import get_test_rsa_keypair ++ ++TEST_USER1_KEYS = get_test_rsa_keypair('test1') ++TEST_USER2_KEYS = get_test_rsa_keypair('test2') ++TEST_DEFAULT_KEYS = get_test_rsa_keypair('test3') ++ ++USERDATA = """\ ++#cloud-config ++bootcmd: ++ - sed -i 's;#AuthorizedKeysFile.*;AuthorizedKeysFile /etc/ssh/authorized_keys %h/.ssh/authorized_keys2;' /etc/ssh/sshd_config ++ssh_authorized_keys: ++ - {default} ++users: ++- default ++- name: test_user1 ++ ssh_authorized_keys: ++ - {user1} ++- name: test_user2 ++ ssh_authorized_keys: ++ - {user2} ++""".format( # noqa: E501 ++ default=TEST_DEFAULT_KEYS.public_key, ++ user1=TEST_USER1_KEYS.public_key, ++ user2=TEST_USER2_KEYS.public_key, ++) ++ ++ ++@pytest.mark.ubuntu ++@pytest.mark.user_data(USERDATA) ++def test_authorized_keys(client: IntegrationInstance): ++ expected_keys = [ ++ ('test_user1', '/home/test_user1/.ssh/authorized_keys2', ++ TEST_USER1_KEYS), ++ ('test_user2', '/home/test_user2/.ssh/authorized_keys2', ++ TEST_USER2_KEYS), ++ ('ubuntu', '/home/ubuntu/.ssh/authorized_keys2', ++ TEST_DEFAULT_KEYS), ++ ('root', '/root/.ssh/authorized_keys2', TEST_DEFAULT_KEYS), ++ ] ++ ++ for user, filename, keys in expected_keys: ++ contents = client.read_from_file(filename) ++ if user in ['ubuntu', 'root']: ++ # Our personal public key gets added by pycloudlib ++ lines = contents.split('\n') ++ assert len(lines) == 2 ++ assert keys.public_key.strip() in contents ++ else: ++ assert contents.strip() == keys.public_key.strip() ++ ++ # Ensure we can actually connect ++ ssh = paramiko.SSHClient() ++ ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ++ paramiko_key = paramiko.RSAKey.from_private_key(StringIO( ++ keys.private_key)) ++ ++ # Will fail with AuthenticationException if ++ # we cannot connect ++ ssh.connect( ++ client.instance.ip, ++ username=user, ++ pkey=paramiko_key, ++ look_for_keys=False, ++ allow_agent=False, ++ ) ++ ++ # Ensure other uses can't connect using our key ++ other_users = [u[0] for u in expected_keys if u[2] != keys] ++ for other_user in other_users: ++ with pytest.raises(SSHException): ++ print('trying to connect as {} with key from {}'.format( ++ other_user, user)) ++ ssh.connect( ++ client.instance.ip, ++ username=other_user, ++ pkey=paramiko_key, ++ look_for_keys=False, ++ allow_agent=False, ++ ) +diff --git a/tests/unittests/test_sshutil.py b/tests/unittests/test_sshutil.py +index fd1d1bac..bcb8044f 100644 +--- a/tests/unittests/test_sshutil.py ++++ b/tests/unittests/test_sshutil.py +@@ -570,20 +570,33 @@ class TestBasicAuthorizedKeyParse(test_helpers.CiTestCase): + ssh_util.render_authorizedkeysfile_paths( + "%h/.keys", "/homedirs/bobby", "bobby")) + ++ def test_all(self): ++ self.assertEqual( ++ ["/homedirs/bobby/.keys", "/homedirs/bobby/.secret/keys", ++ "/keys/path1", "/opt/bobby/keys"], ++ ssh_util.render_authorizedkeysfile_paths( ++ "%h/.keys .secret/keys /keys/path1 /opt/%u/keys", ++ "/homedirs/bobby", "bobby")) ++ + + class TestMultipleSshAuthorizedKeysFile(test_helpers.CiTestCase): + + @patch("cloudinit.ssh_util.pwd.getpwnam") + def test_multiple_authorizedkeys_file_order1(self, m_getpwnam): +- fpw = FakePwEnt(pw_name='bobby', pw_dir='/home2/bobby') ++ fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby') + m_getpwnam.return_value = fpw +- authorized_keys = self.tmp_path('authorized_keys') ++ user_ssh_folder = "%s/.ssh" % fpw.pw_dir ++ ++ # /tmp/home2/bobby/.ssh/authorized_keys = rsa ++ authorized_keys = self.tmp_path('authorized_keys', dir=user_ssh_folder) + util.write_file(authorized_keys, VALID_CONTENT['rsa']) + +- user_keys = self.tmp_path('user_keys') ++ # /tmp/home2/bobby/.ssh/user_keys = dsa ++ user_keys = self.tmp_path('user_keys', dir=user_ssh_folder) + util.write_file(user_keys, VALID_CONTENT['dsa']) + +- sshd_config = self.tmp_path('sshd_config') ++ # /tmp/sshd_config ++ sshd_config = self.tmp_path('sshd_config', dir="/tmp") + util.write_file( + sshd_config, + "AuthorizedKeysFile %s %s" % (authorized_keys, user_keys) +@@ -593,33 +606,244 @@ class TestMultipleSshAuthorizedKeysFile(test_helpers.CiTestCase): + fpw.pw_name, sshd_config) + content = ssh_util.update_authorized_keys(auth_key_entries, []) + +- self.assertEqual("%s/.ssh/authorized_keys" % fpw.pw_dir, auth_key_fn) ++ self.assertEqual(user_keys, auth_key_fn) + self.assertTrue(VALID_CONTENT['rsa'] in content) + self.assertTrue(VALID_CONTENT['dsa'] in content) + + @patch("cloudinit.ssh_util.pwd.getpwnam") + def test_multiple_authorizedkeys_file_order2(self, m_getpwnam): +- fpw = FakePwEnt(pw_name='suzie', pw_dir='/home/suzie') ++ fpw = FakePwEnt(pw_name='suzie', pw_dir='/tmp/home/suzie') + m_getpwnam.return_value = fpw +- authorized_keys = self.tmp_path('authorized_keys') ++ user_ssh_folder = "%s/.ssh" % fpw.pw_dir ++ ++ # /tmp/home/suzie/.ssh/authorized_keys = rsa ++ authorized_keys = self.tmp_path('authorized_keys', dir=user_ssh_folder) + util.write_file(authorized_keys, VALID_CONTENT['rsa']) + +- user_keys = self.tmp_path('user_keys') ++ # /tmp/home/suzie/.ssh/user_keys = dsa ++ user_keys = self.tmp_path('user_keys', dir=user_ssh_folder) + util.write_file(user_keys, VALID_CONTENT['dsa']) + +- sshd_config = self.tmp_path('sshd_config') ++ # /tmp/sshd_config ++ sshd_config = self.tmp_path('sshd_config', dir="/tmp") + util.write_file( + sshd_config, +- "AuthorizedKeysFile %s %s" % (authorized_keys, user_keys) ++ "AuthorizedKeysFile %s %s" % (user_keys, authorized_keys) + ) + + (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( +- fpw.pw_name, sshd_config ++ fpw.pw_name, sshd_config) ++ content = ssh_util.update_authorized_keys(auth_key_entries, []) ++ ++ self.assertEqual(authorized_keys, auth_key_fn) ++ self.assertTrue(VALID_CONTENT['rsa'] in content) ++ self.assertTrue(VALID_CONTENT['dsa'] in content) ++ ++ @patch("cloudinit.ssh_util.pwd.getpwnam") ++ def test_multiple_authorizedkeys_file_local_global(self, m_getpwnam): ++ fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby') ++ m_getpwnam.return_value = fpw ++ user_ssh_folder = "%s/.ssh" % fpw.pw_dir ++ ++ # /tmp/home2/bobby/.ssh/authorized_keys = rsa ++ authorized_keys = self.tmp_path('authorized_keys', dir=user_ssh_folder) ++ util.write_file(authorized_keys, VALID_CONTENT['rsa']) ++ ++ # /tmp/home2/bobby/.ssh/user_keys = dsa ++ user_keys = self.tmp_path('user_keys', dir=user_ssh_folder) ++ util.write_file(user_keys, VALID_CONTENT['dsa']) ++ ++ # /tmp/etc/ssh/authorized_keys = ecdsa ++ authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys', ++ dir="/tmp") ++ util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa']) ++ ++ # /tmp/sshd_config ++ sshd_config = self.tmp_path('sshd_config', dir="/tmp") ++ util.write_file( ++ sshd_config, ++ "AuthorizedKeysFile %s %s %s" % (authorized_keys_global, ++ user_keys, authorized_keys) ++ ) ++ ++ (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( ++ fpw.pw_name, sshd_config) ++ content = ssh_util.update_authorized_keys(auth_key_entries, []) ++ ++ self.assertEqual(authorized_keys, auth_key_fn) ++ self.assertTrue(VALID_CONTENT['rsa'] in content) ++ self.assertTrue(VALID_CONTENT['ecdsa'] in content) ++ self.assertTrue(VALID_CONTENT['dsa'] in content) ++ ++ @patch("cloudinit.ssh_util.pwd.getpwnam") ++ def test_multiple_authorizedkeys_file_local_global2(self, m_getpwnam): ++ fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby') ++ m_getpwnam.return_value = fpw ++ user_ssh_folder = "%s/.ssh" % fpw.pw_dir ++ ++ # /tmp/home2/bobby/.ssh/authorized_keys2 = rsa ++ authorized_keys = self.tmp_path('authorized_keys2', ++ dir=user_ssh_folder) ++ util.write_file(authorized_keys, VALID_CONTENT['rsa']) ++ ++ # /tmp/home2/bobby/.ssh/user_keys3 = dsa ++ user_keys = self.tmp_path('user_keys3', dir=user_ssh_folder) ++ util.write_file(user_keys, VALID_CONTENT['dsa']) ++ ++ # /tmp/etc/ssh/authorized_keys = ecdsa ++ authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys', ++ dir="/tmp") ++ util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa']) ++ ++ # /tmp/sshd_config ++ sshd_config = self.tmp_path('sshd_config', dir="/tmp") ++ util.write_file( ++ sshd_config, ++ "AuthorizedKeysFile %s %s %s" % (authorized_keys_global, ++ authorized_keys, user_keys) ++ ) ++ ++ (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( ++ fpw.pw_name, sshd_config) ++ content = ssh_util.update_authorized_keys(auth_key_entries, []) ++ ++ self.assertEqual(user_keys, auth_key_fn) ++ self.assertTrue(VALID_CONTENT['rsa'] in content) ++ self.assertTrue(VALID_CONTENT['ecdsa'] in content) ++ self.assertTrue(VALID_CONTENT['dsa'] in content) ++ ++ @patch("cloudinit.ssh_util.pwd.getpwnam") ++ def test_multiple_authorizedkeys_file_global(self, m_getpwnam): ++ fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby') ++ m_getpwnam.return_value = fpw ++ ++ # /tmp/etc/ssh/authorized_keys = rsa ++ authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys', ++ dir="/tmp") ++ util.write_file(authorized_keys_global, VALID_CONTENT['rsa']) ++ ++ # /tmp/sshd_config ++ sshd_config = self.tmp_path('sshd_config') ++ util.write_file( ++ sshd_config, ++ "AuthorizedKeysFile %s" % (authorized_keys_global) + ) ++ ++ (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( ++ fpw.pw_name, sshd_config) + content = ssh_util.update_authorized_keys(auth_key_entries, []) + + self.assertEqual("%s/.ssh/authorized_keys" % fpw.pw_dir, auth_key_fn) + self.assertTrue(VALID_CONTENT['rsa'] in content) ++ ++ @patch("cloudinit.ssh_util.pwd.getpwnam") ++ def test_multiple_authorizedkeys_file_multiuser(self, m_getpwnam): ++ fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby') ++ m_getpwnam.return_value = fpw ++ user_ssh_folder = "%s/.ssh" % fpw.pw_dir ++ # /tmp/home2/bobby/.ssh/authorized_keys2 = rsa ++ authorized_keys = self.tmp_path('authorized_keys2', ++ dir=user_ssh_folder) ++ util.write_file(authorized_keys, VALID_CONTENT['rsa']) ++ # /tmp/home2/bobby/.ssh/user_keys3 = dsa ++ user_keys = self.tmp_path('user_keys3', dir=user_ssh_folder) ++ util.write_file(user_keys, VALID_CONTENT['dsa']) ++ ++ fpw2 = FakePwEnt(pw_name='suzie', pw_dir='/tmp/home/suzie') ++ user_ssh_folder = "%s/.ssh" % fpw2.pw_dir ++ # /tmp/home/suzie/.ssh/authorized_keys2 = ssh-xmss@openssh.com ++ authorized_keys2 = self.tmp_path('authorized_keys2', ++ dir=user_ssh_folder) ++ util.write_file(authorized_keys2, ++ VALID_CONTENT['ssh-xmss@openssh.com']) ++ ++ # /tmp/etc/ssh/authorized_keys = ecdsa ++ authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys2', ++ dir="/tmp") ++ util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa']) ++ ++ # /tmp/sshd_config ++ sshd_config = self.tmp_path('sshd_config', dir="/tmp") ++ util.write_file( ++ sshd_config, ++ "AuthorizedKeysFile %s %%h/.ssh/authorized_keys2 %s" % ++ (authorized_keys_global, user_keys) ++ ) ++ ++ # process first user ++ (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( ++ fpw.pw_name, sshd_config) ++ content = ssh_util.update_authorized_keys(auth_key_entries, []) ++ ++ self.assertEqual(user_keys, auth_key_fn) ++ self.assertTrue(VALID_CONTENT['rsa'] in content) ++ self.assertTrue(VALID_CONTENT['ecdsa'] in content) ++ self.assertTrue(VALID_CONTENT['dsa'] in content) ++ self.assertFalse(VALID_CONTENT['ssh-xmss@openssh.com'] in content) ++ ++ m_getpwnam.return_value = fpw2 ++ # process second user ++ (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( ++ fpw2.pw_name, sshd_config) ++ content = ssh_util.update_authorized_keys(auth_key_entries, []) ++ ++ self.assertEqual(authorized_keys2, auth_key_fn) ++ self.assertTrue(VALID_CONTENT['ssh-xmss@openssh.com'] in content) ++ self.assertTrue(VALID_CONTENT['ecdsa'] in content) ++ self.assertTrue(VALID_CONTENT['dsa'] in content) ++ self.assertFalse(VALID_CONTENT['rsa'] in content) ++ ++ @patch("cloudinit.ssh_util.pwd.getpwnam") ++ def test_multiple_authorizedkeys_file_multiuser2(self, m_getpwnam): ++ fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home/bobby') ++ m_getpwnam.return_value = fpw ++ user_ssh_folder = "%s/.ssh" % fpw.pw_dir ++ # /tmp/home/bobby/.ssh/authorized_keys2 = rsa ++ authorized_keys = self.tmp_path('authorized_keys2', ++ dir=user_ssh_folder) ++ util.write_file(authorized_keys, VALID_CONTENT['rsa']) ++ # /tmp/home/bobby/.ssh/user_keys3 = dsa ++ user_keys = self.tmp_path('user_keys3', dir=user_ssh_folder) ++ util.write_file(user_keys, VALID_CONTENT['dsa']) ++ ++ fpw2 = FakePwEnt(pw_name='badguy', pw_dir='/tmp/home/badguy') ++ user_ssh_folder = "%s/.ssh" % fpw2.pw_dir ++ # /tmp/home/badguy/home/bobby = "" ++ authorized_keys2 = self.tmp_path('home/bobby', dir="/tmp/home/badguy") ++ ++ # /tmp/etc/ssh/authorized_keys = ecdsa ++ authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys2', ++ dir="/tmp") ++ util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa']) ++ ++ # /tmp/sshd_config ++ sshd_config = self.tmp_path('sshd_config', dir="/tmp") ++ util.write_file( ++ sshd_config, ++ "AuthorizedKeysFile %s %%h/.ssh/authorized_keys2 %s %s" % ++ (authorized_keys_global, user_keys, authorized_keys2) ++ ) ++ ++ # process first user ++ (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( ++ fpw.pw_name, sshd_config) ++ content = ssh_util.update_authorized_keys(auth_key_entries, []) ++ ++ self.assertEqual(user_keys, auth_key_fn) ++ self.assertTrue(VALID_CONTENT['rsa'] in content) ++ self.assertTrue(VALID_CONTENT['ecdsa'] in content) ++ self.assertTrue(VALID_CONTENT['dsa'] in content) ++ ++ m_getpwnam.return_value = fpw2 ++ # process second user ++ (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( ++ fpw2.pw_name, sshd_config) ++ content = ssh_util.update_authorized_keys(auth_key_entries, []) ++ ++ # badguy should not take the key from the other user! ++ self.assertEqual(authorized_keys2, auth_key_fn) ++ self.assertTrue(VALID_CONTENT['ecdsa'] in content) + self.assertTrue(VALID_CONTENT['dsa'] in content) ++ self.assertFalse(VALID_CONTENT['rsa'] in content) + + # vi: ts=4 expandtab +-- +2.27.0 + diff --git a/SPECS/cloud-init.spec b/SPECS/cloud-init.spec index be118d6..83955a2 100644 --- a/SPECS/cloud-init.spec +++ b/SPECS/cloud-init.spec @@ -6,7 +6,7 @@ Name: cloud-init Version: 21.1 -Release: 3%{?dist} +Release: 6%{?dist} Summary: Cloud instance init scripts Group: System Environment/Base @@ -28,6 +28,10 @@ Patch0009: 0009-Fix-requiring-device-number-on-EC2-derivatives-836.patch Patch10: ci-rhel-cloud.cfg-remove-ssh_genkeytypes-in-settings.py.patch # For bz#1945891 - CVE-2021-3429 cloud-init: randomly generated passwords logged in clear-text to world-readable file [rhel-8] Patch11: ci-write-passwords-only-to-serial-console-lock-down-clo.patch +# For bz#1862967 - [cloud-init]Customize ssh AuthorizedKeysFile causes login failure +Patch12: ci-ssh-util-allow-cloudinit-to-merge-all-ssh-keys-into-.patch +# For bz#1862967 - [cloud-init]Customize ssh AuthorizedKeysFile causes login failure +Patch13: ci-Stop-copying-ssh-system-keys-and-check-folder-permis.patch BuildArch: noarch @@ -75,6 +79,7 @@ Requires: python3-six Requires: shadow-utils Requires: util-linux Requires: xfsprogs +Requires: dhcp-client %{?systemd_requires} @@ -218,6 +223,21 @@ fi %config(noreplace) %{_sysconfdir}/rsyslog.d/21-cloudinit.conf %changelog +* Wed Aug 11 2021 Miroslav Rezanina - 21.1-6 +- ci-Stop-copying-ssh-system-keys-and-check-folder-permis.patch [bz#1862967] +- Resolves: bz#1862967 + ([cloud-init]Customize ssh AuthorizedKeysFile causes login failure) + +* Fri Aug 06 2021 Miroslav Rezanina - 21.1-5 +- ci-Add-dhcp-client-as-a-dependency.patch [bz#1977385] +- Resolves: bz#1977385 + ([Azure][RHEL-8] cloud-init must require dhcp-client on Azure) + +* Mon Jul 19 2021 Miroslav Rezanina - 21.1-4 +- ci-ssh-util-allow-cloudinit-to-merge-all-ssh-keys-into-.patch [bz#1862967] +- Resolves: bz#1862967 + ([cloud-init]Customize ssh AuthorizedKeysFile causes login failure) + * Mon Jul 12 2021 Miroslav Rezanina - 21.1-3 - ci-write-passwords-only-to-serial-console-lock-down-clo.patch [bz#1945891] - Resolves: bz#1945891