From: Adam King Date: Sat, 3 Jun 2023 17:31:58 +0000 (-0400) Subject: mgr/cephadm: add support for CA signed SSH keys setups X-Git-Tag: v19.0.0~675^2~5 X-Git-Url: http://git.apps.os.sepia.ceph.com/?a=commitdiff_plain;h=50f74d6063b820230f57608a1f800dc2507a3e1f;p=ceph.git mgr/cephadm: add support for CA signed SSH keys setups Signed-off-by: Adam King --- diff --git a/src/cephadm/cephadm.py b/src/cephadm/cephadm.py index e8d21b92ce7a2..afa13aa9d1ca9 100755 --- a/src/cephadm/cephadm.py +++ b/src/cephadm/cephadm.py @@ -6013,6 +6013,15 @@ 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']) + authorize_ssh_key(ssh_pub, ctx.ssh_user) + elif ctx.ssh_private_key and ctx.ssh_signed_cert: + logger.info('Using provided ssh private key and signed cert ...') + mounts = { + pathify(ctx.ssh_private_key.name): '/tmp/cephadm-ssh-key:z', + pathify(ctx.ssh_signed_cert.name): '/tmp/cephadm-ssh-key-cert.pub:z' + } + cli(['cephadm', 'set-priv-key', '-i', '/tmp/cephadm-ssh-key'], extra_mounts=mounts) + cli(['cephadm', 'set-signed-cert', '-i', '/tmp/cephadm-ssh-key-cert.pub'], extra_mounts=mounts) else: logger.info('Generating ssh key...') cli(['cephadm', 'generate-key']) @@ -6020,8 +6029,7 @@ def prepare_ssh( 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) - - authorize_ssh_key(ssh_pub, ctx.ssh_user) + authorize_ssh_key(ssh_pub, ctx.ssh_user) host = get_hostname() logger.info('Adding host %s...' % host) @@ -6424,8 +6432,19 @@ 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 ( + (bool(ctx.ssh_private_key) is not bool(ctx.ssh_public_key)) + and (bool(ctx.ssh_private_key) is not bool(ctx.ssh_signed_cert)) + ): + raise Error('--ssh-private-key must be passed with either --ssh-public-key in the case of standard pubkey ' + 'authentication or with --ssh-signed-cert in the case of CA signed signed keys or not provided at all.') + + if (bool(ctx.ssh_public_key) and bool(ctx.ssh_signed_cert)): + raise Error('--ssh-public-key and --ssh-signed-cert are mututally exclusive. --ssh-public-key is intended ' + 'for standard pubkey encryption where the public key is set as an authorized key on cluster hosts. ' + '--ssh-signed-cert is intended for the CA signed keys use case where cluster hosts are configured to trust ' + 'a CA pub key and authentication during SSH is done by authenticating the signed cert, requiring no ' + 'public key to be installed on the cluster hosts.') if ctx.fsid: data_dir_base = os.path.join(ctx.data_dir, ctx.fsid) @@ -6620,7 +6639,10 @@ def command_bootstrap(ctx): with open(ctx.apply_spec) as f: host_dicts = _extract_host_info_from_applied_spec(f) for h in host_dicts: - _distribute_ssh_keys(ctx, h, hostname) + if ctx.ssh_signed_cert: + logger.info('Key distribution is not supported for signed CA key setups. Skipping ...') + else: + _distribute_ssh_keys(ctx, h, hostname) mounts = {} mounts[pathify(ctx.apply_spec)] = '/tmp/spec.yml:ro' @@ -8454,11 +8476,17 @@ def check_ssh_connectivity(ctx: CephadmContext) -> None: logger.warning('Cannot check ssh connectivity. Skipping...') return - logger.info('Verifying ssh connectivity ...') + ssh_priv_key_path = '' + ssh_pub_key_path = '' + ssh_signed_cert_path = '' 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) + elif ctx.ssh_private_key and ctx.ssh_signed_cert: + # CA signed keys use case + ssh_priv_key_path = pathify(ctx.ssh_private_key.name) + ssh_signed_cert_path = pathify(ctx.ssh_signed_cert.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()}' @@ -8469,31 +8497,35 @@ def check_ssh_connectivity(ctx: CephadmContext) -> None: 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""" + if ssh_signed_cert_path: + logger.info('Verification for CA signed keys authentication not implemented. Skipping ...') + elif ssh_pub_key_path: + logger.info('Verifying ssh connectivity using standard pubkey authentication ...') + 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) + if code != 0: + raise Error(err_msg) def command_prepare_host(ctx: CephadmContext) -> None: @@ -10536,6 +10568,10 @@ def _get_parser(): '--ssh-public-key', type=argparse.FileType('r'), help='SSH public key') + parser_bootstrap.add_argument( + '--ssh-signed-cert', + type=argparse.FileType('r'), + help='Signed cert for setups using CA signed SSH keys') parser_bootstrap.add_argument( '--ssh-user', default='root', diff --git a/src/pybind/mgr/cephadm/module.py b/src/pybind/mgr/cephadm/module.py index 6d95ff476604b..8f359af11b6c7 100644 --- a/src/pybind/mgr/cephadm/module.py +++ b/src/pybind/mgr/cephadm/module.py @@ -533,6 +533,7 @@ class CephadmOrchestrator(orchestrator.Orchestrator, MgrModule, self._temp_files: List = [] self.ssh_key: Optional[str] = None self.ssh_pub: Optional[str] = None + self.ssh_cert: Optional[str] = None self.use_agent = False self.agent_refresh_rate = 0 self.agent_down_multiplier = 0.0 @@ -1074,12 +1075,25 @@ class CephadmOrchestrator(orchestrator.Orchestrator, MgrModule, self._validate_and_set_ssh_val('ssh_identity_pub', inbuf, old) return 0, "", "" + @orchestrator._cli_write_command( + 'cephadm set-signed-cert') + def _set_signed_cert(self, inbuf: Optional[str] = None) -> Tuple[int, str, str]: + """Set a signed cert if CA signed keys are being used (use -i )""" + if inbuf is None or len(inbuf) == 0: + return -errno.EINVAL, "", "empty cert file provided" + old = self.ssh_cert + if inbuf == old: + return 0, "value unchanged", "" + self._validate_and_set_ssh_val('ssh_identity_cert', inbuf, old) + return 0, "", "" + @orchestrator._cli_write_command( 'cephadm clear-key') def _clear_key(self) -> Tuple[int, str, str]: """Clear cluster SSH key""" self.set_store('ssh_identity_key', None) self.set_store('ssh_identity_pub', None) + self.set_store('ssh_identity_cert', None) self.ssh._reconfig_ssh() self.log.info('Cleared cluster SSH key') return 0, '', '' @@ -1093,6 +1107,15 @@ class CephadmOrchestrator(orchestrator.Orchestrator, MgrModule, else: return -errno.ENOENT, '', 'No cluster SSH key defined' + @orchestrator._cli_read_command( + 'cephadm get-signed-cert') + def _get_signed_cert(self) -> Tuple[int, str, str]: + """Show SSH signed cert for connecting to cluster hosts using CA signed keys""" + if self.ssh_cert: + return 0, self.ssh_cert, '' + else: + return -errno.ENOENT, '', 'No signed cert defined' + @orchestrator._cli_read_command( 'cephadm get-user') def _get_user(self) -> Tuple[int, str, str]: diff --git a/src/pybind/mgr/cephadm/ssh.py b/src/pybind/mgr/cephadm/ssh.py index c202bb00a4a84..d17cc0fcc1985 100644 --- a/src/pybind/mgr/cephadm/ssh.py +++ b/src/pybind/mgr/cephadm/ssh.py @@ -331,18 +331,28 @@ class SSHManager: # identity ssh_key = self.mgr.get_store("ssh_identity_key") ssh_pub = self.mgr.get_store("ssh_identity_pub") + ssh_cert = self.mgr.get_store("ssh_identity_cert") self.mgr.ssh_pub = ssh_pub self.mgr.ssh_key = ssh_key - if ssh_key and ssh_pub: + self.mgr.ssh_cert = ssh_cert + if ssh_key: self.mgr.tkey = NamedTemporaryFile(prefix='cephadm-identity-') self.mgr.tkey.write(ssh_key.encode('utf-8')) os.fchmod(self.mgr.tkey.fileno(), 0o600) self.mgr.tkey.flush() # make visible to other processes - tpub = open(self.mgr.tkey.name + '.pub', 'w') - os.fchmod(tpub.fileno(), 0o600) - tpub.write(ssh_pub) - tpub.flush() # make visible to other processes - temp_files += [self.mgr.tkey, tpub] + temp_files += [self.mgr.tkey] + if ssh_pub: + tpub = open(self.mgr.tkey.name + '.pub', 'w') + os.fchmod(tpub.fileno(), 0o600) + tpub.write(ssh_pub) + tpub.flush() # make visible to other processes + temp_files += [tpub] + if ssh_cert: + tcert = open(self.mgr.tkey.name + '-cert.pub', 'w') + os.fchmod(tcert.fileno(), 0o600) + tcert.write(ssh_cert) + tcert.flush() # make visible to other processes + temp_files += [tcert] ssh_options += ['-i', self.mgr.tkey.name] self.mgr._temp_files = temp_files