From 6ded070b511263df2b6c06905615a951b29e2035 Mon Sep 17 00:00:00 2001 From: Ade Lee Date: Sat, 4 Sep 2021 15:56:34 -0400 Subject: [PATCH] Add support for ecdsa keys In FIPS mode, using RSA keys for ssh is fine as long as SHA-1 is not used for the signature algorithm. Unfortunately, the version of cirros used in OpenStack CI does not have a version of dropbear that supports SHA-2 signatures. So, any connections from a FIPS enabled machine will fail as the cirros instance will only support ssh-rsa (SHA-1 signatures). To get around this, we add a new option to specify the key type (validation.ssh_key_type). This will allow the addition of other key types in future if needed. Tempest now supports 'rsa' and 'ecdsa' key types. We also add a fips job to the experimental queue to test the usage of the new key type. Change-Id: Ib59eb8432fa1a2813b3047955157d1b3d24a55f8 --- playbooks/enable-fips.yaml | 4 +++ .../add-ssh-key-type-38d7a2f900d79842.yaml | 6 ++++ requirements.txt | 1 + .../compute/servers/test_attach_interfaces.py | 3 +- tempest/clients.py | 3 +- tempest/common/utils/linux/remote_client.py | 3 +- tempest/config.py | 4 +++ tempest/lib/common/ssh.py | 14 +++++++-- .../lib/common/utils/linux/remote_client.py | 7 +++-- tempest/lib/exceptions.py | 4 +++ .../lib/services/compute/keypairs_client.py | 30 ++++++++++++++++++- zuul.d/integrated-gate.yaml | 16 ++++++++++ zuul.d/project.yaml | 2 ++ 13 files changed, 88 insertions(+), 9 deletions(-) create mode 100644 playbooks/enable-fips.yaml create mode 100644 releasenotes/notes/add-ssh-key-type-38d7a2f900d79842.yaml diff --git a/playbooks/enable-fips.yaml b/playbooks/enable-fips.yaml new file mode 100644 index 0000000000..c8f042dba6 --- /dev/null +++ b/playbooks/enable-fips.yaml @@ -0,0 +1,4 @@ +- hosts: all + tasks: + - include_role: + name: enable-fips diff --git a/releasenotes/notes/add-ssh-key-type-38d7a2f900d79842.yaml b/releasenotes/notes/add-ssh-key-type-38d7a2f900d79842.yaml new file mode 100644 index 0000000000..fef3004996 --- /dev/null +++ b/releasenotes/notes/add-ssh-key-type-38d7a2f900d79842.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add parameter to specify the SSH key type. Current options are 'rsa' + (which is the default) and 'ecdsa'. Tempest now supports the importing + and generation of both 'rsa' and 'ecdsa' SSH key types. diff --git a/requirements.txt b/requirements.txt index c71cabea9c..bc8358badf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ cliff!=2.9.0,>=2.8.0 # Apache-2.0 jsonschema>=3.2.0 # MIT testtools>=2.2.0 # MIT paramiko>=2.7.0 # LGPLv2.1+ +cryptography>=2.1 # BSD/Apache-2.0 netaddr>=0.7.18 # BSD oslo.concurrency>=3.26.0 # Apache-2.0 oslo.config>=5.2.0 # Apache-2.0 diff --git a/tempest/api/compute/servers/test_attach_interfaces.py b/tempest/api/compute/servers/test_attach_interfaces.py index ac18442eb4..efecd6ce7e 100644 --- a/tempest/api/compute/servers/test_attach_interfaces.py +++ b/tempest/api/compute/servers/test_attach_interfaces.py @@ -68,7 +68,8 @@ class AttachInterfacesTestBase(base.BaseV2ComputeTest): self.image_ssh_password, validation_resources['keypair']['private_key'], server=server, - servers_client=self.servers_client) + servers_client=self.servers_client, + ssh_key_type=CONF.validation.ssh_key_type) linux_client.validate_authentication() def _create_server_get_interfaces(self): diff --git a/tempest/clients.py b/tempest/clients.py index 327f0daa04..4c3d875aa6 100644 --- a/tempest/clients.py +++ b/tempest/clients.py @@ -118,7 +118,8 @@ class Manager(clients.ServiceClients): self.server_groups_client = self.compute.ServerGroupsClient() self.limits_client = self.compute.LimitsClient() self.compute_images_client = self.compute.ImagesClient() - self.keypairs_client = self.compute.KeyPairsClient() + self.keypairs_client = self.compute.KeyPairsClient( + ssh_key_type=CONF.validation.ssh_key_type) self.quotas_client = self.compute.QuotasClient() self.quota_classes_client = self.compute.QuotaClassesClient() self.flavors_client = self.compute.FlavorsClient() diff --git a/tempest/common/utils/linux/remote_client.py b/tempest/common/utils/linux/remote_client.py index 5d6e129d87..9d9fab70f2 100644 --- a/tempest/common/utils/linux/remote_client.py +++ b/tempest/common/utils/linux/remote_client.py @@ -48,7 +48,8 @@ class RemoteClient(remote_client.RemoteClient): console_output_enabled=CONF.compute_feature_enabled.console_output, ssh_shell_prologue=CONF.validation.ssh_shell_prologue, ping_count=CONF.validation.ping_count, - ping_size=CONF.validation.ping_size) + ping_size=CONF.validation.ping_size, + ssh_key_type=CONF.validation.ssh_key_type) # Note that this method will not work on SLES11 guests, as they do # not support the TYPE column on lsblk diff --git a/tempest/config.py b/tempest/config.py index a840a97f3f..03ddbf564e 100644 --- a/tempest/config.py +++ b/tempest/config.py @@ -970,6 +970,10 @@ ValidationGroup = [ default='public', help="Network used for SSH connections. Ignored if " "connect_method=floating."), + cfg.StrOpt('ssh_key_type', + default='rsa', + help='Type of key to use for ssh connections. ' + 'Valid types are rsa, ecdsa'), ] volume_group = cfg.OptGroup(name='volume', diff --git a/tempest/lib/common/ssh.py b/tempest/lib/common/ssh.py index ee1537542b..eb03faa369 100644 --- a/tempest/lib/common/ssh.py +++ b/tempest/lib/common/ssh.py @@ -37,7 +37,7 @@ class Client(object): def __init__(self, host, username, password=None, timeout=300, pkey=None, channel_timeout=10, look_for_keys=False, key_filename=None, - port=22, proxy_client=None): + port=22, proxy_client=None, ssh_key_type='rsa'): """SSH client. Many of parameters are just passed to the underlying implementation @@ -59,6 +59,7 @@ class Client(object): :param proxy_client: Another SSH client to provide a transport for ssh-over-ssh. The default is None, which means not to use ssh-over-ssh. + :param ssh_key_type: ssh key type (rsa, ecdsa) :type proxy_client: ``tempest.lib.common.ssh.Client`` object """ self.host = host @@ -66,8 +67,15 @@ class Client(object): self.port = port self.password = password if isinstance(pkey, str): - pkey = paramiko.RSAKey.from_private_key( - io.StringIO(str(pkey))) + if ssh_key_type == 'rsa': + pkey = paramiko.RSAKey.from_private_key( + io.StringIO(str(pkey))) + elif ssh_key_type == 'ecdsa': + pkey = paramiko.ECDSAKey.from_private_key( + io.StringIO(str(pkey))) + else: + raise exceptions.SSHClientUnsupportedKeyType( + key_type=ssh_key_type) self.pkey = pkey self.look_for_keys = look_for_keys self.key_filename = key_filename diff --git a/tempest/lib/common/utils/linux/remote_client.py b/tempest/lib/common/utils/linux/remote_client.py index d84dd28e6e..224f3bf2d9 100644 --- a/tempest/lib/common/utils/linux/remote_client.py +++ b/tempest/lib/common/utils/linux/remote_client.py @@ -69,7 +69,7 @@ class RemoteClient(object): server=None, servers_client=None, ssh_timeout=300, connect_timeout=60, console_output_enabled=True, ssh_shell_prologue="set -eu -o pipefail; PATH=$PATH:/sbin;", - ping_count=1, ping_size=56): + ping_count=1, ping_size=56, ssh_key_type='rsa'): """Executes commands in a VM over ssh :param ip_address: IP address to ssh to @@ -84,6 +84,7 @@ class RemoteClient(object): :param ssh_shell_prologue: Shell fragments to use before command :param ping_count: Number of ping packets :param ping_size: Packet size for ping packets + :param ssh_key_type: ssh key type (rsa, ecdsa) """ self.server = server self.servers_client = servers_client @@ -92,10 +93,12 @@ class RemoteClient(object): self.ssh_shell_prologue = ssh_shell_prologue self.ping_count = ping_count self.ping_size = ping_size + self.ssh_key_type = ssh_key_type self.ssh_client = ssh.Client(ip_address, username, password, ssh_timeout, pkey=pkey, - channel_timeout=connect_timeout) + channel_timeout=connect_timeout, + ssh_key_type=ssh_key_type) @debug_ssh def exec_command(self, cmd): diff --git a/tempest/lib/exceptions.py b/tempest/lib/exceptions.py index abe68d22c8..dd7885e014 100644 --- a/tempest/lib/exceptions.py +++ b/tempest/lib/exceptions.py @@ -256,6 +256,10 @@ class SSHClientProxyClientLoop(TempestException): "%(port)s and username: %(username)s as parent") +class SSHClientUnsupportedKeyType(TempestException): + message = ("SSH client: unsupported key type %(key_type)s") + + class UnknownServiceClient(TempestException): message = "Service clients named %(services)s are not known" diff --git a/tempest/lib/services/compute/keypairs_client.py b/tempest/lib/services/compute/keypairs_client.py index 9d7b7fcf00..51a4583536 100644 --- a/tempest/lib/services/compute/keypairs_client.py +++ b/tempest/lib/services/compute/keypairs_client.py @@ -15,6 +15,10 @@ from urllib import parse as urllib +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives import serialization + from oslo_serialization import jsonutils as json from tempest.lib.api_schema.response.compute.v2_1 import keypairs as schemav21 @@ -28,6 +32,12 @@ class KeyPairsClient(base_compute_client.BaseComputeClient): schema_versions_info = [{'min': None, 'max': '2.1', 'schema': schemav21}, {'min': '2.2', 'max': None, 'schema': schemav22}] + def __init__(self, auth_provider, service, region, + ssh_key_type='rsa', **kwargs): + super(KeyPairsClient, self).__init__( + auth_provider, service, region, **kwargs) + self.ssh_key_type = ssh_key_type + def list_keypairs(self, **params): """Lists keypairs that are associated with the account. @@ -67,12 +77,30 @@ class KeyPairsClient(base_compute_client.BaseComputeClient): API reference: https://docs.openstack.org/api-ref/compute/#create-or-import-keypair """ + pkey = None + if (self.ssh_key_type == 'ecdsa' and 'public_key' not in kwargs and + ('type' not in kwargs or kwargs['type'] == 'ssh')): + # create a ecdsa key and pass the public key into the request + pkey = ec.generate_private_key(ec.SECP384R1(), default_backend()) + pubkey = pkey.public_key().public_bytes( + encoding=serialization.Encoding.OpenSSH, + format=serialization.PublicFormat.OpenSSH) + kwargs['public_key'] = pubkey + post_body = json.dumps({'keypair': kwargs}) resp, body = self.post("os-keypairs", body=post_body) body = json.loads(body) schema = self.get_schema(self.schema_versions_info) self.validate_response(schema.create_keypair, resp, body) - return rest_client.ResponseBody(resp, body) + resp_body = rest_client.ResponseBody(resp, body) + if pkey: + # add the privkey to the response as it was generated here + privkey = pkey.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption()) + resp_body['keypair']['private_key'] = privkey.decode('utf-8') + return resp_body def delete_keypair(self, keypair_name, **params): """Deletes a keypair. diff --git a/zuul.d/integrated-gate.yaml b/zuul.d/integrated-gate.yaml index b86268a638..1051ccc786 100644 --- a/zuul.d/integrated-gate.yaml +++ b/zuul.d/integrated-gate.yaml @@ -295,6 +295,22 @@ devstack_localrc: TEMPEST_VOLUME_TYPE: volumev2 +- job: + name: tempest-centos8-stream-fips + parent: devstack-tempest + description: | + Integration testing for a FIPS enabled Centos 8 system + nodeset: devstack-single-node-centos-8-stream + pre-run: playbooks/enable-fips.yaml + vars: + tox_envlist: full + configure_swap_size: 4096 + devstack_local_conf: + test-config: + "$TEMPEST_CONFIG": + validation: + ssh_key_type: 'ecdsa' + - job: name: tempest-pg-full parent: tempest-full-py3 diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml index 9ab10d7d04..3f98f7e63f 100644 --- a/zuul.d/project.yaml +++ b/zuul.d/project.yaml @@ -161,6 +161,8 @@ irrelevant-files: *tempest-irrelevant-files - tempest-full-py3-opensuse15: irrelevant-files: *tempest-irrelevant-files + - tempest-centos8-stream-fips: + irrelevant-files: *tempest-irrelevant-files periodic-stable: jobs: - tempest-full-xena