Merge "TLS by default for the overcloud"

This commit is contained in:
Zuul 2018-05-15 12:11:46 +00:00 committed by Gerrit Code Review
commit 816b3a3453
8 changed files with 240 additions and 36 deletions

View File

@ -0,0 +1,8 @@
---
features:
- |
The default plan deployment workflow now automatically adds the necessary
certificate and key to enable TLS by default in the overcloud. Note that
this doesn't overwrite any certificate or keys given by the deployer;
those still take precedence. This will enable TLS if it isn't already
enabled though.

View File

@ -28,3 +28,4 @@ python-keystoneclient>=3.8.0 # Apache-2.0
keystoneauth1>=3.4.0 # Apache-2.0
tenacity>=4.4.0 # Apache-2.0
futures>=3.0.0;python_version=='2.7' or python_version=='2.6' # BSD
cryptography>=2.1 # BSD/Apache-2.0

48
scripts/tripleo-overcloud-cert Executable file
View File

@ -0,0 +1,48 @@
#!/bin/bash
set -x
# Currently action is unused, but it will be.
action=$1
overcloud_container_name=$2
if [[ "$action" == 'request' || "$action" == 'resubmit' ]]; then
overcloud_fqdn=$3
OVERCLOUD_CERT_PATH="/etc/pki/tls/certs/overcloud-${overcloud_container_name}-cert.pem"
OVERCLOUD_KEY_PATH="/etc/pki/tls/private/overcloud-${overcloud_container_name}-key.pem"
# This validates that overcloud_fqdn is actually an FQDN
if [[ ! $(echo "$overcloud_fqdn" | grep -P '(?=^.{1,254}$)(^(?>(?!\d+\.)[a-zA-Z0-9_\-]{1,63}\.?)+(?:[a-zA-Z]{2,})$)') ]]
then
exit 1
fi
# Skip request if the request already exists
/usr/bin/getcert list -c local -i "overcloud-${overcloud_container_name}-cert" > /dev/null
request_exists=$?
if [[ $request_exists != 0 || "$action" == 'resubmit' ]];
then
if [[ "$action" == "request" ]]; then
/usr/bin/getcert request -c local \
-I "overcloud-${overcloud_container_name}-cert" \
-f $OVERCLOUD_CERT_PATH \
-k $OVERCLOUD_KEY_PATH \
-N "CN=${overcloud_fqdn}" \
-D "$overcloud_fqdn" \
-C "/usr/bin/chown mistral:mistral $OVERCLOUD_CERT_PATH $OVERCLOUD_KEY_PATH" \
-w -v
else
/usr/bin/getcert resubmit -c local \
-i "overcloud-${overcloud_container_name}-cert" \
-f $OVERCLOUD_CERT_PATH \
-N "CN=${overcloud_fqdn}" \
-D "$overcloud_fqdn" \
-C "/usr/bin/chown mistral:mistral $OVERCLOUD_CERT_PATH $OVERCLOUD_KEY_PATH" \
-w -v
fi
fi
elif [[ "$action" == 'query' ]]; then
/usr/bin/getcert list -c local -i "overcloud-${overcloud_container_name}-cert"
else
echo "Unkown action $action"
exit 1
fi

View File

@ -31,6 +31,7 @@ scripts =
scripts/run-validation
scripts/tripleo-build-images
scripts/tripleo-config-download
scripts/tripleo-overcloud-cert
scripts/upgrade-non-controller.sh
scripts/upload-puppet-modules
scripts/upload-swift-artifacts

View File

