Use certmonger for automatic cert generation

This will enable us to use a real CA to request the service certificates.

bp tls-via-certmonger

Depends-On: I32ded4e33abffd51f220fb8a7dc6263aace72acd
Change-Id: I5009273110154f0327ad542d75e83ff67bf72613
This commit is contained in:
Juan Antonio Osorio Robles 2016-06-22 15:03:03 +03:00
parent b487f96a54
commit faeed2494e
7 changed files with 72 additions and 129 deletions

View File

@ -62,6 +62,18 @@ include ::rabbitmq
include ::tripleo::firewall
include ::tripleo::selinux
if hiera('tripleo::haproxy::service_certificate', undef) {
class {'::tripleo::profile::base::haproxy':
enable_load_balancer => true,
}
include ::tripleo::keepalived
# NOTE: This is required because the haproxy configuration should be changed
# before any keystone operations are triggered. Without this, it will try to
# access the new endpoints that point to haproxy even if haproxy hasn't
# started yet.
Class['::tripleo::haproxy'] -> Anchor['keystone::install::begin']
}
# MySQL
include ::tripleo::profile::base::database::mysql
# Raise the mysql file limit
@ -202,7 +214,7 @@ class { '::ironic::db::mysql':
# pre-install swift here so we can build rings
include ::swift
if hiera('service_certificate', undef) {
if hiera('tripleo::haproxy::service_certificate', undef) {
$keystone_public_endpoint = join(['https://', hiera('controller_public_vip'), ':13000'])
$enable_proxy_headers_parsing = true
} else {
@ -418,16 +430,6 @@ class { '::ironic::drivers::pxe':
pxe_bootfile_name => $pxe_bootfile_name
}
if hiera('service_certificate', undef) {
class { '::tripleo::haproxy':
# with our current version of hiera, we can't set tripleo::haproxy::service_certificate in hieradata
# because the value might be empty and puppet would fail to compile the catalog.
service_certificate => hiera('service_certificate', undef),
}
include ::tripleo::keepalived
}
if str2bool(hiera('enable_tempest', true)) {
# tempest
# TODO: when puppet-tempest supports install by package, do that instead

View File

@ -7,10 +7,24 @@ debug: {{UNDERCLOUD_DEBUG}}
controller_host: {{LOCAL_IP}} #local-ipv4
controller_admin_vip: {{UNDERCLOUD_ADMIN_VIP}}
controller_public_vip: {{UNDERCLOUD_PUBLIC_VIP}}
service_certificate: {{UNDERCLOUD_SERVICE_CERTIFICATE}}
ntp::servers:
-
# SSL
tripleo::haproxy::service_certificate: {{UNDERCLOUD_SERVICE_CERTIFICATE}}
generate_service_certificates: {{GENERATE_SERVICE_CERTIFICATE}}
tripleo::profile::base::haproxy::certificates_specs:
undercloud-haproxy-public:
service_pem: {{UNDERCLOUD_SERVICE_CERTIFICATE}}
service_certificate: '/etc/pki/tls/certs/undercloud-front.crt'
service_key: '/etc/pki/tls/private/undercloud-front.key'
hostname: "%{hiera('controller_public_vip')}"
postsave_cmd: "/usr/bin/instack-haproxy-cert-update '/etc/pki/tls/certs/undercloud-front.crt' '/etc/pki/tls/private/undercloud-front.key' {{UNDERCLOUD_SERVICE_CERTIFICATE}}"
principal: {{SERVICE_PRINCIPAL}}
# CA defaults
certmonger_ca: {{CERTIFICATE_GENERATION_CA}}
# Common Hiera data gets applied to all nodes
ssh::server::storeconfigs_enabled: false
@ -495,6 +509,7 @@ zaqar::message_pipeline: 'zaqar.notification.notifier'
zaqar::max_messages_post_size: 524288
# HAproxy
tripleo::profile::base::haproxy::step: 1
tripleo::haproxy::haproxy_stats_password: {{UNDERCLOUD_HAPROXY_STATS_PASSWORD}}
tripleo::haproxy::controller_virtual_ip: "%{hiera('controller_admin_vip')}"
tripleo::haproxy::controller_hosts: "%{hiera('controller_host')}"

View File

@ -290,8 +290,7 @@ class TestGenerateEnvironment(BaseTestCase):
self.assertEqual('https://192.0.2.2:13808/v1/AUTH_%(tenant_id)s',
env['UNDERCLOUD_ENDPOINT_SWIFT_PUBLIC'])
@mock.patch('instack_undercloud.undercloud._generate_certificate')
def test_generate_endpoints_ssl_auto(self, mock_gen_cert):
def test_generate_endpoints_ssl_auto(self):
conf = config_fixture.Config()
self.useFixture(conf)
conf.config(generate_service_certificate=True)
@ -558,41 +557,3 @@ class TestPostConfig(base.BaseTestCase):
mock_instance.flavors.list.return_value = mock_flavors
undercloud._delete_default_flavors(mock_instance)
mock_instance.flavors.delete.assert_called_once_with('8ar')
class FakeException(Exception):
pass
@mock.patch('os.path.exists')
@mock.patch('instack_undercloud.undercloud._run_command')
class TestGenerateCertificate(base.BaseTestCase):
def test_normal(self, mock_run_command, mock_exists):
with mock.patch('instack_undercloud.undercloud.open'):
fake_env = {}
undercloud._generate_certificate(fake_env)
self.assertEqual('/etc/pki/instack-certs/undercloud-192.0.2.2.pem',
fake_env['UNDERCLOUD_SERVICE_CERTIFICATE'])
def test_exists(self, mock_run_command, mock_exists):
mock_exists.return_value = True
with mock.patch('instack_undercloud.undercloud.open') as mock_open:
fake_env = {}
undercloud._generate_certificate(fake_env)
self.assertFalse(mock_open.called)
self.assertEqual('/etc/pki/instack-certs/undercloud-192.0.2.2.pem',
fake_env['UNDERCLOUD_SERVICE_CERTIFICATE'])
@mock.patch('os.remove')
def test_command_fails(self, mock_remove, mock_run_command, mock_exists):
mock_run_command.side_effect = FakeException
mock_exists.side_effect = [False, True, True]
self.assertRaises(FakeException, undercloud._generate_certificate, {})
self.assertEqual(3, mock_remove.call_count)
@mock.patch('os.remove')
def test_file_missing(self, mock_remove, mock_run_command, mock_exists):
mock_run_command.side_effect = FakeException
mock_exists.side_effect = [False, True, False]
self.assertRaises(FakeException, undercloud._generate_certificate, {})
self.assertEqual(2, mock_remove.call_count)

View File

@ -162,6 +162,18 @@ _opts = [
'this certificate will also be added to the system\'s '
'trusted certificate store.')
),
cfg.StrOpt('certificate_generation_ca',
default='local',
help=('The certmonger nickname of the CA from which the '
'certificate will be requested. This is used only if '
'the generate_service_certificate option is set.')
),
cfg.StrOpt('service_principal',
default='',
help=('The kerberos principal for the service that will use '
'the certificate. This is only needed if your CA '
'requires a kerberos principal. e.g. with FreeIPA.')
),
cfg.StrOpt('local_interface',
default='eth1',
help=('Network interface on the Undercloud that will be '
@ -753,82 +765,6 @@ def _write_password_file(instack_env):
password_file.write('%s=%s\n' % (opt.name, value))
SSL_CONFIG_TEMPLATE = '''[req]
distinguished_name = req_distinguished_name
x509_extensions = v3_req
prompt = no
[req_distinguished_name]
C = xx
ST = xx
L = xxxx
O = xxxx
CN = %(public_vip)s
[v3_req]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
basicConstraints = CA:TRUE
subjectAltName = @alt_names
[alt_names]
IP = %(public_vip)s
'''
def _generate_certificate(instack_env):
public_vip = CONF.undercloud_public_vip
home_pem = os.path.expanduser('~/undercloud-%s.pem' % public_vip)
undercloud_pem = ('/etc/pki/instack-certs/undercloud-%s.pem' %
public_vip)
if os.path.exists(home_pem):
LOG.info('%s already exists. Not generating a new '
'certificate', home_pem)
instack_env['UNDERCLOUD_SERVICE_CERTIFICATE'] = undercloud_pem
return
ssl_config = SSL_CONFIG_TEMPLATE % {'public_vip': public_vip}
ssl_config_file = tempfile.mkstemp()[1]
try:
with open(ssl_config_file, 'w') as f:
f.write(ssl_config)
privkey = tempfile.mkstemp()[1]
cacert = tempfile.mkstemp()[1]
args = ['openssl', 'genrsa', '-out', privkey, '2048']
_run_command(args, name='openssl private key')
args = ['openssl', 'req', '-new', '-x509', '-key', privkey, '-out',
cacert, '-days', '3650', '-config', ssl_config_file]
_run_command(args, name='openssl cacert')
with open(home_pem, 'w') as u:
with open(cacert) as c:
u.write(c.read())
with open(privkey) as p:
u.write(p.read())
args = ['sudo', 'mkdir', '-p', '/etc/pki/instack-certs']
_run_command(args, name='mkdir instack-certs')
args = ['sudo', 'cp', '-f', home_pem, undercloud_pem]
_run_command(args, name='cp undercloud.pem')
args = ['sudo', 'semanage', 'fcontext', '-a', '-t', 'etc_t',
'"/etc/pki/instack-certs(/.*)?"']
_run_command(args, name='semanage')
args = ['sudo', 'restorecon', '-R', '/etc/pki/instack-certs']
_run_command(args, name='restorecon')
instack_env['UNDERCLOUD_SERVICE_CERTIFICATE'] = undercloud_pem
LOG.info('Generated new service certificate in %s', undercloud_pem)
cacert_path = ('/etc/pki/ca-trust/source/anchors/cacert-%s.pem' %
public_vip)
args = ['sudo', 'cp', '-f', cacert, cacert_path]
_run_command(args, name='copy cacert')
args = ['sudo', 'update-ca-trust', 'extract']
_run_command(args, name='update-ca-trust')
LOG.info('Added %s to system certificate store', cacert_path)
finally:
os.remove(ssl_config_file)
if os.path.exists(privkey):
os.remove(privkey)
if os.path.exists(cacert):
os.remove(cacert)
def _generate_environment(instack_root):
"""Generate an environment dict for instack
@ -923,7 +859,9 @@ def _generate_environment(instack_root):
_write_password_file(instack_env)
if CONF.generate_service_certificate:
_generate_certificate(instack_env)
public_vip = CONF.undercloud_public_vip
instack_env['UNDERCLOUD_SERVICE_CERTIFICATE'] = (
'/etc/pki/tls/certs/undercloud-%s.pem' % public_vip)
return instack_env

View File

@ -0,0 +1,16 @@
#!/bin/bash
CERT_FILE="$1"
KEY_FILE="$2"
OUTPUT_FILE="$3"
if [[ -z "$CERT_FILE" || -z "$KEY_FILE" || -z "$OUTPUT_FILE" ]]; then
echo "You need to provide CERT_FILE KEY_FILE and finally OUTPUT_FILE" \
"as arguments in that order."
exit 1
fi
if [[ ! -f "$CERT_FILE" || ! -f "$KEY_FILE" ]]; then
echo "Certificate and key files must exist!"
exit 1
fi
cat $CERT_FILE $KEY_FILE > $OUTPUT_FILE
systemctl reload haproxy

View File

@ -26,6 +26,7 @@ scripts =
scripts/instack-create-overcloudrc
scripts/instack-install-undercloud
scripts/instack-virt-setup
scripts/instack-haproxy-cert-update
data_files =
share/instack-undercloud/ = elements/*

View File

@ -52,6 +52,16 @@
# store. (boolean value)
#generate_service_certificate = false
# The certmonger nickname of the CA from which the certificate will be
# requested. This is used only if the generate_service_certificate
# option is set. (string value)
#certificate_generation_ca = local
# The kerberos principal for the service that will use the
# certificate. This is only needed if your CA requires a kerberos
# principal. e.g. with FreeIPA. (string value)
#service_principal =
# Network interface on the Undercloud that will be handling the PXE
# boots and DHCP for Overcloud instances. (string value)
#local_interface = eth1