Handle user-provided TLS certificate/key for the undercloud

instack-undercloud has support for using a user-provided certificate and
key. This is done through the undercloud_service_certificate
configuration option. This commit adds support for the same
functionality for the containerized undercloud.

It relies on pyca/cryptography, so I added it to the requirements.txt
file. And what it does is that it will read the PEM file and add the
parameter_defaults that are needed to enable TLS.

Change-Id: I26dbde57131f6328d0c5e400dcdfdf62cdd43a41
This commit is contained in:
Juan Antonio Osorio Robles 2018-01-02 15:25:46 +02:00
parent 52b71ea76e
commit 7012843075
4 changed files with 225 additions and 13 deletions

View File

@ -0,0 +1,6 @@
---
features:
- |
Similar to what instack-undercloud does, the containerized undercloud can
now take user-provided certificates/keys in the bundled PEM format. This is
done through the service_certificate option and is processed tripleoclient.

View File

@ -18,3 +18,4 @@ osc-lib>=1.8.0 # Apache-2.0
websocket-client<=0.40.0,>=0.33.0 # LGPLv2+
tripleo-common>=7.1.0 # Apache-2.0
python-zaqarclient>=1.0.0 # Apache-2.0
cryptography>=1.9,!=2.0 # BSD/Apache-2.0

View File

@ -12,8 +12,18 @@
# License for the specific language governing permissions and limitations
# under the License.
#
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization
from cryptography import x509
from cryptography.x509.oid import NameOID
from datetime import datetime
from datetime import timedelta
import mock
import os
import tempfile
import yaml
from tripleoclient.tests import base
from tripleoclient.v1 import undercloud_config
@ -90,3 +100,144 @@ class TestProcessDriversAndHardwareTypes(base.TestCase):
'IronicEnabledRaidInterfaces': ['idrac', 'no-raid'],
'IronicEnabledVendorInterfaces': ['idrac', 'ipmitool', 'no-vendor']
}, env)
class TestTLSSettings(base.TestCase):
def test_public_host_with_ip_should_give_ip_endpoint_environment(self):
expected_env_file = os.path.join(
undercloud_config.THT_HOME,
"environments/tls-endpoints-public-ip.yaml")
resulting_env_file1 = undercloud_config._get_tls_endpoint_environment(
'127.0.0.1', undercloud_config.THT_HOME)
self.assertEqual(expected_env_file, resulting_env_file1)
resulting_env_file2 = undercloud_config._get_tls_endpoint_environment(
'192.168.1.1', undercloud_config.THT_HOME)
self.assertEqual(expected_env_file, resulting_env_file2)
def test_public_host_with_fqdn_should_give_dns_endpoint_environment(self):
expected_env_file = os.path.join(
undercloud_config.THT_HOME,
"environments/tls-endpoints-public-dns.yaml")
resulting_env_file1 = undercloud_config._get_tls_endpoint_environment(
'controller-1', undercloud_config.THT_HOME)
self.assertEqual(expected_env_file, resulting_env_file1)
resulting_env_file2 = undercloud_config._get_tls_endpoint_environment(
'controller-1.tripleodomain.com', undercloud_config.THT_HOME)
self.assertEqual(expected_env_file, resulting_env_file2)
def get_certificate_and_private_key(self):
private_key = rsa.generate_private_key(public_exponent=3,
key_size=1024,
backend=default_backend())
issuer = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, u"FI"),
x509.NameAttribute(NameOID.LOCALITY_NAME, u"Helsinki"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, u"Some Company"),
x509.NameAttribute(NameOID.COMMON_NAME, u"Test Certificate"),
])
cert_builder = x509.CertificateBuilder(
issuer_name=issuer, subject_name=issuer,
public_key=private_key.public_key(),
serial_number=x509.random_serial_number(),
not_valid_before=datetime.utcnow(),
not_valid_after=datetime.utcnow() + timedelta(days=10)
)
cert = cert_builder.sign(private_key,
hashes.SHA256(),
default_backend())
cert_pem = cert.public_bytes(encoding=serialization.Encoding.PEM)
key_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption())
return cert_pem, key_pem
def test_get_dict_with_cert_and_key_from_bundled_pem(self):
cert_pem, key_pem = self.get_certificate_and_private_key()
with tempfile.NamedTemporaryFile() as tempbundle:
tempbundle.write(cert_pem)
tempbundle.write(key_pem)
tempbundle.seek(0)
tls_parameters = undercloud_config._get_public_tls_parameters(
tempbundle.name)
self.assertEqual(cert_pem, tls_parameters['SSLCertificate'])
self.assertEqual(key_pem, tls_parameters['SSLKey'])
def test_get_tls_parameters_fails_cause_of_missing_cert(self):
_, key_pem = self.get_certificate_and_private_key()
with tempfile.NamedTemporaryFile() as tempbundle:
tempbundle.write(key_pem)
tempbundle.seek(0)
self.assertRaises(ValueError,
undercloud_config._get_public_tls_parameters,
tempbundle.name)
def test_get_tls_parameters_fails_cause_of_missing_key(self):
cert_pem, _ = self.get_certificate_and_private_key()
with tempfile.NamedTemporaryFile() as tempbundle:
tempbundle.write(cert_pem)
tempbundle.seek(0)
self.assertRaises(ValueError,
undercloud_config._get_public_tls_parameters,
tempbundle.name)
def test_get_tls_parameters_fails_cause_of_unexistent_file(self):
self.assertRaises(IOError,
undercloud_config._get_public_tls_parameters,
'/tmp/unexistent-file-12345.pem')
def test_get_resource_registry_overwrites(self):
enable_tls_yaml = {
"parameter_defaults": {
"SSLCertificate": "12345"
},
"resource_registry": {
"registry_overwrite_key": "registry_overwrite_value"
}
}
with tempfile.NamedTemporaryFile() as enable_tls_file:
enable_tls_file.write(yaml.dump(enable_tls_yaml, encoding='utf-8'))
enable_tls_file.seek(0)
overwrites = \
undercloud_config._get_public_tls_resource_registry_overwrites(
enable_tls_file.name)
self.assertEqual(enable_tls_yaml["resource_registry"], overwrites)
def test_get_resource_registry_overwrites_fails_cause_no_registry_entry(
self):
enable_tls_yaml = {
"parameter_defaults": {
"SSLCertificate": "12345"
},
}
with tempfile.NamedTemporaryFile() as enable_tls_file:
enable_tls_file.write(yaml.dump(enable_tls_yaml, encoding='utf-8'))
enable_tls_file.seek(0)
self.assertRaises(
RuntimeError,
undercloud_config._get_public_tls_resource_registry_overwrites,
enable_tls_file.name)
def test_get_resource_registry_overwrites_fails_cause_missing_file(self):
self.assertRaises(
IOError,
undercloud_config._get_public_tls_resource_registry_overwrites,
'/tmp/unexistent-file-12345.yaml')

