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
This commit is contained in:
parent
fe0ac89a5a
commit
6ded070b51
4
playbooks/enable-fips.yaml
Normal file
4
playbooks/enable-fips.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
- hosts: all
|
||||
tasks:
|
||||
- include_role:
|
||||
name: enable-fips
|
@ -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.
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
|
@ -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):
|
||||
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
|
||||
|
@ -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):
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user