@ -9,4 +9,5 @@ mistral ALL = NOPASSWD: /usr/bin/rm -f /tmp/validations_identity_[A-Za-z0-9_][A-
mistral ALL = NOPASSWD: /bin/nova-manage cell_v2 discover_hosts *
mistral ALL = NOPASSWD: /usr/bin/tar --ignore-failed-read -C / -cf /var/tmp/undercloud-backup-*.tar *
mistral ALL = NOPASSWD: /usr/bin/chown mistral. /var/tmp/undercloud-backup-*/filesystem-*.tar
mistral ALL = NOPASSWD: /usr/bin/tripleo-overcloud-cert *
validations ALL = NOPASSWD: ALL

View File

@ -12,13 +12,17 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cryptography.hazmat.backends import default_backend
from cryptography import x509
import json
import logging
import os
import time
from heatclient.common import deployment_utils
from heatclient import exc as heat_exc
from mistral_lib import actions
from oslo_concurrency import processutils
from swiftclient import exceptions as swiftexceptions
from tripleo_common.actions import base
@ -203,11 +207,21 @@ class DeployStackAction(templates.ProcessTemplatesAction):
return heat.stacks.update(stack.id, **stack_args)
def set_tls_parameters(self, parameters, env,
local_ca_path=constants.LOCAL_CACERT_PATH):
local_ca_path=constants.LOCAL_CACERT_PATH,
overcloud_cert_path=constants.OVERCLOUD_CERT_PATH,
overcloud_key_path=constants.OVERCLOUD_KEY_PATH):
cacert_string = self._get_local_cacert(local_ca_path)
if cacert_string:
parameters['CAMap'] = self._get_updated_camap_entry(
'undercloud-ca', cacert_string, self._get_camap(env))
if not cacert_string:
return
parameters['CAMap'] = self._get_updated_camap_entry(
'undercloud-ca', cacert_string, self._get_camap(env))
cert_path = overcloud_cert_path.format(container=self.container)
key_path = overcloud_key_path.format(container=self.container)
if self._deployment_needs_local_cert(env, cert_path, key_path):
self._request_cert_if_necessary(env, cert_path, key_path)
parameters['SSLCertificate'] = self._get_local_file(cert_path)
parameters['SSLKey'] = self._get_local_file(key_path)
def _get_local_cacert(self, local_ca_path):
# Since the undercloud has TLS by default, we'll add the undercloud's
@ -223,9 +237,20 @@ class DeployStackAction(templates.ProcessTemplatesAction):
except Exception:
raise
def _get_local_file(self, path):
with open(path, 'rb') as openfile:
return openfile.read()
def _get_camap(self, env):
return env['parameter_defaults'].get('CAMap', {})
def _get_overcloud_tls_certificate(self, env):
cert_raw = env['parameter_defaults'].get('SSLCertificate', b'')
if hasattr(cert_raw, 'encode'):
return cert_raw.encode('ascii')
else:
return cert_raw
def _get_updated_camap_entry(self, entry_name, cacert, orig_camap):
ca_map_entry = {
entry_name: {
@ -235,6 +260,68 @@ class DeployStackAction(templates.ProcessTemplatesAction):
orig_camap.update(ca_map_entry)
return orig_camap
def _cert_is_from_local_issuer(self, cert):
for attribute in cert.issuer:
# '2.5.4.3' is the OID of the certificate issuer's CommonName or CN
# 'Local Signing Authority' is the default CA name that certmonger
# uses for the local CA
if (attribute.oid.dotted_string == '2.5.4.3' and
attribute.value == 'Local Signing Authority'):
return True
return False
def _deployment_needs_local_cert(self, env, cert_path, key_path):
overcloud_cert_bytes = self._get_overcloud_tls_certificate(env)
overcloud_certmonger = env['parameter_defaults'].get(
'PublicSSLCertificateAutogenerated', False)
enable_tls_flag = env['parameter_defaults'].get(
'EnablePublicTLS', True)
if overcloud_cert_bytes:
overcloud_cert = x509.load_pem_x509_certificate(
overcloud_cert_bytes, default_backend())
if not self._cert_is_from_local_issuer(overcloud_cert):
return False
elif overcloud_certmonger or not enable_tls_flag:
return False
return True
def _request_cert_if_necessary(self, env, cert_path, key_path):
overcloud_fqdn = env['parameter_defaults'].get(
'CloudName', 'overcloud.localdomain')
if self._local_cert_request_present():
# Even if certmonger is tracking the request, the user
# might have deleted the cert file, so we resubmit the request.
if not os.path.isfile(cert_path):
self._request_local_cert(
overcloud_fqdn,
resubmit=True)
if not os.path.isfile(key_path):
raise RuntimeError(
"The key \"{0}\" is not present, you'll need to "
"recreate the certificate manually.".format(key_path))
else:
self._request_local_cert(overcloud_fqdn)
def _request_local_cert(self, overcloud_fqdn, resubmit=False):
action = 'request' if not resubmit else 'resubmit'
return processutils.execute(
'/usr/bin/sudo',
'/usr/bin/tripleo-overcloud-cert',
action,
self.container,
overcloud_fqdn)
def _local_cert_request_present(self):
try:
processutils.execute(
'/usr/bin/sudo',
'/usr/bin/tripleo-overcloud-cert',
'query',
self.container)
return True
except processutils.ProcessExecutionError:
return False
class OvercloudRcAction(base.TripleOAction):
"""Generate the overcloudrc and overcloudrc.v3 for a plan

View File

@ -57,6 +57,10 @@ DEFAULT_VALIDATIONS_PATH = \
# The path to the local CA certificate installed on the undercloud
LOCAL_CACERT_PATH = '/etc/pki/ca-trust/source/anchors/cm-local-ca.pem'
# The path to the locally generated overcloud certificate and key
OVERCLOUD_CERT_PATH = '/etc/pki/tls/certs/overcloud-{container}-cert.pem'
OVERCLOUD_KEY_PATH = '/etc/pki/tls/private/overcloud-{container}-key.pem'
# TRIPLEO_META_USAGE_KEY is inserted into metadata for containers created in
# Swift via SwiftPlanStorageBackend to identify them from other containers
TRIPLEO_META_USAGE_KEY = 'x-container-meta-usage-tripleo'

View File

@ -12,6 +12,14 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# 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 tempfile
import yaml
@ -362,7 +370,44 @@ class DeployStackActionTest(base.TestCase):
"overcloud-swift-rings", "swift-rings.tar.gz",
"overcloud-swift-rings/swift-rings.tar.gz-%d" % 1473366264)
def test_set_tls_parameters_no_ca_found(self):
class DeployStackActionSetTLSParametersTest(base.TestCase):
def get_self_signed_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 create_temp_file(self, content):
temp_file = tempfile.NamedTemporaryFile()
temp_file.write(content)
temp_file.flush()
self.addCleanup(temp_file.close)
return temp_file.name
def test_no_ca_found(self):
action = deployment.DeployStackAction(1, 'overcloud',
skip_deploy_identifier=True)
my_params = {}
@ -371,49 +416,58 @@ class DeployStackActionTest(base.TestCase):
local_ca_path='/tmp/my-unexistent-file.txt')
self.assertEqual(my_params, {})
def test_set_tls_parameters_ca_found_no_camap_provided(self):
@mock.patch('oslo_concurrency.processutils.execute')
def test_ca_found_no_camap_provided(self, mock_execute):
action = deployment.DeployStackAction(1, 'overcloud',
skip_deploy_identifier=True)
# Write test data
my_params = {}
my_env = {'parameter_defaults': {}}
with tempfile.NamedTemporaryFile() as ca_file:
# Write test data
ca_file.write(b'FAKE CA CERT')
ca_file.flush()
cert_pem, _ = self.get_self_signed_certificate_and_private_key()
ca_file_path = self.create_temp_file(cert_pem)
overcloud_cert_path = self.create_temp_file(b'FAKE OVERCLOUD CERT')
overcloud_key_path = self.create_temp_file(b'FAKE OVERCLOUD KEY')
# Test
action.set_tls_parameters(parameters=my_params, env=my_env,
local_ca_path=ca_file.name)
self.assertIn('CAMap', my_params)
self.assertIn('undercloud-ca', my_params['CAMap'])
self.assertIn('content', my_params['CAMap']['undercloud-ca'])
self.assertEqual(b'FAKE CA CERT',
my_params['CAMap']['undercloud-ca']['content'])
# Test
action.set_tls_parameters(parameters=my_params, env=my_env,
local_ca_path=ca_file_path,
overcloud_cert_path=overcloud_cert_path,
overcloud_key_path=overcloud_key_path)
self.assertIn('CAMap', my_params)
self.assertIn('undercloud-ca', my_params['CAMap'])
self.assertIn('content', my_params['CAMap']['undercloud-ca'])
self.assertEqual(cert_pem,
my_params['CAMap']['undercloud-ca']['content'])
def test_set_tls_parameters_ca_found_camap_provided(self):
@mock.patch('oslo_concurrency.processutils.execute')
def test_ca_found_camap_provided(self, mock_execute):
action = deployment.DeployStackAction(1, 'overcloud',
skip_deploy_identifier=True)
# Write test data
undercloud_pem, _ = self.get_self_signed_certificate_and_private_key()
overcloud_pem, _ = self.get_self_signed_certificate_and_private_key()
my_params = {}
my_env = {
'parameter_defaults': {
'CAMap': {'overcloud-ca': {'content': b'ANOTER FAKE CERT'}}}}
with tempfile.NamedTemporaryFile() as ca_file:
# Write test data
ca_file.write(b'FAKE CA CERT')
ca_file.flush()
'CAMap': {'overcloud-ca': {'content': overcloud_pem}}}}
ca_file_path = self.create_temp_file(undercloud_pem)
overcloud_cert_path = self.create_temp_file(b'FAKE OVERCLOUD CERT')
overcloud_key_path = self.create_temp_file(b'FAKE OVERCLOUD KEY')
# Test
action.set_tls_parameters(parameters=my_params, env=my_env,
local_ca_path=ca_file.name)
self.assertIn('CAMap', my_params)
self.assertIn('undercloud-ca', my_params['CAMap'])
self.assertIn('content', my_params['CAMap']['undercloud-ca'])
self.assertEqual(b'FAKE CA CERT',
my_params['CAMap']['undercloud-ca']['content'])
self.assertIn('overcloud-ca', my_params['CAMap'])
self.assertIn('content', my_params['CAMap']['overcloud-ca'])
self.assertEqual(b'ANOTER FAKE CERT',
my_params['CAMap']['overcloud-ca']['content'])
# Test
action.set_tls_parameters(parameters=my_params, env=my_env,
local_ca_path=ca_file_path,
overcloud_cert_path=overcloud_cert_path,
overcloud_key_path=overcloud_key_path)
self.assertIn('CAMap', my_params)
self.assertIn('undercloud-ca', my_params['CAMap'])
self.assertIn('content', my_params['CAMap']['undercloud-ca'])
self.assertEqual(undercloud_pem,
my_params['CAMap']['undercloud-ca']['content'])
self.assertIn('overcloud-ca', my_params['CAMap'])
self.assertIn('content', my_params['CAMap']['overcloud-ca'])
self.assertEqual(overcloud_pem,
my_params['CAMap']['overcloud-ca']['content'])
class OvercloudRcActionTestCase(base.TestCase):