diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index fb62354466..16fd1d9229 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -1320,6 +1320,26 @@ timeout_tcp_inspect-optional: min_version: 2.1 required: false type: integer +tls_container_ref: + description: | + The reference to the `key manager service + `__ secret containing a + PKCS12 format certificate/key bundle for ``tls_enabled`` pools for + TLS client authentication to the member servers. + in: body + min_version: 2.8 + required: true + type: string +tls_container_ref-optional: + description: | + The reference to the `key manager service + `__ secret containing a + PKCS12 format certificate/key bundle for ``tls_enabled`` pools for + TLS client authentication to the member servers. + in: body + min_version: 2.8 + required: false + type: string total_connections: description: | The total connections handled. diff --git a/api-ref/source/v2/examples/pool-create-curl b/api-ref/source/v2/examples/pool-create-curl index fe12f38383..a2a4fcf72b 100644 --- a/api-ref/source/v2/examples/pool-create-curl +++ b/api-ref/source/v2/examples/pool-create-curl @@ -1 +1 @@ -curl -X POST -H "Content-Type: application/json" -H "X-Auth-Token: " -d '{"pool":{"lb_algorithm":"ROUND_ROBIN","protocol":"HTTP","description":"Super Round Robin Pool","admin_state_up":true,"session_persistence":{"cookie_name":"ChocolateChip","type":"APP_COOKIE"},"listener_id":"023f2e34-7806-443b-bfae-16c324569a3d","name":"super-pool","tags":["test_tag"]}}' http://198.51.100.10:9876/v2/lbaas/pools +curl -X POST -H "Content-Type: application/json" -H "X-Auth-Token: " -d '{"pool":{"lb_algorithm":"ROUND_ROBIN","protocol":"HTTP","description":"Super Round Robin Pool","admin_state_up":true,"session_persistence":{"cookie_name":"ChocolateChip","type":"APP_COOKIE"},"listener_id":"023f2e34-7806-443b-bfae-16c324569a3d","name":"super-pool","tags":["test_tag"],"tls_container_ref":"http://198.51.100.10:9311/v1/containers/4073846f-1d5e-42e1-a4cf-a7046419d0e6"}}' http://198.51.100.10:9876/v2/lbaas/pools diff --git a/api-ref/source/v2/examples/pool-create-request.json b/api-ref/source/v2/examples/pool-create-request.json index 20f46575e5..8b2b7cb322 100644 --- a/api-ref/source/v2/examples/pool-create-request.json +++ b/api-ref/source/v2/examples/pool-create-request.json @@ -10,6 +10,7 @@ }, "listener_id": "023f2e34-7806-443b-bfae-16c324569a3d", "name": "super-pool", - "tags": ["test_tag"] + "tags": ["test_tag"], + "tls_container_ref": "http://198.51.100.10:9311/v1/containers/4073846f-1d5e-42e1-a4cf-a7046419d0e6" } } diff --git a/api-ref/source/v2/examples/pool-create-response.json b/api-ref/source/v2/examples/pool-create-response.json index 4ae94d2c4a..578c61630d 100644 --- a/api-ref/source/v2/examples/pool-create-response.json +++ b/api-ref/source/v2/examples/pool-create-response.json @@ -27,6 +27,7 @@ "id": "4029d267-3983-4224-a3d0-afb3fe16a2cd", "operating_status": "ONLINE", "name": "super-pool", - "tags": ["test_tag"] + "tags": ["test_tag"], + "tls_container_ref": "http://198.51.100.10:9311/v1/containers/4073846f-1d5e-42e1-a4cf-a7046419d0e6" } } diff --git a/api-ref/source/v2/examples/pool-show-response.json b/api-ref/source/v2/examples/pool-show-response.json index 4ae94d2c4a..578c61630d 100644 --- a/api-ref/source/v2/examples/pool-show-response.json +++ b/api-ref/source/v2/examples/pool-show-response.json @@ -27,6 +27,7 @@ "id": "4029d267-3983-4224-a3d0-afb3fe16a2cd", "operating_status": "ONLINE", "name": "super-pool", - "tags": ["test_tag"] + "tags": ["test_tag"], + "tls_container_ref": "http://198.51.100.10:9311/v1/containers/4073846f-1d5e-42e1-a4cf-a7046419d0e6" } } diff --git a/api-ref/source/v2/examples/pool-update-curl b/api-ref/source/v2/examples/pool-update-curl index 9468472770..7b9f195ad0 100644 --- a/api-ref/source/v2/examples/pool-update-curl +++ b/api-ref/source/v2/examples/pool-update-curl @@ -1 +1 @@ -curl -X PUT -H "Content-Type: application/json" -H "X-Auth-Token: " -d '{"pool":{"lb_algorithm":"LEAST_CONNECTIONS","session_persistence":{"type":"SOURCE_IP"},"description":"second description","name":"second_name","tags":["updated_tag"]}}' http://198.51.100.10:9876/v2/lbaas/pools/4029d267-3983-4224-a3d0-afb3fe16a2cd +curl -X PUT -H "Content-Type: application/json" -H "X-Auth-Token: " -d '{"pool":{"lb_algorithm":"LEAST_CONNECTIONS","session_persistence":{"type":"SOURCE_IP"},"description":"second description","name":"second_name","tags":["updated_tag"],"tls_container_ref":"http://198.51.100.10:9311/v1/containers/c1cd501d-3cf9-4873-a11b-a74bebcde929"}}' http://198.51.100.10:9876/v2/lbaas/pools/4029d267-3983-4224-a3d0-afb3fe16a2cd diff --git a/api-ref/source/v2/examples/pool-update-request.json b/api-ref/source/v2/examples/pool-update-request.json index 775de253ef..2fcdc2f5d3 100644 --- a/api-ref/source/v2/examples/pool-update-request.json +++ b/api-ref/source/v2/examples/pool-update-request.json @@ -6,6 +6,7 @@ }, "description": "Super Least Connections Pool", "name": "super-least-conn-pool", - "tags": ["updated_tag"] + "tags": ["updated_tag"], + "tls_container_ref": "http://198.51.100.10:9311/v1/containers/c1cd501d-3cf9-4873-a11b-a74bebcde929" } } diff --git a/api-ref/source/v2/examples/pool-update-response.json b/api-ref/source/v2/examples/pool-update-response.json index 1cf7e26d21..8f0a757482 100644 --- a/api-ref/source/v2/examples/pool-update-response.json +++ b/api-ref/source/v2/examples/pool-update-response.json @@ -27,6 +27,7 @@ "id": "4029d267-3983-4224-a3d0-afb3fe16a2cd", "operating_status": "ONLINE", "name": "super-least-conn-pool", - "tags": ["updated_tag"] + "tags": ["updated_tag"], + "tls_container_ref": "http://198.51.100.10:9311/v1/containers/c1cd501d-3cf9-4873-a11b-a74bebcde929" } } diff --git a/api-ref/source/v2/examples/pools-list-response.json b/api-ref/source/v2/examples/pools-list-response.json index b4d85f5815..a19eb6074f 100644 --- a/api-ref/source/v2/examples/pools-list-response.json +++ b/api-ref/source/v2/examples/pools-list-response.json @@ -33,7 +33,8 @@ "id": "ddb2b28f-89e9-45d3-a329-a359c3e39e4a", "operating_status": "ONLINE", "name": "round_robin_pool", - "tags": ["test_tag"] + "tags": ["test_tag"], + "tls_container_ref": "http://198.51.100.10:9311/v1/containers/4073846f-1d5e-42e1-a4cf-a7046419d0e6" } ] } diff --git a/api-ref/source/v2/pool.inc b/api-ref/source/v2/pool.inc index 31ced664e9..111898c5e0 100644 --- a/api-ref/source/v2/pool.inc +++ b/api-ref/source/v2/pool.inc @@ -61,6 +61,7 @@ Response Parameters - provisioning_status: provisioning_status - session_persistence: session_persistence - tags: tags + - tls_container_ref: tls_container_ref - updated_at: updated_at Response Example @@ -169,6 +170,7 @@ Request - protocol: protocol-pools - session_persistence: session_persistence-optional - tags: tags-optional + - tls_container_ref: tls_container_ref-optional .. _session_persistence: @@ -246,6 +248,7 @@ Response Parameters - provisioning_status: provisioning_status - session_persistence: session_persistence - tags: tags + - tls_container_ref: tls_container_ref - updated_at: updated_at Response Example @@ -313,6 +316,7 @@ Response Parameters - provisioning_status: provisioning_status - session_persistence: session_persistence - tags: tags + - tls_container_ref: tls_container_ref - updated_at: updated_at Response Example @@ -361,6 +365,7 @@ Request - pool_id: path-pool-id - session_persistence: session_persistence-optional - tags: tags-optional + - tls_container_ref: tls_container_ref-optional Request Example --------------- @@ -395,6 +400,7 @@ Response Parameters - provisioning_status: provisioning_status - session_persistence: session_persistence - tags: tags + - tls_container_ref: tls_container_ref - updated_at: updated_at Response Example diff --git a/doc/source/contributor/guides/providers.rst b/doc/source/contributor/guides/providers.rst index ea158e3b20..7e0b9a342b 100644 --- a/doc/source/contributor/guides/providers.rst +++ b/doc/source/contributor/guides/providers.rst @@ -676,6 +676,11 @@ contain the following: | | | {'type': 'APP_COOKIE', | | | | 'cookie_name': } | +-----------------------+--------+------------------------------------------+ +| tls_container_data | dict | A `TLS container`_ dict. | ++-----------------------+--------+------------------------------------------+ +| tls_container_ref | string | The reference to the secrets | +| | | container. | ++-----------------------+--------+------------------------------------------+ Delete ^^^^^^ @@ -724,6 +729,11 @@ contain the following: | | | {'type': 'APP_COOKIE', | | | | 'cookie_name': } | +-----------------------+--------+------------------------------------------+ +| tls_container_data | dict | A `TLS container`_ dict. | ++-----------------------+--------+------------------------------------------+ +| tls_container_ref | string | The reference to the secrets | +| | | container. | ++-----------------------+--------+------------------------------------------+ The pool will be in the ``PENDING_UPDATE`` provisioning_status when it is passed to the driver. The driver will update the provisioning_status of the diff --git a/octavia/amphorae/drivers/haproxy/rest_api_driver.py b/octavia/amphorae/drivers/haproxy/rest_api_driver.py index e682850176..b7441d9510 100644 --- a/octavia/amphorae/drivers/haproxy/rest_api_driver.py +++ b/octavia/amphorae/drivers/haproxy/rest_api_driver.py @@ -14,6 +14,7 @@ # under the License. import functools import hashlib +import os import time import warnings @@ -120,6 +121,7 @@ class HaproxyAmphoraLoadBalancerDriver( listener, listener.client_ca_tls_certificate_id) crl_filename = self._process_secret( listener, listener.client_crl_container_id) + pool_tls_certs = self._process_listener_pool_certs(listener) # Generate HaProxy configuration from listener object config = self.jinja.build_config( @@ -127,7 +129,8 @@ class HaproxyAmphoraLoadBalancerDriver( tls_cert=certs['tls_cert'], haproxy_versions=haproxy_versions, client_ca_filename=client_ca_filename, - client_crl=crl_filename) + client_crl=crl_filename, + pool_tls_certs=pool_tls_certs) self.client.upload_config(amp, listener.id, config, timeout_dict=timeout_dict) self.client.reload_listener(amp, listener.id, @@ -161,6 +164,7 @@ class HaproxyAmphoraLoadBalancerDriver( listener, listener.client_ca_tls_certificate_id) crl_filename = self._process_secret( listener, listener.client_crl_container_id) + pool_tls_certs = self._process_listener_pool_certs(listener) for amp in listener.load_balancer.amphorae: if amp.status != consts.DELETED: @@ -173,7 +177,8 @@ class HaproxyAmphoraLoadBalancerDriver( tls_cert=certs['tls_cert'], haproxy_versions=haproxy_versions, client_ca_filename=client_ca_filename, - client_crl=crl_filename) + client_crl=crl_filename, + pool_tls_certs=pool_tls_certs) self.client.upload_config(amp, listener.id, config) self.client.reload_listener(amp, listener.id) @@ -301,12 +306,51 @@ class HaproxyAmphoraLoadBalancerDriver( return None context = oslo_context.RequestContext(project_id=listener.project_id) secret = self.cert_manager.get_secret(context, secret_ref) - md5 = hashlib.md5(secret.encode('utf-8')).hexdigest() # nosec - id = hashlib.sha1(secret.encode('utf-8')).hexdigest() # nosec + try: + secret = secret.encode('utf-8') + except AttributeError: + pass + md5 = hashlib.md5(secret).hexdigest() # nosec + id = hashlib.sha1(secret).hexdigest() # nosec name = '{id}.pem'.format(id=id) self._apply(self._upload_cert, listener, None, secret, md5, name) return name + def _process_listener_pool_certs(self, listener): + # {'POOL-ID': { + # 'client_cert': client_full_filename, + # 'ca_cert': ca_cert_full_filename, + # 'crl': crl_full_filename}} + pool_certs_dict = dict() + for pool in listener.pools: + if pool.id not in pool_certs_dict: + pool_certs_dict[pool.id] = self._process_pool_certs(listener, + pool) + for l7policy in listener.l7policies: + if (l7policy.redirect_pool and + l7policy.redirect_pool.id not in pool_certs_dict): + pool_certs_dict[l7policy.redirect_pool.id] = ( + self._process_pool_certs(listener, l7policy.redirect_pool)) + return pool_certs_dict + + def _process_pool_certs(self, listener, pool): + pool_cert_dict = dict() + + # Handle the cleint cert(s) and key + if pool.tls_certificate_id: + data = cert_parser.load_certificates_data(self.cert_manager, pool) + pem = cert_parser.build_pem(data) + try: + pem = pem.encode('utf-8') + except AttributeError: + pass + md5 = hashlib.md5(pem).hexdigest() # nosec + name = '{id}.pem'.format(id=data.id) + self._apply(self._upload_cert, listener, None, pem, md5, name) + pool_cert_dict['client_cert'] = os.path.join( + CONF.haproxy_amphora.base_cert_dir, listener.id, name) + return pool_cert_dict + def _upload_cert(self, amp, listener_id, pem, md5, name): try: if self.client.get_cert_md5sum( @@ -315,8 +359,7 @@ class HaproxyAmphoraLoadBalancerDriver( except exc.NotFound: pass - self.client.upload_cert_pem( - amp, listener_id, name, pem) + self.client.upload_cert_pem(amp, listener_id, name, pem) def update_amphora_agent_config(self, amphora, agent_config, timeout_dict=None): @@ -488,24 +531,29 @@ class AmphoraAPIClient(object): def upload_cert_pem(self, amp, listener_id, pem_filename, pem_file): r = self.put( - amp, - 'listeners/{listener_id}/certificates/{filename}'.format( + amp, 'listeners/{listener_id}/certificates/{filename}'.format( listener_id=listener_id, filename=pem_filename), data=pem_file) return exc.check_exception(r) - def update_cert_for_rotation(self, amp, pem_file): - r = self.put(amp, 'certificate', data=pem_file) - return exc.check_exception(r) - def get_cert_md5sum(self, amp, listener_id, pem_filename, ignore=tuple()): - r = self.get(amp, - 'listeners/{listener_id}/certificates/{filename}'.format( - listener_id=listener_id, filename=pem_filename)) + r = self.get( + amp, 'listeners/{listener_id}/certificates/{filename}'.format( + listener_id=listener_id, filename=pem_filename)) if exc.check_exception(r, ignore): return r.json().get("md5sum") return None + def delete_cert_pem(self, amp, listener_id, pem_filename): + r = self.delete( + amp, 'listeners/{listener_id}/certificates/{filename}'.format( + listener_id=listener_id, filename=pem_filename)) + return exc.check_exception(r, (404,)) + + def update_cert_for_rotation(self, amp, pem_file): + r = self.put(amp, 'certificate', data=pem_file) + return exc.check_exception(r) + def delete_listener(self, amp, listener_id): r = self.delete( amp, 'listeners/{listener_id}'.format(listener_id=listener_id)) @@ -529,13 +577,6 @@ class AmphoraAPIClient(object): return r.json() return None - def delete_cert_pem(self, amp, listener_id, pem_filename): - r = self.delete( - amp, - 'listeners/{listener_id}/certificates/{filename}'.format( - listener_id=listener_id, filename=pem_filename)) - return exc.check_exception(r, (404,)) - def plug_network(self, amp, port): r = self.post(amp, 'plug/network', json=port) diff --git a/octavia/api/drivers/amphora_driver/driver.py b/octavia/api/drivers/amphora_driver/driver.py index c029e04bd6..5b1953bfb0 100644 --- a/octavia/api/drivers/amphora_driver/driver.py +++ b/octavia/api/drivers/amphora_driver/driver.py @@ -135,7 +135,9 @@ class AmphoraProviderDriver(driver_base.ProviderDriver): if 'admin_state_up' in pool_dict: pool_dict['enabled'] = pool_dict.pop('admin_state_up') pool_id = pool_dict.pop('pool_id') - + if 'tls_container_ref' in pool_dict: + pool_dict['tls_container_id'] = pool_dict.pop('tls_container_ref') + pool_dict.pop('tls_container_data', None) payload = {consts.POOL_ID: pool_id, consts.POOL_UPDATES: pool_dict} self.client.cast({}, 'update_pool', **payload) diff --git a/octavia/api/drivers/data_models.py b/octavia/api/drivers/data_models.py index b7fb4d49c5..ef95500528 100644 --- a/octavia/api/drivers/data_models.py +++ b/octavia/api/drivers/data_models.py @@ -170,7 +170,8 @@ class Pool(BaseDataModel): healthmonitor=Unset, lb_algorithm=Unset, loadbalancer_id=Unset, members=Unset, name=Unset, pool_id=Unset, listener_id=Unset, protocol=Unset, - session_persistence=Unset): + session_persistence=Unset, tls_container_ref=Unset, + tls_container_data=Unset): self.admin_state_up = admin_state_up self.description = description @@ -183,6 +184,8 @@ class Pool(BaseDataModel): self.listener_id = listener_id self.protocol = protocol self.session_persistence = session_persistence + self.tls_container_ref = tls_container_ref + self.tls_container_data = tls_container_data class Member(BaseDataModel): diff --git a/octavia/api/drivers/utils.py b/octavia/api/drivers/utils.py index 779c173757..2a2865b8a6 100644 --- a/octavia/api/drivers/utils.py +++ b/octavia/api/drivers/utils.py @@ -281,6 +281,25 @@ def db_pool_to_provider_pool(db_pool): def pool_dict_to_provider_dict(pool_dict): new_pool_dict = _base_to_provider_dict(pool_dict) new_pool_dict['pool_id'] = new_pool_dict.pop('id') + + # Pull the certs out of the certificate manager to pass to the provider + if 'tls_certificate_id' in new_pool_dict: + new_pool_dict['tls_container_ref'] = new_pool_dict.pop( + 'tls_certificate_id') + + pool_obj = data_models.Pool(**pool_dict) + if pool_obj.tls_certificate_id: + cert_manager = stevedore_driver.DriverManager( + namespace='octavia.cert_manager', + name=CONF.certificates.cert_manager, + invoke_on_load=True, + ).driver + cert_dict = cert_parser.load_certificates_data(cert_manager, + pool_obj) + if 'tls_cert' in cert_dict and cert_dict['tls_cert']: + new_pool_dict['tls_container_data'] = ( + cert_dict['tls_cert'].to_dict()) + # Remove the DB back references if ('session_persistence' in new_pool_dict and new_pool_dict['session_persistence']): diff --git a/octavia/api/v2/controllers/pool.py b/octavia/api/v2/controllers/pool.py index 2866046676..9b5b75515c 100644 --- a/octavia/api/v2/controllers/pool.py +++ b/octavia/api/v2/controllers/pool.py @@ -18,6 +18,7 @@ from oslo_db import exception as odb_exceptions from oslo_log import log as logging from oslo_utils import excutils import pecan +from stevedore import driver as stevedore_driver from wsme import types as wtypes from wsmeext import pecan as wsme_pecan @@ -46,6 +47,11 @@ class PoolsController(base.BaseController): def __init__(self): super(PoolsController, self).__init__() + self.cert_manager = stevedore_driver.DriverManager( + namespace='octavia.cert_manager', + name=CONF.certificates.cert_manager, + invoke_on_load=True, + ).driver @wsme_pecan.wsexpose(pool_types.PoolRootResponse, wtypes.text, [wtypes.text], ignore_extra_args=True) @@ -98,6 +104,19 @@ class PoolsController(base.BaseController): raise exceptions.ImmutableObject(resource=_('Load Balancer'), id=lb_id) + def _validate_tls_refs(self, tls_refs): + context = pecan.request.context.get('octavia_context') + bad_refs = [] + for ref in tls_refs: + try: + self.cert_manager.set_acls(context, ref) + self.cert_manager.get_cert(context, ref, check_only=True) + except Exception: + bad_refs.append(ref) + + if bad_refs: + raise exceptions.CertificateRetrievalException(ref=bad_refs) + def _validate_create_pool(self, lock_session, pool_dict, listener_id=None): """Validate creating pool on load balancer. @@ -105,6 +124,9 @@ class PoolsController(base.BaseController): provisioning status. """ try: + tls_certificate_id = pool_dict.get('tls_certificate_id', None) + tls_refs = [tls_certificate_id] if tls_certificate_id else [] + self._validate_tls_refs(tls_refs) return self.repositories.create_pool_on_load_balancer( lock_session, pool_dict, listener_id=listener_id) @@ -327,6 +349,9 @@ class PoolsController(base.BaseController): sp_dict = pool.session_persistence.to_dict(render_unsets=False) validate.check_session_persistence(sp_dict) + if pool.tls_container_ref: + self._validate_tls_refs([pool.tls_container_ref]) + # Load the driver early as it also provides validation driver = driver_factory.get_driver(provider) diff --git a/octavia/api/v2/types/pool.py b/octavia/api/v2/types/pool.py index a5fdeec8e7..d6cd5c95da 100644 --- a/octavia/api/v2/types/pool.py +++ b/octavia/api/v2/types/pool.py @@ -52,7 +52,8 @@ class SessionPersistencePUT(types.BaseType): class BasePoolType(types.BaseType): _type_to_model_map = {'admin_state_up': 'enabled', 'healthmonitor': 'health_monitor', - 'healthmonitor_id': 'health_monitor.id'} + 'healthmonitor_id': 'health_monitor.id', + 'tls_container_ref': 'tls_certificate_id'} _child_map = {'health_monitor': {'id': 'healthmonitor_id'}} @@ -76,6 +77,7 @@ class PoolResponse(BasePoolType): healthmonitor_id = wtypes.wsattr(wtypes.UuidType()) members = wtypes.wsattr([types.IdOnlyType]) tags = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType())) + tls_container_ref = wtypes.wsattr(wtypes.StringType()) @classmethod def from_data_model(cls, data_model, children=False): @@ -147,6 +149,8 @@ class PoolPOST(BasePoolType): healthmonitor = wtypes.wsattr(health_monitor.HealthMonitorSingleCreate) members = wtypes.wsattr([member.MemberSingleCreate]) tags = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType(max_length=255))) + tls_container_ref = wtypes.wsattr( + wtypes.StringType(max_length=255)) class PoolRootPOST(types.BaseType): @@ -162,6 +166,7 @@ class PoolPUT(BasePoolType): wtypes.Enum(str, *constants.SUPPORTED_LB_ALGORITHMS)) session_persistence = wtypes.wsattr(SessionPersistencePUT) tags = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType(max_length=255))) + tls_container_ref = wtypes.wsattr(wtypes.StringType(max_length=255)) class PoolRootPut(types.BaseType): @@ -180,6 +185,7 @@ class PoolSingleCreate(BasePoolType): healthmonitor = wtypes.wsattr(health_monitor.HealthMonitorSingleCreate) members = wtypes.wsattr([member.MemberSingleCreate]) tags = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType(max_length=255))) + tls_container_ref = wtypes.wsattr(wtypes.StringType(max_length=255)) class PoolStatusResponse(BasePoolType): diff --git a/octavia/common/data_models.py b/octavia/common/data_models.py index 47b5cc5558..602700a08f 100644 --- a/octavia/common/data_models.py +++ b/octavia/common/data_models.py @@ -263,7 +263,7 @@ class Pool(BaseDataModel): session_persistence=None, load_balancer_id=None, load_balancer=None, listeners=None, l7policies=None, created_at=None, updated_at=None, provisioning_status=None, - tags=None): + tags=None, tls_certificate_id=None): self.id = id self.project_id = project_id self.name = name @@ -283,6 +283,7 @@ class Pool(BaseDataModel): self.updated_at = updated_at self.provisioning_status = provisioning_status self.tags = tags + self.tls_certificate_id = tls_certificate_id def update(self, update_dict): for key, value in update_dict.items(): diff --git a/octavia/common/jinja/haproxy/jinja_cfg.py b/octavia/common/jinja/haproxy/jinja_cfg.py index b239506f0b..9b3a026a47 100644 --- a/octavia/common/jinja/haproxy/jinja_cfg.py +++ b/octavia/common/jinja/haproxy/jinja_cfg.py @@ -83,7 +83,8 @@ class JinjaTemplater(object): def build_config(self, host_amphora, listener, tls_cert, haproxy_versions, socket_path=None, - client_ca_filename=None, client_crl=None): + client_ca_filename=None, client_crl=None, + pool_tls_certs=None): """Convert a logical configuration to the HAProxy version :param host_amphora: The Amphora this configuration is hosted on @@ -105,7 +106,8 @@ class JinjaTemplater(object): return self.render_loadbalancer_obj( host_amphora, listener, tls_cert=tls_cert, socket_path=socket_path, feature_compatibility=feature_compatibility, - client_ca_filename=client_ca_filename, client_crl=client_crl) + client_ca_filename=client_ca_filename, client_crl=client_crl, + pool_tls_certs=pool_tls_certs) def _get_template(self): """Returns the specified Jinja configuration template.""" @@ -124,7 +126,8 @@ class JinjaTemplater(object): def render_loadbalancer_obj(self, host_amphora, listener, tls_cert=None, socket_path=None, feature_compatibility=None, - client_ca_filename=None, client_crl=None): + client_ca_filename=None, client_crl=None, + pool_tls_certs=None): """Renders a templated configuration from a load balancer object :param host_amphora: The Amphora this configuration is hosted on @@ -142,7 +145,8 @@ class JinjaTemplater(object): tls_cert, feature_compatibility, client_ca_filename=client_ca_filename, - client_crl=client_crl) + client_crl=client_crl, + pool_tls_certs=pool_tls_certs) if not socket_path: socket_path = '%s/%s.sock' % (self.base_amp_path, listener.id) return self._get_template().render( @@ -155,14 +159,16 @@ class JinjaTemplater(object): def _transform_loadbalancer(self, host_amphora, loadbalancer, listener, tls_cert, feature_compatibility, - client_ca_filename=None, client_crl=None): + client_ca_filename=None, client_crl=None, + pool_tls_certs=None): """Transforms a load balancer into an object that will be processed by the templating system """ t_listener = self._transform_listener( listener, tls_cert, feature_compatibility, - client_ca_filename=client_ca_filename, client_crl=client_crl) + client_ca_filename=client_ca_filename, client_crl=client_crl, + pool_tls_certs=pool_tls_certs) ret_value = { 'id': loadbalancer.id, 'vip_address': loadbalancer.vip.ip_address, @@ -202,7 +208,8 @@ class JinjaTemplater(object): } def _transform_listener(self, listener, tls_cert, feature_compatibility, - client_ca_filename=None, client_crl=None): + client_ca_filename=None, client_crl=None, + pool_tls_certs=None): """Transforms a listener into an object that will be processed by the templating system @@ -251,17 +258,28 @@ class JinjaTemplater(object): os.path.join(self.base_crt_dir, listener.id, client_crl)) if listener.default_pool: + kwargs = {} + if pool_tls_certs and pool_tls_certs.get(listener.default_pool.id): + kwargs = {'pool_tls_certs': pool_tls_certs.get( + listener.default_pool.id)} ret_value['default_pool'] = self._transform_pool( - listener.default_pool, feature_compatibility) - pools = [self._transform_pool(x, feature_compatibility) - for x in listener.pools] + listener.default_pool, feature_compatibility, **kwargs) + pools = [] + for x in listener.pools: + kwargs = {} + if pool_tls_certs and pool_tls_certs.get(x.id): + kwargs = {'pool_tls_certs': pool_tls_certs.get(x.id)} + pools.append(self._transform_pool( + x, feature_compatibility, **kwargs)) ret_value['pools'] = pools - l7policies = [self._transform_l7policy(x, feature_compatibility) + l7policies = [self._transform_l7policy( + x, feature_compatibility, pool_tls_certs) for x in listener.l7policies] ret_value['l7policies'] = l7policies return ret_value - def _transform_pool(self, pool, feature_compatibility): + def _transform_pool(self, pool, feature_compatibility, + pool_tls_certs=None): """Transforms a pool into an object that will be processed by the templating system @@ -289,6 +307,10 @@ class JinjaTemplater(object): ret_value[ 'session_persistence'] = self._transform_session_persistence( pool.session_persistence, feature_compatibility) + if (pool.tls_certificate_id and pool_tls_certs and + pool_tls_certs.get('client_cert')): + ret_value['client_cert'] = pool_tls_certs.get('client_cert') + return ret_value @staticmethod @@ -343,7 +365,8 @@ class JinjaTemplater(object): 'enabled': monitor.enabled, } - def _transform_l7policy(self, l7policy, feature_compatibility): + def _transform_l7policy(self, l7policy, feature_compatibility, + pool_tls_certs=None): """Transforms an L7 policy into an object that will be processed by the templating system @@ -356,8 +379,13 @@ class JinjaTemplater(object): 'enabled': l7policy.enabled } if l7policy.redirect_pool: + kwargs = {} + if pool_tls_certs and pool_tls_certs.get( + l7policy.redirect_pool.id): + kwargs = {'pool_tls_certs': + pool_tls_certs.get(l7policy.redirect_pool.id)} ret_value['redirect_pool'] = self._transform_pool( - l7policy.redirect_pool, feature_compatibility) + l7policy.redirect_pool, feature_compatibility, **kwargs) else: ret_value['redirect_pool'] = None l7rules = [self._transform_l7rule(x, feature_compatibility) diff --git a/octavia/common/jinja/haproxy/templates/macros.j2 b/octavia/common/jinja/haproxy/templates/macros.j2 index e84d3a6f58..ae46c4b0db 100644 --- a/octavia/common/jinja/haproxy/templates/macros.j2 +++ b/octavia/common/jinja/haproxy/templates/macros.j2 @@ -210,10 +210,19 @@ frontend {{ listener.id }} {% else %} {% set member_enabled_opt = " disabled" %} {% endif %} - {{ "server %s %s:%d weight %s%s%s%s%s%s"|e|format( + {% if pool.client_cert %} + {% set def_opt_prefix = " ssl" %} + {% set def_crt_opt = " crt %s"|format(pool.client_cert) %} + {% set def_verify_opt = " verify none" %} + {% else %} + {% set def_opt_prefix = "" %} + {% set def_crt_opt = "" %} + {% set def_verify_opt = "" %} + {% endif %} + {{ "server %s %s:%d weight %s%s%s%s%s%s%s%s%s"|e|format( member.id, member.address, member.protocol_port, member.weight, hm_opt, persistence_opt, proxy_protocol_opt, member_backup_opt, - member_enabled_opt)|trim() }} + member_enabled_opt, def_opt_prefix, def_crt_opt, def_verify_opt)|trim() }} {% endmacro %} diff --git a/octavia/common/tls_utils/cert_parser.py b/octavia/common/tls_utils/cert_parser.py index ac99f689e0..3e5e7c5399 100644 --- a/octavia/common/tls_utils/cert_parser.py +++ b/octavia/common/tls_utils/cert_parser.py @@ -336,23 +336,23 @@ def build_pem(tls_container): return b'\n'.join(pem) + b'\n' -def load_certificates_data(cert_mngr, listener, context=None): - """Load TLS certificate data from the listener. +def load_certificates_data(cert_mngr, obj, context=None): + """Load TLS certificate data from the listener/pool. return TLS_CERT and SNI_CERTS """ tls_cert = None sni_certs = [] if not context: - context = oslo_context.RequestContext(project_id=listener.project_id) + context = oslo_context.RequestContext(project_id=obj.project_id) - if listener.tls_certificate_id: + if obj.tls_certificate_id: tls_cert = _map_cert_tls_container( cert_mngr.get_cert(context, - listener.tls_certificate_id, + obj.tls_certificate_id, check_only=True)) - if listener.sni_containers: - for sni_cont in listener.sni_containers: + if hasattr(obj, 'sni_containers') and obj.sni_containers: + for sni_cont in obj.sni_containers: cert_container = _map_cert_tls_container( cert_mngr.get_cert(context, sni_cont.tls_container_id, diff --git a/octavia/db/migration/alembic_migrations/versions/a1f689aecc1d_extend_pool_for_support_backend_reencryption.py b/octavia/db/migration/alembic_migrations/versions/a1f689aecc1d_extend_pool_for_support_backend_reencryption.py new file mode 100644 index 0000000000..f19934a85c --- /dev/null +++ b/octavia/db/migration/alembic_migrations/versions/a1f689aecc1d_extend_pool_for_support_backend_reencryption.py @@ -0,0 +1,35 @@ +# Copyright 2018 Huawei +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +"""Extend pool for support backend re-encryption + +Revision ID: a1f689aecc1d +Revises: 1afc932f1ca2 +Create Date: 2018-10-23 20:47:52.405865 + +""" + + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = 'a1f689aecc1d' +down_revision = '1afc932f1ca2' + + +def upgrade(): + op.add_column(u'pool', sa.Column(u'tls_certificate_id', sa.String(255), + nullable=True)) diff --git a/octavia/db/models.py b/octavia/db/models.py index c0f07f237e..0b68a6d219 100644 --- a/octavia/db/models.py +++ b/octavia/db/models.py @@ -328,6 +328,7 @@ class Pool(base_models.BASE, base_models.IdMixin, base_models.ProjectMixin, cascade='all,delete-orphan', primaryjoin='and_(foreign(Tags.resource_id)==Pool.id)' ) + tls_certificate_id = sa.Column(sa.String(255), nullable=True) # This property should be a unique list of any listeners that reference # this pool as its default_pool and any listeners referenced by enabled @@ -544,7 +545,6 @@ class SNI(base_models.BASE): __table_args__ = ( sa.PrimaryKeyConstraint('listener_id', 'tls_container_id'), ) - listener_id = sa.Column( sa.String(36), sa.ForeignKey("listener.id", name="fk_sni_listener_id"), diff --git a/octavia/tests/functional/api/v2/test_pool.py b/octavia/tests/functional/api/v2/test_pool.py index b5065686e6..3cdeb34d7d 100644 --- a/octavia/tests/functional/api/v2/test_pool.py +++ b/octavia/tests/functional/api/v2/test_pool.py @@ -858,6 +858,43 @@ class TestPool(base.BaseAPITest): pool_prov_status=constants.PENDING_CREATE, pool_op_status=constants.OFFLINE) + @mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data') + def test_create_with_tls_container_ref(self, mock_cert_data): + tls_container_ref = uuidutils.generate_uuid() + pool_cert = data_models.TLSContainer(certificate='pool cert') + mock_cert_data.return_value = {'tls_cert': pool_cert, + 'sni_certs': [], + 'client_ca_cert': None} + api_pool = self.create_pool( + self.lb_id, + constants.PROTOCOL_HTTP, + constants.LB_ALGORITHM_ROUND_ROBIN, + listener_id=self.listener_id, + tls_container_ref=tls_container_ref).get(self.root_tag) + self.assert_correct_status( + lb_id=self.lb_id, listener_id=self.listener_id, + pool_id=api_pool.get('id'), + lb_prov_status=constants.PENDING_UPDATE, + listener_prov_status=constants.PENDING_UPDATE, + pool_prov_status=constants.PENDING_CREATE, + pool_op_status=constants.OFFLINE) + self.set_lb_status(self.lb_id) + self.assertEqual(tls_container_ref, api_pool.get('tls_container_ref')) + self.assert_correct_status( + lb_id=self.lb_id, listener_id=self.listener_id, + pool_id=api_pool.get('id')) + + def test_create_with_bad_tls_container_ref(self): + tls_container_ref = uuidutils.generate_uuid() + self.cert_manager_mock().get_cert.side_effect = [Exception( + "bad secret")] + api_pool = self.create_pool( + self.lb_id, constants.PROTOCOL_HTTP, + constants.LB_ALGORITHM_ROUND_ROBIN, + listener_id=self.listener_id, + tls_container_ref=tls_container_ref, status=400) + self.assertIn(tls_container_ref, api_pool['faultstring']) + def test_negative_create_udp_case(self): # Error create pool with udp protocol but non-udp-type sp = {"type": constants.SESSION_PERSISTENCE_HTTP_COOKIE, @@ -1205,6 +1242,39 @@ class TestPool(base.BaseAPITest): self.assert_correct_status( lb_id=self.udp_lb_id, listener_id=self.udp_listener_id) + @mock.patch( + 'octavia.common.tls_utils.cert_parser.load_certificates_data') + def test_update_with_tls_container_ref(self, mock_cert_data): + tls_container_ref = uuidutils.generate_uuid() + api_pool = self.create_pool( + self.lb_id, + constants.PROTOCOL_HTTP, + constants.LB_ALGORITHM_ROUND_ROBIN, + listener_id=self.listener_id).get(self.root_tag) + self.set_lb_status(lb_id=self.lb_id) + new_pool = {'tls_container_ref': tls_container_ref} + pool_cert = data_models.TLSContainer(certificate='pool cert') + mock_cert_data.return_value = {'tls_cert': pool_cert, + 'sni_certs': [], + 'client_ca_cert': None} + self.put(self.POOL_PATH.format(pool_id=api_pool.get('id')), + self._build_body(new_pool)) + self.assert_correct_status( + lb_id=self.lb_id, listener_id=self.listener_id, + pool_id=api_pool.get('id'), + lb_prov_status=constants.PENDING_UPDATE, + listener_prov_status=constants.PENDING_UPDATE, + pool_prov_status=constants.PENDING_UPDATE) + self.set_lb_status(self.lb_id) + response = self.get(self.POOL_PATH.format( + pool_id=api_pool.get('id'))).json.get(self.root_tag) + self.assertEqual(tls_container_ref, response.get('tls_container_ref')) + self.assertIsNotNone(response.get('created_at')) + self.assertIsNotNone(response.get('updated_at')) + self.assert_correct_status( + lb_id=self.lb_id, listener_id=self.listener_id, + pool_id=response.get('id')) + def test_bad_update(self): api_pool = self.create_pool( self.lb_id, @@ -1256,6 +1326,22 @@ class TestPool(base.BaseAPITest): self.assert_correct_status( lb_id=self.udp_lb_id, listener_id=self.udp_listener_id) + def test_update_with_bad_tls_container_ref(self): + api_pool = self.create_pool( + self.lb_id, + constants.PROTOCOL_HTTP, + constants.LB_ALGORITHM_ROUND_ROBIN, + listener_id=self.listener_id).get(self.root_tag) + self.set_lb_status(lb_id=self.lb_id) + tls_container_ref = uuidutils.generate_uuid() + new_pool = {'tls_container_ref': tls_container_ref} + + self.cert_manager_mock().get_cert.side_effect = [Exception( + "bad secret")] + resp = self.put(self.POOL_PATH.format(pool_id=api_pool.get('id')), + self._build_body(new_pool), status=400).json + self.assertIn(tls_container_ref, resp['faultstring']) + def test_delete(self): api_pool = self.create_pool( self.lb_id, diff --git a/octavia/tests/functional/db/test_repositories.py b/octavia/tests/functional/db/test_repositories.py index 2a5ed91221..a4bb948fd9 100644 --- a/octavia/tests/functional/db/test_repositories.py +++ b/octavia/tests/functional/db/test_repositories.py @@ -179,7 +179,8 @@ class AllRepositoriesTest(base.OctaviaDBTestBase): 'project_id': uuidutils.generate_uuid(), 'id': uuidutils.generate_uuid(), 'provisioning_status': constants.ACTIVE, - 'tags': ['test_tag']} + 'tags': ['test_tag'], + 'tls_certificate_id': uuidutils.generate_uuid()} pool_dm = self.repos.create_pool_on_load_balancer( self.session, pool, listener_id=self.listener.id) pool_dm_dict = pool_dm.to_dict() @@ -205,7 +206,8 @@ class AllRepositoriesTest(base.OctaviaDBTestBase): 'project_id': uuidutils.generate_uuid(), 'id': uuidutils.generate_uuid(), 'provisioning_status': constants.ACTIVE, - 'tags': ['test_tag']} + 'tags': ['test_tag'], + 'tls_certificate_id': uuidutils.generate_uuid()} sp = {'type': constants.SESSION_PERSISTENCE_HTTP_COOKIE, 'cookie_name': 'cookie_monster', 'pool_id': pool['id'], @@ -261,6 +263,7 @@ class AllRepositoriesTest(base.OctaviaDBTestBase): del pool_dm_dict['created_at'] del pool_dm_dict['updated_at'] pool.update(update_pool) + pool['tls_certificate_id'] = None self.assertEqual(pool, pool_dm_dict) self.assertIsNone(new_pool_dm.session_persistence) @@ -272,7 +275,8 @@ class AllRepositoriesTest(base.OctaviaDBTestBase): 'project_id': uuidutils.generate_uuid(), 'id': uuidutils.generate_uuid(), 'provisioning_status': constants.ACTIVE, - 'tags': ['test_tag']} + 'tags': ['test_tag'], + 'tls_certificate_id': uuidutils.generate_uuid()} sp = {'type': constants.SESSION_PERSISTENCE_HTTP_COOKIE, 'cookie_name': 'cookie_monster', 'pool_id': pool['id'], @@ -364,6 +368,33 @@ class AllRepositoriesTest(base.OctaviaDBTestBase): self.session, pool_dm.id, update_pool) self.assertIsNone(new_pool_dm.session_persistence) + def test_update_pool_with_cert(self): + pool = {'protocol': constants.PROTOCOL_HTTP, 'name': 'pool1', + 'description': 'desc1', + 'lb_algorithm': constants.LB_ALGORITHM_ROUND_ROBIN, + 'enabled': True, 'operating_status': constants.ONLINE, + 'project_id': uuidutils.generate_uuid(), + 'id': uuidutils.generate_uuid(), + 'provisioning_status': constants.ACTIVE} + pool_dm = self.repos.create_pool_on_load_balancer( + self.session, pool, listener_id=self.listener.id) + update_pool = {'tls_certificate_id': uuidutils.generate_uuid()} + new_pool_dm = self.repos.update_pool_and_sp( + self.session, pool_dm.id, update_pool) + pool_dm_dict = new_pool_dm.to_dict() + del pool_dm_dict['members'] + del pool_dm_dict['health_monitor'] + del pool_dm_dict['session_persistence'] + del pool_dm_dict['listeners'] + del pool_dm_dict['load_balancer'] + del pool_dm_dict['load_balancer_id'] + del pool_dm_dict['l7policies'] + del pool_dm_dict['created_at'] + del pool_dm_dict['updated_at'] + del pool_dm_dict['tags'] + pool.update(update_pool) + self.assertEqual(pool, pool_dm_dict) + def test_create_load_balancer_tree(self): project_id = uuidutils.generate_uuid() member = {'project_id': project_id, 'ip_address': '11.0.0.1', diff --git a/octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver.py b/octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver.py index d1b24a57c3..ae4040542b 100644 --- a/octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver.py +++ b/octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver.py @@ -73,6 +73,8 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): persistence_timeout=33, persistence_granularity='255.255.0.0', monitor_proto=constants.HEALTH_MONITOR_UDP_CONNECT) + self.pool_has_cert = sample_configs.sample_pool_tuple( + pool_cert=True, full_store=True) self.amp = self.sl.load_balancer.amphorae[0] self.sv = sample_configs.sample_vip_tuple() self.lb = self.sl.load_balancer @@ -152,11 +154,9 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): sconts = [] for sni_container in self.sl.sni_containers: sconts.append(sni_container.tls_container) - mock_load_crt.return_value = { - 'tls_cert': self.sl.default_tls_container, - 'sni_certs': sconts, - 'client_ca_cert': self.sl.client_ca_tls_certificate - } + mock_load_crt.side_effect = [{ + 'tls_cert': self.sl.default_tls_container, 'sni_certs': sconts}, + {'tls_cert': None, 'sni_certs': []}] self.driver.client.get_cert_md5sum.side_effect = [ exc.NotFound, 'Fake_MD5', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'CA_CERT_MD5'] @@ -243,8 +243,7 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): sconts.append(sni_container.tls_container) mock_load_crt.return_value = { 'tls_cert': self.sl.default_tls_container, - 'sni_certs': sconts, - 'client_ca_cert': None + 'sni_certs': sconts } self.driver.client.get_cert_md5sum.side_effect = [ exc.NotFound, 'Fake_MD5', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'] @@ -298,12 +297,12 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): sample_listener = sample_configs.sample_listener_tuple( tls=True, sni=True, client_ca_cert=True) fake_context = 'fake context' - fake_secret = 'fake cert' + fake_secret = b'fake cert' mock_oslo.return_value = fake_context self.driver.cert_manager.get_secret.reset_mock() self.driver.cert_manager.get_secret.return_value = fake_secret - ref_md5 = hashlib.md5(fake_secret.encode('utf-8')).hexdigest() # nosec - ref_id = hashlib.sha1(fake_secret.encode('utf-8')).hexdigest() # nosec + ref_md5 = hashlib.md5(fake_secret).hexdigest() # nosec + ref_id = hashlib.sha1(fake_secret).hexdigest() # nosec ref_name = '{id}.pem'.format(id=ref_id) result = self.driver._process_secret( @@ -318,6 +317,60 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): ref_md5, ref_name) self.assertEqual(ref_name, result) + @mock.patch('octavia.amphorae.drivers.haproxy.rest_api_driver.' + 'HaproxyAmphoraLoadBalancerDriver._process_pool_certs') + def test__process_listener_pool_certs(self, mock_pool_cert): + sample_listener = sample_configs.sample_listener_tuple(l7=True) + + ref_pool_cert_1 = {'client_cert': '/some/fake/cert-1.pem'} + ref_pool_cert_2 = {'client_cert': '/some/fake/cert-2.pem'} + + mock_pool_cert.side_effect = [ref_pool_cert_1, ref_pool_cert_2] + + ref_cert_dict = {'sample_pool_id_1': ref_pool_cert_1, + 'sample_pool_id_2': ref_pool_cert_2} + + result = self.driver._process_listener_pool_certs(sample_listener) + + pool_certs_calls = [ + mock.call(sample_listener, sample_listener.default_pool), + mock.call(sample_listener, sample_listener.pools[1]) + ] + + mock_pool_cert.assert_has_calls(pool_certs_calls, any_order=True) + + self.assertEqual(ref_cert_dict, result) + + @mock.patch('octavia.amphorae.drivers.haproxy.rest_api_driver.' + 'HaproxyAmphoraLoadBalancerDriver._apply') + @mock.patch('octavia.common.tls_utils.cert_parser.build_pem') + @mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data') + def test__process_pool_certs(self, mock_load_certs, mock_build_pem, + mock_apply): + fake_cert_dir = '/fake/cert/dir' + conf = oslo_fixture.Config(cfg.CONF) + conf.config(group="haproxy_amphora", base_cert_dir=fake_cert_dir) + sample_listener = sample_configs.sample_listener_tuple(pool_cert=True) + cert_data_mock = mock.MagicMock() + cert_data_mock.id = uuidutils.generate_uuid() + mock_load_certs.return_value = cert_data_mock + fake_pem = b'fake pem' + mock_build_pem.return_value = fake_pem + ref_md5 = hashlib.md5(fake_pem).hexdigest() # nosec + ref_name = '{id}.pem'.format(id=cert_data_mock.id) + ref_path = '{cert_dir}/{list_id}/{name}'.format( + cert_dir=fake_cert_dir, list_id=sample_listener.id, name=ref_name) + ref_result = {'client_cert': ref_path} + + result = self.driver._process_pool_certs(sample_listener, + sample_listener.default_pool) + + mock_build_pem.assert_called_once_with(cert_data_mock) + mock_apply.assert_called_once_with( + self.driver._upload_cert, sample_listener, None, fake_pem, + ref_md5, ref_name) + self.assertEqual(ref_result, result) + def test_stop(self): self.driver.client.stop_listener.__name__ = 'stop_listener' # Execute driver method @@ -340,6 +393,7 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): listener.id = uuidutils.generate_uuid() listener.load_balancer.amphorae = [amp1, amp2] listener.protocol = 'listener_protocol' + del listener.listeners self.driver.client.start_listener.__name__ = 'start_listener' # Execute driver method self.driver.start(listener, self.sv) diff --git a/octavia/tests/unit/api/drivers/sample_data_models.py b/octavia/tests/unit/api/drivers/sample_data_models.py index b307688e54..2a8e96dcdd 100644 --- a/octavia/tests/unit/api/drivers/sample_data_models.py +++ b/octavia/tests/unit/api/drivers/sample_data_models.py @@ -39,6 +39,7 @@ class SampleDriverDataModels(object): self.sni_container_ref_2 = uuidutils.generate_uuid() self.client_ca_tls_certificate_ref = uuidutils.generate_uuid() self.client_crl_container_ref = uuidutils.generate_uuid() + self.pool_sni_container_ref = uuidutils.generate_uuid() self.pool1_id = uuidutils.generate_uuid() self.pool2_id = uuidutils.generate_uuid() @@ -205,7 +206,9 @@ class SampleDriverDataModels(object): 'health_monitor': self.test_hm1_dict, 'session_persistence': {'type': 'SOURCE'}, 'listeners': [], - 'l7policies': []} + 'l7policies': [], + 'tls_certificate_id': + self.pool_sni_container_ref} self.test_pool1_dict.update(self._common_test_dict) @@ -214,6 +217,7 @@ class SampleDriverDataModels(object): self.test_pool2_dict['name'] = 'pool2' self.test_pool2_dict['description'] = 'Pool 2' self.test_pool2_dict['members'] = self.test_pool2_members_dict + del self.test_pool2_dict['tls_certificate_id'] self.test_pools = [self.test_pool1_dict, self.test_pool2_dict] @@ -225,6 +229,7 @@ class SampleDriverDataModels(object): self.db_pool2.members = self.db_pool2_members self.test_db_pools = [self.db_pool1, self.db_pool2] + pool_cert = data_models.TLSContainer(certificate='pool cert') self.provider_pool1_dict = { 'admin_state_up': True, @@ -236,7 +241,9 @@ class SampleDriverDataModels(object): 'name': 'pool1', 'pool_id': self.pool1_id, 'protocol': 'avian', - 'session_persistence': {'type': 'SOURCE'}} + 'session_persistence': {'type': 'SOURCE'}, + 'tls_container_ref': self.pool_sni_container_ref, + 'tls_container_data': pool_cert.to_dict()} self.provider_pool2_dict = copy.deepcopy(self.provider_pool1_dict) self.provider_pool2_dict['pool_id'] = self.pool2_id @@ -244,6 +251,8 @@ class SampleDriverDataModels(object): self.provider_pool2_dict['description'] = 'Pool 2' self.provider_pool2_dict['members'] = self.provider_pool2_members_dict self.provider_pool2_dict['healthmonitor'] = self.provider_hm2_dict + self.provider_pool2_dict['tls_container_ref'] = None + del self.provider_pool2_dict['tls_container_data'] self.provider_pool1 = driver_dm.Pool(**self.provider_pool1_dict) self.provider_pool1.members = self.provider_pool1_members diff --git a/octavia/tests/unit/api/drivers/test_utils.py b/octavia/tests/unit/api/drivers/test_utils.py index a0dbe77dd9..f0186deaf5 100644 --- a/octavia/tests/unit/api/drivers/test_utils.py +++ b/octavia/tests/unit/api/drivers/test_utils.py @@ -91,8 +91,12 @@ class TestUtils(base.TestCase): cert2 = data_models.TLSContainer(certificate='cert 2') cert3 = data_models.TLSContainer(certificate='cert 3') mock_secret.side_effect = ['ca cert', 'X509 CRL FILE'] - mock_load_cert.return_value = {'tls_cert': cert1, - 'sni_certs': [cert2, cert3]} + listener_certs = {'tls_cert': cert1, 'sni_certs': [cert2, cert3]} + pool_cert = data_models.TLSContainer(certificate='pool cert') + pool_certs = {'tls_cert': pool_cert, 'sni_certs': []} + mock_load_cert.side_effect = [pool_certs, listener_certs, + listener_certs, listener_certs, + listener_certs] test_lb_dict = {'name': 'lb1', 'project_id': self.sample_data.project_id, 'vip_subnet_id': self.sample_data.subnet_id, @@ -180,8 +184,10 @@ class TestUtils(base.TestCase): cert1 = data_models.TLSContainer(certificate='cert 1') cert2 = data_models.TLSContainer(certificate='cert 2') cert3 = data_models.TLSContainer(certificate='cert 3') - mock_load_cert.return_value = {'tls_cert': cert1, - 'sni_certs': [cert2, cert3]} + listener_certs = {'tls_cert': cert1, 'sni_certs': [cert2, cert3]} + pool_cert = data_models.TLSContainer(certificate='pool cert') + pool_certs = {'tls_cert': pool_cert, 'sni_certs': []} + mock_load_cert.side_effect = [listener_certs, pool_certs] # The reason to do this, as before the logic arrives the test func, # there are two data sources, one is from db_dict, the other is from # the api layer model_dict, actually, they are different and contain @@ -191,8 +197,6 @@ class TestUtils(base.TestCase): expect_prov = copy.deepcopy(self.sample_data.provider_listener1_dict) provider_listener = utils.listener_dict_to_provider_dict( self.sample_data.test_listener1_dict) - expect_prov.pop('client_crl_container_ref') - provider_listener.pop('client_crl_container_ref') self.assertEqual(expect_prov, provider_listener) @mock.patch('octavia.api.drivers.utils._get_secret_data') @@ -212,23 +216,43 @@ class TestUtils(base.TestCase): utils.listener_dict_to_provider_dict, test_listener) - def test_db_pool_to_provider_pool(self): + @mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data') + def test_db_pool_to_provider_pool(self, mock_load_cert): + pool_cert = data_models.TLSContainer(certificate='pool cert') + mock_load_cert.return_value = {'tls_cert': pool_cert, + 'sni_certs': None, + 'client_ca_cert': None} provider_pool = utils.db_pool_to_provider_pool( self.sample_data.db_pool1) self.assertEqual(self.sample_data.provider_pool1, provider_pool) - def test_db_pool_to_provider_pool_partial(self): + @mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data') + def test_db_pool_to_provider_pool_partial(self, mock_load_cert): + pool_cert = data_models.TLSContainer(certificate='pool cert') + mock_load_cert.return_value = {'tls_cert': pool_cert, + 'sni_certs': None, + 'client_ca_cert': None} test_db_pool = self.sample_data.db_pool1 test_db_pool.members = [self.sample_data.db_member1] provider_pool = utils.db_pool_to_provider_pool(test_db_pool) self.assertEqual(self.sample_data.provider_pool1, provider_pool) - def test_db_pools_to_provider_pools(self): + @mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data') + def test_db_pools_to_provider_pools(self, mock_load_cert): + pool_cert = data_models.TLSContainer(certificate='pool cert') + mock_load_cert.return_value = {'tls_cert': pool_cert, + 'sni_certs': None, + 'client_ca_cert': None} provider_pools = utils.db_pools_to_provider_pools( self.sample_data.test_db_pools) self.assertEqual(self.sample_data.provider_pools, provider_pools) - def test_pool_dict_to_provider_dict(self): + @mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data') + def test_pool_dict_to_provider_dict(self, mock_load_cert): + pool_cert = data_models.TLSContainer(certificate='pool cert') + mock_load_cert.return_value = {'tls_cert': pool_cert, + 'sni_certs': None, + 'client_ca_cert': None} provider_pool_dict = utils.pool_dict_to_provider_dict( self.sample_data.test_pool1_dict) self.assertEqual(self.sample_data.provider_pool1_dict, diff --git a/octavia/tests/unit/common/jinja/haproxy/test_jinja_cfg.py b/octavia/tests/unit/common/jinja/haproxy/test_jinja_cfg.py index e4d9fdcfd3..eb4bd63bc4 100644 --- a/octavia/tests/unit/common/jinja/haproxy/test_jinja_cfg.py +++ b/octavia/tests/unit/common/jinja/haproxy/test_jinja_cfg.py @@ -13,6 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. +import copy +import os + from octavia.common import constants from octavia.common.jinja.haproxy import jinja_cfg from octavia.tests.unit import base @@ -744,6 +747,39 @@ class TestHaproxyCfg(base.TestCase): sample_configs.sample_base_expected_config(backend=be), rendered_obj) + def test_render_template_pool_cert(self): + cert_file_path = os.path.join(self.jinja_cfg.base_crt_dir, + 'sample_listener_id_1', 'fake path') + be = ("backend sample_pool_id_1\n" + " mode http\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " timeout check 31s\n" + " option httpchk GET /index.html\n" + " http-check expect rstatus 418\n" + " fullconn {maxconn}\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_1 10.0.0.99:82 weight 13 " + "check inter 30s fall 3 rise 2 cookie sample_member_id_1 " + "{opts}\n" + " server sample_member_id_2 10.0.0.98:82 weight 13 " + "check inter 30s fall 3 rise 2 cookie sample_member_id_2 " + "{opts}\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN, + opts="%s %s %s %s" % ("ssl", "crt", cert_file_path, "verify none")) + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs.sample_amphora_tuple(), + sample_configs.sample_listener_tuple(pool_cert=True), + pool_tls_certs={ + 'sample_pool_id_1': + {'client_cert': cert_file_path, + 'sni_certs': []}}) + self.assertEqual( + sample_configs.sample_base_expected_config(backend=be), + rendered_obj) + def test_transform_session_persistence(self): in_persistence = sample_configs.sample_session_persistence_tuple() ret = self.jinja_cfg._transform_session_persistence(in_persistence, {}) @@ -774,11 +810,20 @@ class TestHaproxyCfg(base.TestCase): in_pool = sample_configs.sample_pool_tuple(sample_pool=2) ret = self.jinja_cfg._transform_pool( in_pool, {constants.HTTP_REUSE: True}) - import copy expected_config = copy.copy(sample_configs.RET_POOL_2) expected_config[constants.HTTP_REUSE] = True self.assertEqual(expected_config, ret) + def test_transform_pool_cert(self): + in_pool = sample_configs.sample_pool_tuple(pool_cert=True) + cert_path = os.path.join(self.jinja_cfg.base_crt_dir, + 'test_listener_id', 'pool_cert.pem') + ret = self.jinja_cfg._transform_pool( + in_pool, {}, pool_tls_certs={'client_cert': cert_path}) + expected_config = copy.copy(sample_configs.RET_POOL_1) + expected_config['client_cert'] = cert_path + self.assertEqual(expected_config, ret) + def test_transform_listener(self): in_listener = sample_configs.sample_listener_tuple() ret = self.jinja_cfg._transform_listener(in_listener, None, {}) diff --git a/octavia/tests/unit/common/sample_configs/sample_configs.py b/octavia/tests/unit/common/sample_configs/sample_configs.py index 3fdb3cf9de..1aded01cb7 100644 --- a/octavia/tests/unit/common/sample_configs/sample_configs.py +++ b/octavia/tests/unit/common/sample_configs/sample_configs.py @@ -23,22 +23,27 @@ from octavia.tests.unit.common.sample_configs import sample_certs CONF = cfg.CONF -def sample_amphora_tuple(): +def sample_amphora_tuple(id='sample_amphora_id_1', lb_network_ip='10.0.1.1', + vrrp_ip='10.1.1.1', ha_ip='192.168.10.1', + vrrp_port_id='1234', ha_port_id='1234', role=None, + status='ACTIVE', vrrp_interface=None, + vrrp_priority=None): in_amphora = collections.namedtuple( 'amphora', 'id, lb_network_ip, vrrp_ip, ha_ip, vrrp_port_id, ' 'ha_port_id, role, status, vrrp_interface,' 'vrrp_priority') return in_amphora( - id='sample_amphora_id_1', - lb_network_ip='10.0.1.1', - vrrp_ip='10.1.1.1', - ha_ip='192.168.10.1', - vrrp_port_id='1234', - ha_port_id='1234', - role=None, - status='ACTIVE', - vrrp_interface=None, - vrrp_priority=None) + id=id, + lb_network_ip=lb_network_ip, + vrrp_ip=vrrp_ip, + ha_ip=ha_ip, + vrrp_port_id=vrrp_port_id, + ha_port_id=ha_port_id, + role=role, + status=status, + vrrp_interface=vrrp_interface, + vrrp_priority=vrrp_priority) + RET_PERSISTENCE = { 'type': 'HTTP_COOKIE', @@ -464,7 +469,11 @@ def sample_loadbalancer_tuple(proto=None, monitor=True, persistence=True, def sample_listener_loadbalancer_tuple(proto=None, topology=None, enabled=True): proto = 'HTTP' if proto is None else proto - topology = 'SINGLE' if topology is None else topology + if topology and topology in ['ACTIVE_STANDBY', 'ACTIVE_ACTIVE']: + more_amp = True + else: + more_amp = False + topology = constants.TOPOLOGY_SINGLE in_lb = collections.namedtuple( 'load_balancer', 'id, name, protocol, vip, amphorae, topology, ' 'enabled') @@ -473,7 +482,13 @@ def sample_listener_loadbalancer_tuple(proto=None, topology=None, name='test-lb', protocol=proto, vip=sample_vip_tuple(), - amphorae=[sample_amphora_tuple()], + amphorae=[sample_amphora_tuple(role=constants.ROLE_MASTER), + sample_amphora_tuple( + id='sample_amphora_id_2', + lb_network_ip='10.0.1.2', + vrrp_ip='10.1.1.2', + role=constants.ROLE_BACKUP)] + if more_amp else [sample_amphora_tuple()], topology=topology, enabled=enabled ) @@ -513,7 +528,7 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True, timeout_member_data=50000, timeout_tcp_inspect=0, client_ca_cert=False, client_crl_cert=False, - ssl_type_l7=False): + ssl_type_l7=False, pool_cert=False): proto = 'HTTP' if proto is None else proto if be_proto is None: be_proto = 'HTTP' if proto is 'TERMINATED_HTTPS' else proto @@ -537,12 +552,14 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True, proto=be_proto, monitor=monitor, persistence=persistence, persistence_type=persistence_type, persistence_cookie=persistence_cookie, - monitor_ip_port=monitor_ip_port, monitor_proto=monitor_proto), + monitor_ip_port=monitor_ip_port, monitor_proto=monitor_proto, + pool_cert=pool_cert), sample_pool_tuple( proto=be_proto, monitor=monitor, persistence=persistence, persistence_type=persistence_type, persistence_cookie=persistence_cookie, sample_pool=2, - monitor_ip_port=monitor_ip_port, monitor_proto=monitor_proto)] + monitor_ip_port=monitor_ip_port, monitor_proto=monitor_proto, + pool_cert=pool_cert)] l7policies = [ sample_l7policy_tuple('sample_l7policy_id_1', sample_policy=1), sample_l7policy_tuple('sample_l7policy_id_2', sample_policy=2), @@ -561,7 +578,8 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True, persistence_type=persistence_type, persistence_cookie=persistence_cookie, monitor_ip_port=monitor_ip_port, monitor_proto=monitor_proto, - backup_member=backup_member, disabled_member=disabled_member)] + backup_member=backup_member, disabled_member=disabled_member, + pool_cert=pool_cert)] l7policies = [] return in_listener( id='sample_listener_id_1', @@ -578,7 +596,8 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True, persistence_timeout=persistence_timeout, persistence_granularity=persistence_granularity, monitor_ip_port=monitor_ip_port, - monitor_proto=monitor_proto) if alloc_default_pool else '', + monitor_proto=monitor_proto, + pool_cert=pool_cert) if alloc_default_pool else '', connection_limit=connection_limit, tls_certificate_id='cont_id_1' if tls else '', sni_container_ids=['cont_id_2', 'cont_id_3'] if sni else [], @@ -651,13 +670,15 @@ def sample_pool_tuple(proto=None, monitor=True, persistence=True, persistence_timeout=None, persistence_granularity=None, sample_pool=1, monitor_ip_port=False, monitor_proto=None, backup_member=False, - disabled_member=False, has_http_reuse=True): + disabled_member=False, has_http_reuse=True, + pool_cert=False, full_store=False): proto = 'HTTP' if proto is None else proto monitor_proto = proto if monitor_proto is None else monitor_proto in_pool = collections.namedtuple( 'pool', 'id, protocol, lb_algorithm, members, health_monitor, ' - 'session_persistence, enabled, operating_status, ' + - constants.HTTP_REUSE) + 'session_persistence, enabled, operating_status, ' + 'tls_certificate_id, tls_container, load_balancer, ' + 'listeners, ' + constants.HTTP_REUSE) if (proto == constants.PROTOCOL_UDP and persistence_type == constants.SESSION_PERSISTENCE_SOURCE_IP): kwargs = {'persistence_type': persistence_type, @@ -684,6 +705,8 @@ def sample_pool_tuple(proto=None, monitor=True, persistence=True, monitor_ip_port=monitor_ip_port)] if monitor is True: mon = sample_health_monitor_tuple(proto=monitor_proto, sample_hm=2) + + in_pool_listener = collections.namedtuple('listener', 'id',) return in_pool( id=id, protocol=proto, @@ -692,7 +715,15 @@ def sample_pool_tuple(proto=None, monitor=True, persistence=True, health_monitor=mon, session_persistence=persis if persistence is True else None, enabled=True, - operating_status='ACTIVE', has_http_reuse=has_http_reuse) + operating_status='ACTIVE', has_http_reuse=has_http_reuse, + tls_certificate_id='pool_cont_1' if pool_cert else None, + tls_container={'client_cert': 'fake path'} if pool_cert else '', + load_balancer=sample_listener_loadbalancer_tuple( + proto=proto, + topology=constants.TOPOLOGY_ACTIVE_STANDBY) if full_store else '', + listeners=[in_pool_listener(id='used_as_default_pool_listener_id'), + in_pool_listener(id='used_as_redirect_pool_l7_listener_id')] + if full_store else []) def sample_member_tuple(id, ip, enabled=True, operating_status='ACTIVE', diff --git a/releasenotes/notes/Add-pool-tls-client-auth-01d3b8acfb78ab14.yaml b/releasenotes/notes/Add-pool-tls-client-auth-01d3b8acfb78ab14.yaml new file mode 100644 index 0000000000..d57ee6f644 --- /dev/null +++ b/releasenotes/notes/Add-pool-tls-client-auth-01d3b8acfb78ab14.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + You can now specify a tls_container_ref on pools for TLS client + authentication to pool members.