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
|
jsonschema>=3.2.0 # MIT
|
||||||
testtools>=2.2.0 # MIT
|
testtools>=2.2.0 # MIT
|
||||||
paramiko>=2.7.0 # LGPLv2.1+
|
paramiko>=2.7.0 # LGPLv2.1+
|
||||||
|
cryptography>=2.1 # BSD/Apache-2.0
|
||||||
netaddr>=0.7.18 # BSD
|
netaddr>=0.7.18 # BSD
|
||||||
oslo.concurrency>=3.26.0 # Apache-2.0
|
oslo.concurrency>=3.26.0 # Apache-2.0
|
||||||
oslo.config>=5.2.0 # Apache-2.0
|
oslo.config>=5.2.0 # Apache-2.0
|
||||||
|
@ -68,7 +68,8 @@ class AttachInterfacesTestBase(base.BaseV2ComputeTest):
|
|||||||
self.image_ssh_password,
|
self.image_ssh_password,
|
||||||
validation_resources['keypair']['private_key'],
|
validation_resources['keypair']['private_key'],
|
||||||
server=server,
|
server=server,
|
||||||
servers_client=self.servers_client)
|
servers_client=self.servers_client,
|
||||||
|
ssh_key_type=CONF.validation.ssh_key_type)
|
||||||
linux_client.validate_authentication()
|
linux_client.validate_authentication()
|
||||||
|
|
||||||
def _create_server_get_interfaces(self):
|
def _create_server_get_interfaces(self):
|
||||||
|
@ -118,7 +118,8 @@ class Manager(clients.ServiceClients):
|
|||||||
self.server_groups_client = self.compute.ServerGroupsClient()
|
self.server_groups_client = self.compute.ServerGroupsClient()
|
||||||
self.limits_client = self.compute.LimitsClient()
|
self.limits_client = self.compute.LimitsClient()
|
||||||
self.compute_images_client = self.compute.ImagesClient()
|
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.quotas_client = self.compute.QuotasClient()
|
||||||
self.quota_classes_client = self.compute.QuotaClassesClient()
|
self.quota_classes_client = self.compute.QuotaClassesClient()
|
||||||
self.flavors_client = self.compute.FlavorsClient()
|
self.flavors_client = self.compute.FlavorsClient()
|
||||||
|
@ -48,7 +48,8 @@ class RemoteClient(remote_client.RemoteClient):
|
|||||||
console_output_enabled=CONF.compute_feature_enabled.console_output,
|
console_output_enabled=CONF.compute_feature_enabled.console_output,
|
||||||
ssh_shell_prologue=CONF.validation.ssh_shell_prologue,
|
ssh_shell_prologue=CONF.validation.ssh_shell_prologue,
|
||||||
ping_count=CONF.validation.ping_count,
|
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
|
# Note that this method will not work on SLES11 guests, as they do
|
||||||
# not support the TYPE column on lsblk
|
# not support the TYPE column on lsblk
|
||||||
|
@ -970,6 +970,10 @@ ValidationGroup = [
|
|||||||
default='public',
|
default='public',
|
||||||
help="Network used for SSH connections. Ignored if "
|
help="Network used for SSH connections. Ignored if "
|
||||||
"connect_method=floating."),
|
"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',
|
volume_group = cfg.OptGroup(name='volume',
|
||||||
|
@ -37,7 +37,7 @@ class Client(object):
|
|||||||
|
|
||||||
def __init__(self, host, username, password=None, timeout=300, pkey=None,
|
def __init__(self, host, username, password=None, timeout=300, pkey=None,
|
||||||
channel_timeout=10, look_for_keys=False, key_filename=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.
|
"""SSH client.
|
||||||
|
|
||||||
Many of parameters are just passed to the underlying implementation
|
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
|
:param proxy_client: Another SSH client to provide a transport
|
||||||
for ssh-over-ssh. The default is None, which means
|
for ssh-over-ssh. The default is None, which means
|
||||||
not to use ssh-over-ssh.
|
not to use ssh-over-ssh.
|
||||||
|
:param ssh_key_type: ssh key type (rsa, ecdsa)
|
||||||
:type proxy_client: ``tempest.lib.common.ssh.Client`` object
|
:type proxy_client: ``tempest.lib.common.ssh.Client`` object
|
||||||
"""
|
"""
|
||||||
self.host = host
|
self.host = host
|
||||||
@ -66,8 +67,15 @@ class Client(object):
|
|||||||
self.port = port
|
self.port = port
|
||||||
self.password = password
|
self.password = password
|
||||||
if isinstance(pkey, str):
|
if isinstance(pkey, str):
|
||||||
|
if ssh_key_type == 'rsa':
|
||||||
pkey = paramiko.RSAKey.from_private_key(
|
pkey = paramiko.RSAKey.from_private_key(
|
||||||
io.StringIO(str(pkey)))
|
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.pkey = pkey
|
||||||
self.look_for_keys = look_for_keys
|
self.look_for_keys = look_for_keys
|
||||||
self.key_filename = key_filename
|
self.key_filename = key_filename
|
||||||
|
@ -69,7 +69,7 @@ class RemoteClient(object):
|
|||||||
server=None, servers_client=None, ssh_timeout=300,
|
server=None, servers_client=None, ssh_timeout=300,
|
||||||
connect_timeout=60, console_output_enabled=True,
|
connect_timeout=60, console_output_enabled=True,
|
||||||
ssh_shell_prologue="set -eu -o pipefail; PATH=$PATH:/sbin;",
|
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
|
"""Executes commands in a VM over ssh
|
||||||
|
|
||||||
:param ip_address: IP address to ssh to
|
: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 ssh_shell_prologue: Shell fragments to use before command
|
||||||
:param ping_count: Number of ping packets
|
:param ping_count: Number of ping packets
|
||||||
:param ping_size: Packet size for ping packets
|
:param ping_size: Packet size for ping packets
|
||||||
|
:param ssh_key_type: ssh key type (rsa, ecdsa)
|
||||||
"""
|
"""
|
||||||
self.server = server
|
self.server = server
|
||||||
self.servers_client = servers_client
|
self.servers_client = servers_client
|
||||||
@ -92,10 +93,12 @@ class RemoteClient(object):
|
|||||||
self.ssh_shell_prologue = ssh_shell_prologue
|
self.ssh_shell_prologue = ssh_shell_prologue
|
||||||
self.ping_count = ping_count
|
self.ping_count = ping_count
|
||||||
self.ping_size = ping_size
|
self.ping_size = ping_size
|
||||||
|
self.ssh_key_type = ssh_key_type
|
||||||
|
|
||||||
self.ssh_client = ssh.Client(ip_address, username, password,
|
self.ssh_client = ssh.Client(ip_address, username, password,
|
||||||
ssh_timeout, pkey=pkey,
|
ssh_timeout, pkey=pkey,
|
||||||
channel_timeout=connect_timeout)
|
channel_timeout=connect_timeout,
|
||||||
|
ssh_key_type=ssh_key_type)
|
||||||
|
|
||||||
@debug_ssh
|
@debug_ssh
|
||||||
def exec_command(self, cmd):
|
def exec_command(self, cmd):
|
||||||
|
@ -256,6 +256,10 @@ class SSHClientProxyClientLoop(TempestException):
|
|||||||
"%(port)s and username: %(username)s as parent")
|
"%(port)s and username: %(username)s as parent")
|
||||||
|
|
||||||
|
|
||||||
|
class SSHClientUnsupportedKeyType(TempestException):
|
||||||
|
message = ("SSH client: unsupported key type %(key_type)s")
|
||||||
|
|
||||||
|
|
||||||
class UnknownServiceClient(TempestException):
|
class UnknownServiceClient(TempestException):
|
||||||
message = "Service clients named %(services)s are not known"
|
message = "Service clients named %(services)s are not known"
|
||||||
|
|
||||||
|
@ -15,6 +15,10 @@
|
|||||||
|
|
||||||
from urllib import parse as urllib
|
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 oslo_serialization import jsonutils as json
|
||||||
|
|
||||||
from tempest.lib.api_schema.response.compute.v2_1 import keypairs as schemav21
|
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},
|
schema_versions_info = [{'min': None, 'max': '2.1', 'schema': schemav21},
|
||||||
{'min': '2.2', 'max': None, 'schema': schemav22}]
|
{'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):
|
def list_keypairs(self, **params):
|
||||||
"""Lists keypairs that are associated with the account.
|
"""Lists keypairs that are associated with the account.
|
||||||
|
|
||||||
@ -67,12 +77,30 @@ class KeyPairsClient(base_compute_client.BaseComputeClient):
|
|||||||
API reference:
|
API reference:
|
||||||
https://docs.openstack.org/api-ref/compute/#create-or-import-keypair
|
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})
|
post_body = json.dumps({'keypair': kwargs})
|
||||||
resp, body = self.post("os-keypairs", body=post_body)
|
resp, body = self.post("os-keypairs", body=post_body)
|
||||||
body = json.loads(body)
|
body = json.loads(body)
|
||||||
schema = self.get_schema(self.schema_versions_info)
|
schema = self.get_schema(self.schema_versions_info)
|
||||||
self.validate_response(schema.create_keypair, resp, body)
|
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):
|
def delete_keypair(self, keypair_name, **params):
|
||||||
"""Deletes a keypair.
|
"""Deletes a keypair.
|
||||||
|
@ -295,6 +295,22 @@
|
|||||||
devstack_localrc:
|
devstack_localrc:
|
||||||
TEMPEST_VOLUME_TYPE: volumev2
|
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:
|
- job:
|
||||||
name: tempest-pg-full
|
name: tempest-pg-full
|
||||||
parent: tempest-full-py3
|
parent: tempest-full-py3
|
||||||
|
@ -161,6 +161,8 @@
|
|||||||
irrelevant-files: *tempest-irrelevant-files
|
irrelevant-files: *tempest-irrelevant-files
|
||||||
- tempest-full-py3-opensuse15:
|
- tempest-full-py3-opensuse15:
|
||||||
irrelevant-files: *tempest-irrelevant-files
|
irrelevant-files: *tempest-irrelevant-files
|
||||||
|
- tempest-centos8-stream-fips:
|
||||||
|
irrelevant-files: *tempest-irrelevant-files
|
||||||
periodic-stable:
|
periodic-stable:
|
||||||
jobs:
|
jobs:
|
||||||
- tempest-full-xena
|
- tempest-full-xena
|
||||||
|
Loading…
Reference in New Issue
Block a user