From dda803b38d7125673af4c61e7ad53def29408991 Mon Sep 17 00:00:00 2001 From: Redouane Kachach Date: Fri, 29 Apr 2022 08:26:27 +0200 Subject: [PATCH] mgr/cephadm: Adding an early ssh connectivity check during bootsrap Fixes: https://tracker.ceph.com/issues/55493 Fixes: https://tracker.ceph.com/issues/51665 Signed-off-by: Redouane Kachach --- src/cephadm/cephadm | 175 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 144 insertions(+), 31 deletions(-) diff --git a/src/cephadm/cephadm b/src/cephadm/cephadm index 0de6dc4e523b3..26ff721b863c6 100755 --- a/src/cephadm/cephadm +++ b/src/cephadm/cephadm @@ -26,7 +26,6 @@ import errno import struct import ssl from enum import Enum - from typing import Dict, List, Tuple, Optional, Union, Any, NoReturn, Callable, IO, Sequence, TypeVar, cast, Set, Iterable import re @@ -5036,44 +5035,16 @@ def prepare_ssh( } cli(['cephadm', 'set-priv-key', '-i', '/tmp/cephadm-ssh-key'], extra_mounts=mounts) cli(['cephadm', 'set-pub-key', '-i', '/tmp/cephadm-ssh-key.pub'], extra_mounts=mounts) + ssh_pub = cli(['cephadm', 'get-pub-key']) else: logger.info('Generating ssh key...') cli(['cephadm', 'generate-key']) ssh_pub = cli(['cephadm', 'get-pub-key']) - with open(ctx.output_pub_ssh_key, 'w') as f: f.write(ssh_pub) logger.info('Wrote public SSH key to %s' % ctx.output_pub_ssh_key) - logger.info('Adding key to %s@localhost authorized_keys...' % ctx.ssh_user) - try: - s_pwd = pwd.getpwnam(ctx.ssh_user) - except KeyError: - raise Error('Cannot find uid/gid for ssh-user: %s' % (ctx.ssh_user)) - ssh_uid = s_pwd.pw_uid - ssh_gid = s_pwd.pw_gid - ssh_dir = os.path.join(s_pwd.pw_dir, '.ssh') - - if not os.path.exists(ssh_dir): - makedirs(ssh_dir, ssh_uid, ssh_gid, 0o700) - - auth_keys_file = '%s/authorized_keys' % ssh_dir - add_newline = False - - if os.path.exists(auth_keys_file): - with open(auth_keys_file, 'r') as f: - f.seek(0, os.SEEK_END) - if f.tell() > 0: - f.seek(f.tell() - 1, os.SEEK_SET) # go to last char - if f.read() != '\n': - add_newline = True - - with open(auth_keys_file, 'a') as f: - os.fchown(f.fileno(), ssh_uid, ssh_gid) # just in case we created it - os.fchmod(f.fileno(), 0o600) # just in case we created it - if add_newline: - f.write('\n') - f.write(ssh_pub.strip() + '\n') + authorize_ssh_key(ssh_pub, ctx.ssh_user) host = get_hostname() logger.info('Adding host %s...' % host) @@ -5377,6 +5348,9 @@ def command_bootstrap(ctx): if not ctx.output_pub_ssh_key: ctx.output_pub_ssh_key = os.path.join(ctx.output_dir, CEPH_PUBKEY) + if bool(ctx.ssh_private_key) is not bool(ctx.ssh_public_key): + raise Error('--ssh-private-key and --ssh-public-key must be provided together or not at all.') + if ctx.fsid: data_dir_base = os.path.join(ctx.data_dir, ctx.fsid) if os.path.exists(data_dir_base): @@ -5403,6 +5377,9 @@ def command_bootstrap(ctx): (user_conf, _) = get_config_and_keyring(ctx) + if ctx.ssh_user != 'root': + check_ssh_connectivity(ctx) + if not ctx.skip_prepare_host: command_prepare_host(ctx) else: @@ -7080,6 +7057,142 @@ def command_check_host(ctx: CephadmContext) -> None: ################################## +def get_ssh_vars(ssh_user: str) -> Tuple[int, int, str]: + try: + s_pwd = pwd.getpwnam(ssh_user) + except KeyError: + raise Error('Cannot find uid/gid for ssh-user: %s' % (ssh_user)) + + ssh_uid = s_pwd.pw_uid + ssh_gid = s_pwd.pw_gid + ssh_dir = os.path.join(s_pwd.pw_dir, '.ssh') + return ssh_uid, ssh_gid, ssh_dir + + +def authorize_ssh_key(ssh_pub_key: str, ssh_user: str) -> bool: + """Authorize the public key for the provided ssh user""" + + def key_in_file(path: str, key: str) -> bool: + if not os.path.exists(path): + return False + with open(path) as f: + lines = f.readlines() + for line in lines: + if line.strip() == key.strip(): + return True + return False + + logger.info(f'Adding key to {ssh_user}@localhost authorized_keys...') + if ssh_pub_key is None or ssh_pub_key.isspace(): + raise Error('Trying to authorize an empty ssh key') + + ssh_pub_key = ssh_pub_key.strip() + ssh_uid, ssh_gid, ssh_dir = get_ssh_vars(ssh_user) + if not os.path.exists(ssh_dir): + makedirs(ssh_dir, ssh_uid, ssh_gid, 0o700) + + auth_keys_file = '%s/authorized_keys' % ssh_dir + if key_in_file(auth_keys_file, ssh_pub_key): + logger.info(f'key already in {ssh_user}@localhost authorized_keys...') + return False + + add_newline = False + if os.path.exists(auth_keys_file): + with open(auth_keys_file, 'r') as f: + f.seek(0, os.SEEK_END) + if f.tell() > 0: + f.seek(f.tell() - 1, os.SEEK_SET) # go to last char + if f.read() != '\n': + add_newline = True + + with open(auth_keys_file, 'a') as f: + os.fchown(f.fileno(), ssh_uid, ssh_gid) # just in case we created it + os.fchmod(f.fileno(), 0o600) # just in case we created it + if add_newline: + f.write('\n') + f.write(ssh_pub_key + '\n') + + return True + + +def revoke_ssh_key(key: str, ssh_user: str) -> None: + """Revoke the public key authorization for the ssh user""" + ssh_uid, ssh_gid, ssh_dir = get_ssh_vars(ssh_user) + auth_keys_file = '%s/authorized_keys' % ssh_dir + deleted = False + if os.path.exists(auth_keys_file): + with open(auth_keys_file, 'r') as f: + lines = f.readlines() + _, filename = tempfile.mkstemp() + with open(filename, 'w') as f: + os.fchown(f.fileno(), ssh_uid, ssh_gid) + os.fchmod(f.fileno(), 0o600) # secure access to the keys file + for line in lines: + if line.strip() == key.strip(): + deleted = True + else: + f.write(line) + + if deleted: + shutil.move(filename, auth_keys_file) + else: + logger.warning('Cannot find the ssh key to be deleted') + + +def check_ssh_connectivity(ctx: CephadmContext) -> None: + + def cmd_is_available(cmd: str) -> bool: + if shutil.which(cmd) is None: + logger.warning(f'Command not found: {cmd}') + return False + return True + + if not cmd_is_available('ssh') or not cmd_is_available('ssh-keygen'): + logger.warning('Cannot check ssh connectivity. Skipping...') + return + + logger.info('Verifying ssh connectivity ...') + if ctx.ssh_private_key and ctx.ssh_public_key: + # let's use the keys provided by the user + ssh_priv_key_path = pathify(ctx.ssh_private_key.name) + ssh_pub_key_path = pathify(ctx.ssh_public_key.name) + else: + # no custom keys, let's generate some random keys just for this check + ssh_priv_key_path = f'/tmp/ssh_key_{uuid.uuid1()}' + ssh_pub_key_path = f'{ssh_priv_key_path}.pub' + ssh_key_gen_cmd = ['ssh-keygen', '-q', '-t', 'rsa', '-N', '', '-C', '', '-f', ssh_priv_key_path] + _, _, code = call(ctx, ssh_key_gen_cmd) + if code != 0: + logger.warning('Cannot generate keys to check ssh connectivity.') + return + + with open(ssh_pub_key_path, 'r') as f: + key = f.read().strip() + new_key = authorize_ssh_key(key, ctx.ssh_user) + ssh_cfg_file_arg = ['-F', pathify(ctx.ssh_config.name)] if ctx.ssh_config else [] + _, _, code = call(ctx, ['ssh', '-o StrictHostKeyChecking=no', + *ssh_cfg_file_arg, '-i', ssh_priv_key_path, + '-o PasswordAuthentication=no', + f'{ctx.ssh_user}@{get_hostname()}', + 'sudo echo']) + + # we only remove the key if it's a new one. In case the user has provided + # some already existing key then we don't alter authorized_keys file + if new_key: + revoke_ssh_key(key, ctx.ssh_user) + + pub_key_msg = '- The public key file configured by --ssh-public-key is valid\n' if ctx.ssh_public_key else '' + prv_key_msg = '- The private key file configured by --ssh-private-key is valid\n' if ctx.ssh_private_key else '' + ssh_cfg_msg = '- The ssh configuration file configured by --ssh-config is valid\n' if ctx.ssh_config else '' + err_msg = f""" +** Please verify your user's ssh configuration and make sure: +- User {ctx.ssh_user} must have passwordless sudo access +{pub_key_msg}{prv_key_msg}{ssh_cfg_msg} +""" + if code != 0: + raise Error(err_msg) + + def command_prepare_host(ctx: CephadmContext) -> None: logger.info('Verifying podman|docker is present...') pkg = None -- 2.39.5