From faeed2494ed8b4512ac293a62007c1503a1f4961 Mon Sep 17 00:00:00 2001 From: Juan Antonio Osorio Robles Date: Wed, 22 Jun 2016 15:03:03 +0300 Subject: [PATCH] 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 --- .../puppet-stack-config.pp | 24 ++--- .../puppet-stack-config.yaml.template | 17 +++- instack_undercloud/tests/test_undercloud.py | 41 +-------- instack_undercloud/undercloud.py | 92 +++---------------- scripts/instack-haproxy-cert-update | 16 ++++ setup.cfg | 1 + undercloud.conf.sample | 10 ++ 7 files changed, 72 insertions(+), 129 deletions(-) create mode 100644 scripts/instack-haproxy-cert-update diff --git a/elements/puppet-stack-config/puppet-stack-config.pp b/elements/puppet-stack-config/puppet-stack-config.pp index e12ca3cb7..80a6be52c 100644 --- a/elements/puppet-stack-config/puppet-stack-config.pp +++ b/elements/puppet-stack-config/puppet-stack-config.pp @@ -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 diff --git a/elements/puppet-stack-config/puppet-stack-config.yaml.template b/elements/puppet-stack-config/puppet-stack-config.yaml.template index 9c978b2b6..8a46521df 100644 --- a/elements/puppet-stack-config/puppet-stack-config.yaml.template +++ b/elements/puppet-stack-config/puppet-stack-config.yaml.template @@ -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')}" diff --git a/instack_undercloud/tests/test_undercloud.py b/instack_undercloud/tests/test_undercloud.py index 09a3183c4..a81337396 100644 --- a/instack_undercloud/tests/test_undercloud.py +++ b/instack_undercloud/tests/test_undercloud.py @@ -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) diff --git a/instack_undercloud/undercloud.py b/instack_undercloud/undercloud.py index 97566496b..7504d101f 100644 --- a/instack_undercloud/undercloud.py +++ b/instack_undercloud/undercloud.py @@ -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 diff --git a/scripts/instack-haproxy-cert-update b/scripts/instack-haproxy-cert-update new file mode 100644 index 000000000..705558a49 --- /dev/null +++ b/scripts/instack-haproxy-cert-update @@ -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 diff --git a/setup.cfg b/setup.cfg index 6df35bf33..a775b9dd7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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/* diff --git a/undercloud.conf.sample b/undercloud.conf.sample index 5c50ebbe0..953083076 100644 --- a/undercloud.conf.sample +++ b/undercloud.conf.sample @@ -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