View File

@ -16,6 +16,9 @@
"""Plugin action implementation"""
import copy
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography import x509
import logging
import netaddr
import os
@ -452,6 +455,7 @@ def prepare_undercloud_deploy(upgrade=False, no_validations=False):
"""Prepare Undercloud deploy command based on undercloud.conf"""
env_data = {}
registry_overwrites = {}
deploy_args = []
_load_config()
@ -542,21 +546,27 @@ def prepare_undercloud_deploy(upgrade=False, no_validations=False):
"environments/services-docker/undercloud-cinder.yaml")]
if CONF.get('generate_service_certificate'):
try:
public_host = CONF.get('undercloud_public_host')
netaddr.IPAddress(public_host)
endpoint_environment = os.path.join(
tht_templates,
"environments/tls-endpoints-public-ip.yaml")
except netaddr.core.AddrFormatError:
endpoint_environment = os.path.join(
tht_templates,
"environments/tls-endpoints-public-dns.yaml")
endpoint_environment = _get_tls_endpoint_environment(
CONF.get('undercloud_public_host'), tht_templates)
deploy_args += ['-e', os.path.join(
tht_templates,
"environments/public-tls-undercloud.yaml"),
'-e', endpoint_environment]
elif CONF.get('undercloud_service_certificate'):
endpoint_environment = _get_tls_endpoint_environment(
CONF.get('undercloud_public_host'), tht_templates)
enable_tls_yaml_path = os.path.join(tht_templates,
"environments/ssl/enable-tls.yaml")
env_data.update(
_get_public_tls_parameters(
CONF.get('undercloud_service_certificate')))
registry_overwrites.update(
_get_public_tls_resource_registry_overwrites(enable_tls_yaml_path))
deploy_args += [
'-e', endpoint_environment, '-e',
'environments/services-docker/undercloud-haproxy.yaml',
'-e', 'environments/services-docker/undercloud-keepalived.yaml']
deploy_args += [
"-e", os.path.join(tht_templates, "environments/docker.yaml"),
@ -565,7 +575,8 @@ def prepare_undercloud_deploy(upgrade=False, no_validations=False):
"environments/config-download-environment.yaml"),
"-e", os.path.join(tht_templates, "environments/undercloud.yaml")]
env_file = _write_env_file(env_data)
env_file = _write_env_file(
env_data, registry_overwrites=registry_overwrites)
deploy_args += ['-e', env_file]
if CONF.get('custom_env_files'):
@ -581,11 +592,54 @@ def prepare_undercloud_deploy(upgrade=False, no_validations=False):
return cmd
def _get_tls_endpoint_environment(public_host, tht_templates):
try:
netaddr.IPAddress(public_host)
return os.path.join(tht_templates,
"environments/tls-endpoints-public-ip.yaml")
except netaddr.core.AddrFormatError:
return os.path.join(tht_templates,
"environments/tls-endpoints-public-dns.yaml")
def _get_public_tls_parameters(service_certificate_path):
with open(service_certificate_path, "rb") as pem_file:
pem_data = pem_file.read()
cert = x509.load_pem_x509_certificate(pem_data, default_backend())
private_key = serialization.load_pem_private_key(
pem_data,
password=None,
backend=default_backend())
key_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption())
cert_pem = cert.public_bytes(serialization.Encoding.PEM)
return {
'SSLCertificate': cert_pem,
'SSLKey': key_pem
}
def _get_public_tls_resource_registry_overwrites(enable_tls_yaml_path):
with open(enable_tls_yaml_path, 'rb') as enable_tls_file:
enable_tls_dict = yaml.load(enable_tls_file.read())
try:
return enable_tls_dict['resource_registry']
except KeyError:
raise RuntimeError('%s is malformed and is missing the resource '
'registry.' % enable_tls_yaml_path)
def _write_env_file(env_data,
env_file="/tmp/undercloud_parameters.yaml"):
env_file="/tmp/undercloud_parameters.yaml",
registry_overwrites={}):
"""Write the undercloud parameters to yaml"""
data = {'parameter_defaults': env_data}
if registry_overwrites:
data['resource_registry'] = registry_overwrites
env_file = os.path.abspath(env_file)
with open(env_file, "w") as f:
try: