Add crl-file option for certification
Add crl-file in Listener side. Story: 2002165 Co-Authored-By: Michael Johnson <johnsomor@gmail.com> Change-Id: I9e2ec06719fbbfd19482c2b8d39220e7e4ed81e3
This commit is contained in:
parent
7a8eb3ce22
commit
20509e2337
@ -282,6 +282,24 @@ client_ca_tls_container_ref-optional:
|
||||
min_version: 2.8
|
||||
required: false
|
||||
type: string
|
||||
client_crl_container_ref:
|
||||
description: |
|
||||
The URI of the `key manager service
|
||||
<https://docs.openstack.org/castellan/latest/>`__ secret containing a
|
||||
PEM format CA revocation list file for ``TERMINATED_TLS`` listeners.
|
||||
in: body
|
||||
min_version: 2.8
|
||||
required: true
|
||||
type: string
|
||||
client_crl_container_ref-optional:
|
||||
description: |
|
||||
The URI of the `key manager service
|
||||
<https://docs.openstack.org/castellan/latest/>`__ secret containing a
|
||||
PEM format CA revocation list file for ``TERMINATED_TLS`` listeners.
|
||||
in: body
|
||||
min_version: 2.8
|
||||
required: false
|
||||
type: string
|
||||
compute-flavor:
|
||||
description: |
|
||||
The ID of the compute flavor used for the amphora.
|
||||
|
@ -1 +1 @@
|
||||
curl -X POST -H "Content-Type: application/json" -H "X-Auth-Token: <token>" -d '{"listener": {"protocol": "TERMINATED_HTTPS", "description": "A great TLS listener", "admin_state_up": true, "connection_limit": 200, "protocol_port": "443", "loadbalancer_id": "607226db-27ef-4d41-ae89-f2a800e9c2db", "name": "great_tls_listener", "insert_headers": {"X-Forwarded-For": "true", "X-Forwarded-Port": "true"}, "default_tls_container_ref": "http://198.51.100.10:9311/v1/containers/a570068c-d295-4780-91d4-3046a325db51", "sni_container_refs": ["http://198.51.100.10:9311/v1/containers/a570068c-d295-4780-91d4-3046a325db51", "http://198.51.100.10:9311/v1/containers/aaebb31e-7761-4826-8cb4-2b829caca3ee"], "timeout_client_data": 50000, "timeout_member_connect": 5000, "timeout_member_data": 50000, "timeout_tcp_inspect": 0, "tags": ["test_tag"], "client_ca_tls_container_ref": "http://198.51.100.10:9311/v1/containers/35649991-49f3-4625-81ce-2465fe8932e5", "client_authentication": "MANDATORY"}}' http://198.51.100.10:9876/v2/lbaas/listeners
|
||||
curl -X POST -H "Content-Type: application/json" -H "X-Auth-Token: <token>" -d '{"listener": {"protocol": "TERMINATED_HTTPS", "description": "A great TLS listener", "admin_state_up": true, "connection_limit": 200, "protocol_port": "443", "loadbalancer_id": "607226db-27ef-4d41-ae89-f2a800e9c2db", "name": "great_tls_listener", "insert_headers": {"X-Forwarded-For": "true", "X-Forwarded-Port": "true"}, "default_tls_container_ref": "http://198.51.100.10:9311/v1/containers/a570068c-d295-4780-91d4-3046a325db51", "sni_container_refs": ["http://198.51.100.10:9311/v1/containers/a570068c-d295-4780-91d4-3046a325db51", "http://198.51.100.10:9311/v1/containers/aaebb31e-7761-4826-8cb4-2b829caca3ee"], "timeout_client_data": 50000, "timeout_member_connect": 5000, "timeout_member_data": 50000, "timeout_tcp_inspect": 0, "tags": ["test_tag"], "client_ca_tls_container_ref": "http://198.51.100.10:9311/v1/containers/35649991-49f3-4625-81ce-2465fe8932e5", "client_authentication": "MANDATORY", "client_crl_container_ref": "http://198.51.100.10:9311/v1/containers/e222b065-b93b-4e2a-9a02-804b7a118c3c"}}' http://198.51.100.10:9876/v2/lbaas/listeners
|
||||
|
@ -22,6 +22,7 @@
|
||||
"timeout_tcp_inspect": 0,
|
||||
"tags": ["test_tag"],
|
||||
"client_ca_tls_container_ref": "http://198.51.100.10:9311/v1/containers/35649991-49f3-4625-81ce-2465fe8932e5",
|
||||
"client_authentication": "MANDATORY"
|
||||
"client_authentication": "MANDATORY",
|
||||
"client_crl_container_ref": "http://198.51.100.10:9311/v1/containers/e222b065-b93b-4e2a-9a02-804b7a118c3c"
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,7 @@
|
||||
"timeout_tcp_inspect": 0,
|
||||
"tags": ["test_tag"],
|
||||
"client_ca_tls_container_ref": "http://198.51.100.10:9311/v1/containers/35649991-49f3-4625-81ce-2465fe8932e5",
|
||||
"client_authentication": "MANDATORY"
|
||||
"client_authentication": "MANDATORY",
|
||||
"client_crl_container_ref": "http://198.51.100.10:9311/v1/containers/e222b065-b93b-4e2a-9a02-804b7a118c3c"
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,7 @@
|
||||
"timeout_tcp_inspect": 0,
|
||||
"tags": ["test_tag"],
|
||||
"client_ca_tls_container_ref": "http://198.51.100.10:9311/v1/containers/35649991-49f3-4625-81ce-2465fe8932e5",
|
||||
"client_authentication": "MANDATORY"
|
||||
"client_authentication": "MANDATORY",
|
||||
"client_crl_container_ref": "http://198.51.100.10:9311/v1/containers/e222b065-b93b-4e2a-9a02-804b7a118c3c"
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,7 @@
|
||||
"timeout_tcp_inspect": 5,
|
||||
"tags": ["updated_tag"],
|
||||
"client_ca_tls_container_ref": null,
|
||||
"client_authentication": "NONE"
|
||||
"client_authentication": "NONE",
|
||||
"client_crl_container_ref": null
|
||||
}
|
||||
}
|
||||
|
@ -39,7 +39,8 @@
|
||||
"timeout_tcp_inspect": 0,
|
||||
"tags": ["test_tag"],
|
||||
"client_ca_tls_container_ref": "http://198.51.100.10:9311/v1/containers/35649991-49f3-4625-81ce-2465fe8932e5",
|
||||
"client_authentication": "NONE"
|
||||
"client_authentication": "NONE",
|
||||
"client_crl_container_ref": "http://198.51.100.10:9311/v1/containers/e222b065-b93b-4e2a-9a02-804b7a118c3c"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -48,6 +48,7 @@ Response Parameters
|
||||
- admin_state_up: admin_state_up
|
||||
- client_authentication: client_authentication
|
||||
- client_ca_tls_container_ref: client_ca_tls_container_ref
|
||||
- client_crl_container_ref: client_crl_container_ref
|
||||
- connection_limit: connection_limit
|
||||
- created_at: created_at
|
||||
- default_pool_id: default_pool_id
|
||||
@ -140,6 +141,7 @@ Request
|
||||
- admin_state_up: admin_state_up-default-optional
|
||||
- client_authentication: client_authentication-optional
|
||||
- client_ca_tls_container_ref: client_ca_tls_container_ref-optional
|
||||
- client_crl_container_ref: client_crl_container_ref-optional
|
||||
- connection_limit: connection_limit-optional
|
||||
- default_pool: pool-optional
|
||||
- default_pool_id: default_pool_id-optional
|
||||
@ -210,6 +212,7 @@ Response Parameters
|
||||
- admin_state_up: admin_state_up
|
||||
- client_authentication: client_authentication
|
||||
- client_ca_tls_container_ref: client_ca_tls_container_ref
|
||||
- client_crl_container_ref: client_crl_container_ref
|
||||
- connection_limit: connection_limit
|
||||
- created_at: created_at
|
||||
- default_pool_id: default_pool_id
|
||||
@ -286,6 +289,7 @@ Response Parameters
|
||||
- admin_state_up: admin_state_up
|
||||
- client_authentication: client_authentication
|
||||
- client_ca_tls_container_ref: client_ca_tls_container_ref
|
||||
- client_crl_container_ref: client_crl_container_ref
|
||||
- connection_limit: connection_limit
|
||||
- created_at: created_at
|
||||
- default_pool_id: default_pool_id
|
||||
@ -352,6 +356,7 @@ Request
|
||||
- admin_state_up: admin_state_up-default-optional
|
||||
- client_authentication: client_authentication-optional
|
||||
- client_ca_tls_container_ref: client_ca_tls_container_ref-optional
|
||||
- client_crl_container_ref: client_crl_container_ref-optional
|
||||
- connection_limit: connection_limit-optional
|
||||
- default_pool_id: default_pool_id-optional
|
||||
- default_tls_container_ref: default_tls_container_ref-optional
|
||||
@ -386,6 +391,7 @@ Response Parameters
|
||||
- admin_state_up: admin_state_up
|
||||
- client_authentication: client_authentication
|
||||
- client_ca_tls_container_ref: client_ca_tls_container_ref
|
||||
- client_crl_container_ref: client_crl_container_ref
|
||||
- connection_limit: connection_limit
|
||||
- created_at: created_at
|
||||
- default_pool_id: default_pool_id
|
||||
|
@ -373,6 +373,11 @@ contain the following:
|
||||
| client_ca_tls_container_ref | string | The reference to the secrets |
|
||||
| | | container. |
|
||||
+------------------------------+--------+-------------------------------------+
|
||||
| client_crl_container_data | string | A PEM encoded CRL file. |
|
||||
+------------------------------+--------+-------------------------------------+
|
||||
| client_crl_container_ref | string | The reference to the secrets |
|
||||
| | | container. |
|
||||
+------------------------------+--------+-------------------------------------+
|
||||
| connection_limit | int | The max number of connections |
|
||||
| | | permitted for this listener. Default|
|
||||
| | | is -1, which is infinite |
|
||||
|
@ -118,13 +118,16 @@ class HaproxyAmphoraLoadBalancerDriver(
|
||||
certs = self._process_tls_certificates(listener)
|
||||
client_ca_filename = self._process_secret(
|
||||
listener, listener.client_ca_tls_certificate_id)
|
||||
crl_filename = self._process_secret(
|
||||
listener, listener.client_crl_container_id)
|
||||
|
||||
# Generate HaProxy configuration from listener object
|
||||
config = self.jinja.build_config(
|
||||
host_amphora=amp, listener=listener,
|
||||
tls_cert=certs['tls_cert'],
|
||||
haproxy_versions=haproxy_versions,
|
||||
client_ca_filename=client_ca_filename)
|
||||
client_ca_filename=client_ca_filename,
|
||||
client_crl=crl_filename)
|
||||
self.client.upload_config(amp, listener.id, config,
|
||||
timeout_dict=timeout_dict)
|
||||
self.client.reload_listener(amp, listener.id,
|
||||
@ -156,6 +159,8 @@ class HaproxyAmphoraLoadBalancerDriver(
|
||||
certs = self._process_tls_certificates(listener)
|
||||
client_ca_filename = self._process_secret(
|
||||
listener, listener.client_ca_tls_certificate_id)
|
||||
crl_filename = self._process_secret(
|
||||
listener, listener.client_crl_container_id)
|
||||
|
||||
for amp in listener.load_balancer.amphorae:
|
||||
if amp.status != consts.DELETED:
|
||||
@ -167,7 +172,8 @@ class HaproxyAmphoraLoadBalancerDriver(
|
||||
host_amphora=amp, listener=listener,
|
||||
tls_cert=certs['tls_cert'],
|
||||
haproxy_versions=haproxy_versions,
|
||||
client_ca_filename=client_ca_filename)
|
||||
client_ca_filename=client_ca_filename,
|
||||
client_crl=crl_filename)
|
||||
self.client.upload_config(amp, listener.id, config)
|
||||
self.client.reload_listener(amp, listener.id)
|
||||
|
||||
|
@ -135,7 +135,8 @@ class Listener(BaseDataModel):
|
||||
timeout_member_connect=Unset, timeout_member_data=Unset,
|
||||
timeout_tcp_inspect=Unset, client_ca_tls_container_ref=Unset,
|
||||
client_ca_tls_container_data=Unset,
|
||||
client_authentication=Unset):
|
||||
client_authentication=Unset, client_crl_container_ref=Unset,
|
||||
client_crl_container_data=Unset):
|
||||
|
||||
self.admin_state_up = admin_state_up
|
||||
self.connection_limit = connection_limit
|
||||
@ -160,6 +161,8 @@ class Listener(BaseDataModel):
|
||||
self.client_ca_tls_container_ref = client_ca_tls_container_ref
|
||||
self.client_ca_tls_container_data = client_ca_tls_container_data
|
||||
self.client_authentication = client_authentication
|
||||
self.client_crl_container_ref = client_crl_container_ref
|
||||
self.client_crl_container_data = client_crl_container_data
|
||||
|
||||
|
||||
class Pool(BaseDataModel):
|
||||
|
@ -184,7 +184,9 @@ def listener_dict_to_provider_dict(listener_dict):
|
||||
if 'client_ca_tls_certificate_id' in new_listener_dict:
|
||||
new_listener_dict['client_ca_tls_container_ref'] = (
|
||||
new_listener_dict.pop('client_ca_tls_certificate_id'))
|
||||
|
||||
if 'client_crl_container_id' in new_listener_dict:
|
||||
new_listener_dict['client_crl_container_ref'] = (
|
||||
new_listener_dict.pop('client_crl_container_id'))
|
||||
listener_obj = data_models.Listener(**listener_dict)
|
||||
if (listener_obj.tls_certificate_id or listener_obj.sni_containers or
|
||||
listener_obj.client_ca_tls_certificate_id):
|
||||
@ -220,6 +222,10 @@ def listener_dict_to_provider_dict(listener_dict):
|
||||
cert = _get_secret_data(cert_manager, listener_obj,
|
||||
listener_obj.client_ca_tls_certificate_id)
|
||||
new_listener_dict['client_ca_tls_container_data'] = cert
|
||||
if listener_obj.client_crl_container_id:
|
||||
crl_file = _get_secret_data(cert_manager, listener_obj,
|
||||
listener_obj.client_crl_container_id)
|
||||
new_listener_dict['client_crl_container_data'] = crl_file
|
||||
|
||||
# Remove the DB back references
|
||||
if 'load_balancer' in new_listener_dict:
|
||||
|
@ -142,7 +142,7 @@ class ListenersController(base.BaseController):
|
||||
if bad_refs:
|
||||
raise exceptions.CertificateRetrievalException(ref=bad_refs)
|
||||
|
||||
def _validate_client_ca_ref(self, client_ca_ref):
|
||||
def _validate_client_ca_and_crl_refs(self, client_ca_ref, crl_ref):
|
||||
context = pecan.request.context.get('octavia_context')
|
||||
bad_refs = []
|
||||
try:
|
||||
@ -151,23 +151,51 @@ class ListenersController(base.BaseController):
|
||||
except Exception:
|
||||
bad_refs.append(client_ca_ref)
|
||||
|
||||
# This will be used in a later patch
|
||||
pem_crl = None
|
||||
if crl_ref:
|
||||
try:
|
||||
self.cert_manager.set_acls(context, crl_ref)
|
||||
pem_crl = self.cert_manager.get_secret(context, crl_ref)
|
||||
except Exception:
|
||||
bad_refs.append(crl_ref)
|
||||
if bad_refs:
|
||||
raise exceptions.CertificateRetrievalException(ref=bad_refs)
|
||||
|
||||
ca_cert = None
|
||||
try:
|
||||
# Test if it needs to be UTF-8 encoded
|
||||
try:
|
||||
ca_pem = ca_pem.encode('utf-8')
|
||||
except AttributeError:
|
||||
pass
|
||||
x509.load_pem_x509_certificate(ca_pem, default_backend())
|
||||
ca_cert = x509.load_pem_x509_certificate(ca_pem, default_backend())
|
||||
except Exception as e:
|
||||
raise exceptions.ValidationException(detail=_(
|
||||
"The client authentication CA certificate is invalid. "
|
||||
"It must be a valid x509 PEM format certificate. "
|
||||
"Error: %s") % str(e))
|
||||
|
||||
# Validate the CRL is for the client CA
|
||||
if pem_crl:
|
||||
ca_pub_key = ca_cert.public_key()
|
||||
crl = None
|
||||
# Test if it needs to be UTF-8 encoded
|
||||
try:
|
||||
pem_crl = pem_crl.encode('utf-8')
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
crl = x509.load_pem_x509_crl(pem_crl, default_backend())
|
||||
except Exception as e:
|
||||
raise exceptions.ValidationException(detail=_(
|
||||
"The client authentication certificate revocation list "
|
||||
"is invalid. It must be a valid x509 PEM format "
|
||||
"certificate revocation list. Error: %s") % str(e))
|
||||
if not crl.is_signature_valid(ca_pub_key):
|
||||
raise exceptions.ValidationException(detail=_(
|
||||
"The CRL specified is not valid for client certificate "
|
||||
"authority reference supplied."))
|
||||
|
||||
def _has_tls_container_refs(self, listener_dict):
|
||||
return (listener_dict.get('tls_certificate_id') or
|
||||
listener_dict.get('client_ca_tls_container_id') or
|
||||
@ -239,15 +267,27 @@ class ListenersController(base.BaseController):
|
||||
"container reference.") %
|
||||
listener_dict.get('client_authentication'))
|
||||
|
||||
# Make sure we have a client CA if they specify a CRL
|
||||
if (listener_dict.get('client_crl_container_id') and
|
||||
not listener_dict.get('client_ca_tls_certificate_id')):
|
||||
raise exceptions.ValidationException(detail=_(
|
||||
"A client authentication CA reference is required to "
|
||||
"specify a client authentication revocation list."))
|
||||
|
||||
# Validate the TLS containers
|
||||
sni_containers = listener_dict.pop('sni_containers', [])
|
||||
tls_refs = [sni['tls_container_id'] for sni in sni_containers]
|
||||
if listener_dict.get('tls_certificate_id'):
|
||||
tls_refs.append(listener_dict.get('tls_certificate_id'))
|
||||
self._validate_tls_refs(tls_refs)
|
||||
|
||||
# Validate the client CA cert and optional client CRL
|
||||
if listener_dict.get('client_ca_tls_certificate_id'):
|
||||
self._validate_client_ca_and_crl_refs(
|
||||
listener_dict.get('client_ca_tls_certificate_id'),
|
||||
listener_dict.get('client_crl_container_id', None))
|
||||
|
||||
try:
|
||||
sni_containers = listener_dict.pop('sni_containers', [])
|
||||
tls_refs = [sni['tls_container_id'] for sni in sni_containers]
|
||||
if listener_dict.get('tls_certificate_id'):
|
||||
tls_refs.append(listener_dict.get('tls_certificate_id'))
|
||||
self._validate_tls_refs(tls_refs)
|
||||
if listener_dict.get('client_ca_tls_certificate_id'):
|
||||
self._validate_client_ca_ref(
|
||||
listener_dict.get('client_ca_tls_certificate_id'))
|
||||
db_listener = self.repositories.listener.create(
|
||||
lock_session, **listener_dict)
|
||||
if sni_containers:
|
||||
@ -406,8 +446,28 @@ class ListenersController(base.BaseController):
|
||||
if listener.default_tls_container_ref:
|
||||
tls_refs.append(listener.default_tls_container_ref)
|
||||
self._validate_tls_refs(tls_refs)
|
||||
if listener.client_ca_tls_container_ref:
|
||||
self._validate_client_ca_ref(listener.client_ca_tls_container_ref)
|
||||
|
||||
ca_ref = None
|
||||
if (listener.client_ca_tls_container_ref and
|
||||
listener.client_ca_tls_container_ref != wtypes.Unset):
|
||||
ca_ref = listener.client_ca_tls_container_ref
|
||||
elif db_listener.client_ca_tls_certificate_id:
|
||||
ca_ref = db_listener.client_ca_tls_certificate_id
|
||||
|
||||
crl_ref = None
|
||||
if (listener.client_crl_container_ref and
|
||||
listener.client_crl_container_ref != wtypes.Unset):
|
||||
crl_ref = listener.client_crl_container_ref
|
||||
elif db_listener.client_crl_container_id:
|
||||
crl_ref = db_listener.client_crl_container_id
|
||||
|
||||
if crl_ref and not ca_ref:
|
||||
raise exceptions.ValidationException(detail=_(
|
||||
"A client authentication CA reference is required to "
|
||||
"specify a client authentication revocation list."))
|
||||
|
||||
if ca_ref or crl_ref:
|
||||
self._validate_client_ca_and_crl_refs(ca_ref, crl_ref)
|
||||
|
||||
@wsme_pecan.wsexpose(listener_types.ListenerRootResponse, wtypes.text,
|
||||
body=listener_types.ListenerRootPUT, status_code=200)
|
||||
|
@ -28,7 +28,8 @@ class BaseListenerType(types.BaseType):
|
||||
_type_to_model_map = {
|
||||
'admin_state_up': 'enabled',
|
||||
'default_tls_container_ref': 'tls_certificate_id',
|
||||
'client_ca_tls_container_ref': 'client_ca_tls_certificate_id'}
|
||||
'client_ca_tls_container_ref': 'client_ca_tls_certificate_id',
|
||||
'client_crl_container_ref': 'client_crl_container_id'}
|
||||
_child_map = {}
|
||||
|
||||
|
||||
@ -59,6 +60,7 @@ class ListenerResponse(BaseListenerType):
|
||||
tags = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType()))
|
||||
client_ca_tls_container_ref = wtypes.StringType()
|
||||
client_authentication = wtypes.wsattr(wtypes.StringType())
|
||||
client_crl_container_ref = wtypes.wsattr(wtypes.StringType())
|
||||
|
||||
@classmethod
|
||||
def from_data_model(cls, data_model, children=False):
|
||||
@ -142,6 +144,7 @@ class ListenerPOST(BaseListenerType):
|
||||
client_authentication = wtypes.wsattr(
|
||||
wtypes.Enum(str, *constants.SUPPORTED_CLIENT_AUTH_MODES),
|
||||
default=constants.CLIENT_AUTH_NONE)
|
||||
client_crl_container_ref = wtypes.StringType(max_length=255)
|
||||
|
||||
|
||||
class ListenerRootPOST(types.BaseType):
|
||||
@ -177,6 +180,7 @@ class ListenerPUT(BaseListenerType):
|
||||
client_ca_tls_container_ref = wtypes.StringType(max_length=255)
|
||||
client_authentication = wtypes.wsattr(
|
||||
wtypes.Enum(str, *constants.SUPPORTED_CLIENT_AUTH_MODES))
|
||||
client_crl_container_ref = wtypes.StringType(max_length=255)
|
||||
|
||||
|
||||
class ListenerRootPUT(types.BaseType):
|
||||
@ -224,6 +228,7 @@ class ListenerSingleCreate(BaseListenerType):
|
||||
client_authentication = wtypes.wsattr(
|
||||
wtypes.Enum(str, *constants.SUPPORTED_CLIENT_AUTH_MODES),
|
||||
default=constants.CLIENT_AUTH_NONE)
|
||||
client_crl_container_ref = wtypes.StringType(max_length=255)
|
||||
|
||||
|
||||
class ListenerStatusResponse(BaseListenerType):
|
||||
|
@ -369,7 +369,7 @@ class Listener(BaseDataModel):
|
||||
timeout_client_data=None, timeout_member_connect=None,
|
||||
timeout_member_data=None, timeout_tcp_inspect=None,
|
||||
tags=None, client_ca_tls_certificate_id=None,
|
||||
client_authentication=None):
|
||||
client_authentication=None, client_crl_container_id=None):
|
||||
self.id = id
|
||||
self.project_id = project_id
|
||||
self.name = name
|
||||
@ -400,6 +400,7 @@ class Listener(BaseDataModel):
|
||||
self.tags = tags
|
||||
self.client_ca_tls_certificate_id = client_ca_tls_certificate_id
|
||||
self.client_authentication = client_authentication
|
||||
self.client_crl_container_id = client_crl_container_id
|
||||
|
||||
def update(self, update_dict):
|
||||
for key, value in update_dict.items():
|
||||
|
@ -83,7 +83,7 @@ class JinjaTemplater(object):
|
||||
|
||||
def build_config(self, host_amphora, listener, tls_cert,
|
||||
haproxy_versions, socket_path=None,
|
||||
client_ca_filename=None):
|
||||
client_ca_filename=None, client_crl=None):
|
||||
"""Convert a logical configuration to the HAProxy version
|
||||
|
||||
:param host_amphora: The Amphora this configuration is hosted on
|
||||
@ -105,7 +105,7 @@ 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_ca_filename=client_ca_filename, client_crl=client_crl)
|
||||
|
||||
def _get_template(self):
|
||||
"""Returns the specified Jinja configuration template."""
|
||||
@ -124,7 +124,7 @@ 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_ca_filename=None, client_crl=None):
|
||||
"""Renders a templated configuration from a load balancer object
|
||||
|
||||
:param host_amphora: The Amphora this configuration is hosted on
|
||||
@ -141,7 +141,8 @@ class JinjaTemplater(object):
|
||||
listener,
|
||||
tls_cert,
|
||||
feature_compatibility,
|
||||
client_ca_filename=client_ca_filename)
|
||||
client_ca_filename=client_ca_filename,
|
||||
client_crl=client_crl)
|
||||
if not socket_path:
|
||||
socket_path = '%s/%s.sock' % (self.base_amp_path, listener.id)
|
||||
return self._get_template().render(
|
||||
@ -154,14 +155,14 @@ class JinjaTemplater(object):
|
||||
|
||||
def _transform_loadbalancer(self, host_amphora, loadbalancer, listener,
|
||||
tls_cert, feature_compatibility,
|
||||
client_ca_filename=None):
|
||||
client_ca_filename=None, client_crl=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_ca_filename=client_ca_filename, client_crl=client_crl)
|
||||
ret_value = {
|
||||
'id': loadbalancer.id,
|
||||
'vip_address': loadbalancer.vip.ip_address,
|
||||
@ -201,7 +202,7 @@ class JinjaTemplater(object):
|
||||
}
|
||||
|
||||
def _transform_listener(self, listener, tls_cert, feature_compatibility,
|
||||
client_ca_filename=None):
|
||||
client_ca_filename=None, client_crl=None):
|
||||
"""Transforms a listener into an object that will
|
||||
|
||||
be processed by the templating system
|
||||
@ -245,6 +246,9 @@ class JinjaTemplater(object):
|
||||
client_ca_filename))
|
||||
ret_value['client_auth'] = CLIENT_AUTH_MAP.get(
|
||||
listener.client_authentication)
|
||||
if listener.client_crl_container_id:
|
||||
ret_value['client_crl_path'] = '%s' % (
|
||||
os.path.join(self.base_crt_dir, listener.id, client_crl))
|
||||
|
||||
if listener.default_pool:
|
||||
ret_value['default_pool'] = self._transform_pool(
|
||||
|
@ -43,8 +43,13 @@ peers {{ "%s_peers"|format(listener.id.replace("-", ""))|trim() }}
|
||||
{% else %}
|
||||
{% set client_ca_opt = "" %}
|
||||
{% endif %}
|
||||
{% if listener.client_crl_path and listener.client_ca_tls_path %}
|
||||
{% set ca_crl_opt = "crl-file %s"|format(listener.client_crl_path)|trim() %}
|
||||
{% else %}
|
||||
{% set ca_crl_opt = "" %}
|
||||
{% endif %}
|
||||
bind {{ lb_vip_address }}:{{ listener.protocol_port }} {{
|
||||
"%s %s %s"|format(def_crt_opt, crt_dir_opt, client_ca_opt)|trim() }}
|
||||
"%s %s %s %s"|format(def_crt_opt, crt_dir_opt, client_ca_opt, ca_crl_opt)|trim() }}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
|
@ -0,0 +1,36 @@
|
||||
# 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.
|
||||
#
|
||||
|
||||
"""Add certificate revoke revocation list field
|
||||
|
||||
Revision ID: ffad172e98c1
|
||||
Revises: f21ae3f21adc
|
||||
Create Date: 2018-10-01 20:47:52.405865
|
||||
|
||||
"""
|
||||
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'ffad172e98c1'
|
||||
down_revision = 'f21ae3f21adc'
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column(u'listener',
|
||||
sa.Column(u'client_crl_container_id', sa.String(255),
|
||||
nullable=True))
|
@ -505,6 +505,7 @@ class Listener(base_models.BASE, base_models.IdMixin,
|
||||
sa.ForeignKey("client_authentication_mode.name",
|
||||
name="fk_listener_client_authentication_mode_name"),
|
||||
nullable=False, default=constants.CLIENT_AUTH_NONE)
|
||||
client_crl_container_id = sa.Column(sa.String(255), nullable=True)
|
||||
|
||||
_tags = orm.relationship(
|
||||
'Tags',
|
||||
|
@ -555,7 +555,8 @@ class TestListener(base.BaseAPITest):
|
||||
|
||||
# TODO(johnsom) Fix this when there is a noop certificate manager
|
||||
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
|
||||
def test_create(self, mock_cert_data, response_status=201, **optionals):
|
||||
def test_create(self, mock_cert_data,
|
||||
response_status=201, **optionals):
|
||||
cert1 = data_models.TLSContainer(certificate='cert 1')
|
||||
cert2 = data_models.TLSContainer(certificate='cert 2')
|
||||
cert3 = data_models.TLSContainer(certificate='cert 3')
|
||||
@ -767,6 +768,18 @@ class TestListener(base.BaseAPITest):
|
||||
self.assertIn(
|
||||
'be provided with a client CA container reference.', fault)
|
||||
|
||||
def test_create_crl_without_ca_cert(self):
|
||||
optionals = {
|
||||
'protocol': constants.PROTOCOL_TERMINATED_HTTPS,
|
||||
'client_ca_tls_container_ref': None,
|
||||
'client_crl_container_ref': uuidutils.generate_uuid()
|
||||
}
|
||||
resp = self.test_create(response_status=400, **optionals).json
|
||||
fault = resp.get('faultstring')
|
||||
self.assertIn(
|
||||
'A client authentication CA reference is required to specify a '
|
||||
'client authentication revocation list.', fault)
|
||||
|
||||
def test_create_with_default_pool_id(self):
|
||||
lb_listener = {'name': 'listener1',
|
||||
'default_pool_id': self.pool_id,
|
||||
@ -973,11 +986,36 @@ class TestListener(base.BaseAPITest):
|
||||
self.assertEqual(optionals['client_authentication'],
|
||||
listener_api.get('client_authentication'))
|
||||
|
||||
def test_create_with_ca_cert_negative_cases(self):
|
||||
# create just with option, no client_ca_tls_container_ref specified.
|
||||
def test_create_with_ca_cert_and_crl(self):
|
||||
# Load up sample certs to test the validation
|
||||
self.cert_manager_mock().get_secret.side_effect = [
|
||||
sample_certs.X509_CA_CERT, sample_certs.X509_CA_CRL,
|
||||
sample_certs.X509_CA_CERT, sample_certs.X509_CA_CRL]
|
||||
|
||||
optionals = {
|
||||
'client_authentication': constants.CLIENT_AUTH_MANDATORY
|
||||
'client_ca_tls_container_ref': uuidutils.generate_uuid(),
|
||||
'client_crl_container_ref': uuidutils.generate_uuid()
|
||||
}
|
||||
listener_api = self.test_create(**optionals)
|
||||
self.assertEqual(optionals['client_ca_tls_container_ref'],
|
||||
listener_api.get('client_ca_tls_container_ref'))
|
||||
self.assertEqual(constants.CLIENT_AUTH_NONE,
|
||||
listener_api.get('client_authentication'))
|
||||
self.assertEqual(optionals['client_crl_container_ref'],
|
||||
listener_api.get('client_crl_container_ref'))
|
||||
|
||||
# TODO(johnsom) Fix this when there is a noop certificate manager
|
||||
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
|
||||
def test_create_with_crl_mismatch_ca_cert(self, mock_cert_data):
|
||||
cert1 = data_models.TLSContainer(certificate='cert 1')
|
||||
cert2 = data_models.TLSContainer(certificate='cert 2')
|
||||
cert3 = data_models.TLSContainer(certificate='cert 3')
|
||||
mock_cert_data.return_value = {'tls_cert': cert1,
|
||||
'sni_certs': [cert2, cert3]}
|
||||
self.cert_manager_mock().get_secret.side_effect = [
|
||||
sample_certs.X509_CERT, sample_certs.X509_CA_CRL,
|
||||
sample_certs.X509_CERT, sample_certs.X509_CA_CRL]
|
||||
|
||||
sni1 = uuidutils.generate_uuid()
|
||||
sni2 = uuidutils.generate_uuid()
|
||||
lb_listener = {
|
||||
@ -989,14 +1027,45 @@ class TestListener(base.BaseAPITest):
|
||||
'default_tls_container_ref': uuidutils.generate_uuid(),
|
||||
'sni_container_refs': [sni1, sni2],
|
||||
'project_id': self.project_id,
|
||||
'loadbalancer_id': self.lb_id}
|
||||
lb_listener.update(optionals)
|
||||
'loadbalancer_id': self.lb_id,
|
||||
'client_ca_tls_container_ref': uuidutils.generate_uuid(),
|
||||
'client_crl_container_ref': uuidutils.generate_uuid()
|
||||
}
|
||||
body = self._build_body(lb_listener)
|
||||
response = self.post(self.LISTENERS_PATH, body, status=400).json
|
||||
self.assertEqual(
|
||||
"Validation failure: Client authentication setting %s "
|
||||
"requires a client CA container reference." %
|
||||
constants.CLIENT_AUTH_MANDATORY, response['faultstring'])
|
||||
"Validation failure: The CRL specified is not valid for client "
|
||||
"certificate authority reference supplied.",
|
||||
response['faultstring'])
|
||||
|
||||
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
|
||||
def test_create_with_ca_cert_negative_cases(self, mock_load_cert):
|
||||
# create just with option or crl,
|
||||
# no client_ca_tls_container_ref specified.
|
||||
sni1 = uuidutils.generate_uuid()
|
||||
sni2 = uuidutils.generate_uuid()
|
||||
|
||||
for opt in [{'client_authentication': constants.CLIENT_AUTH_MANDATORY,
|
||||
'client_crl_container_ref': uuidutils.generate_uuid()},
|
||||
{'client_authentication': constants.CLIENT_AUTH_OPTIONAL,
|
||||
'client_crl_container_ref': uuidutils.generate_uuid()}]:
|
||||
lb_listener = {
|
||||
'name': 'listener1', 'default_pool_id': None,
|
||||
'description': 'desc1',
|
||||
'admin_state_up': False,
|
||||
'protocol': constants.PROTOCOL_TERMINATED_HTTPS,
|
||||
'protocol_port': 80,
|
||||
'default_tls_container_ref': uuidutils.generate_uuid(),
|
||||
'sni_container_refs': [sni1, sni2],
|
||||
'project_id': self.project_id,
|
||||
'loadbalancer_id': self.lb_id}
|
||||
lb_listener.update(opt)
|
||||
body = self._build_body(lb_listener)
|
||||
response = self.post(self.LISTENERS_PATH, body, status=400).json
|
||||
self.assertEqual(
|
||||
"Validation failure: Client authentication setting %s "
|
||||
"requires a client CA container reference." %
|
||||
opt['client_authentication'], response['faultstring'])
|
||||
|
||||
def test_create_with_bad_ca_cert_ref(self):
|
||||
sni1 = uuidutils.generate_uuid()
|
||||
@ -1018,7 +1087,30 @@ class TestListener(base.BaseAPITest):
|
||||
self.cert_manager_mock().get_secret.side_effect = [
|
||||
Exception('bad ca cert')]
|
||||
response = self.post(self.LISTENERS_PATH, body, status=400).json
|
||||
self.assertIn(lb_listener['client_ca_tls_container_ref'],
|
||||
self.assertEqual("Could not retrieve certificate: ['%s']" %
|
||||
lb_listener['client_ca_tls_container_ref'],
|
||||
response['faultstring'])
|
||||
|
||||
def test_create_with_unreachable_crl(self):
|
||||
sni1 = uuidutils.generate_uuid()
|
||||
sni2 = uuidutils.generate_uuid()
|
||||
lb_listener = {
|
||||
'name': 'listener1', 'default_pool_id': None,
|
||||
'description': 'desc1',
|
||||
'admin_state_up': False,
|
||||
'protocol': constants.PROTOCOL_TERMINATED_HTTPS,
|
||||
'protocol_port': 80,
|
||||
'default_tls_container_ref': uuidutils.generate_uuid(),
|
||||
'sni_container_refs': [sni1, sni2],
|
||||
'project_id': self.project_id,
|
||||
'loadbalancer_id': self.lb_id,
|
||||
'client_ca_tls_container_ref': uuidutils.generate_uuid(),
|
||||
'client_crl_container_ref': uuidutils.generate_uuid()}
|
||||
body = self._build_body(lb_listener)
|
||||
self.cert_manager_mock().get_secret.side_effect = Exception(
|
||||
'bad CRL ref')
|
||||
response = self.post(self.LISTENERS_PATH, body, status=400).json
|
||||
self.assertIn(lb_listener['client_crl_container_ref'],
|
||||
response['faultstring'])
|
||||
|
||||
def test_create_with_bad_ca_cert(self):
|
||||
@ -1213,6 +1305,22 @@ class TestListener(base.BaseAPITest):
|
||||
self.assertNotEqual(ori_listener['client_authentication'],
|
||||
optionals['client_authentication'])
|
||||
|
||||
def test_update_with_crl(self):
|
||||
# Load up sample certs to test the validation
|
||||
self.cert_manager_mock().get_secret.side_effect = [
|
||||
sample_certs.X509_CA_CERT, sample_certs.X509_CA_CRL,
|
||||
sample_certs.X509_CA_CERT, sample_certs.X509_CA_CRL,
|
||||
sample_certs.X509_CA_CERT, sample_certs.X509_CA_CRL]
|
||||
|
||||
optionals = {
|
||||
'client_crl_container_ref': uuidutils.generate_uuid()
|
||||
}
|
||||
ori_listener, update_listener = self.test_update(**optionals)
|
||||
self.assertEqual(optionals['client_crl_container_ref'],
|
||||
update_listener.get('client_crl_container_ref'))
|
||||
self.assertNotEqual(ori_listener['client_crl_container_ref'],
|
||||
optionals['client_crl_container_ref'])
|
||||
|
||||
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
|
||||
def test_update_from_nonexist_ca_cert_to_new_ca_cert(self, mock_cert_data):
|
||||
cert1 = data_models.TLSContainer(certificate='cert 1')
|
||||
@ -1244,7 +1352,7 @@ class TestListener(base.BaseAPITest):
|
||||
api_listener['client_authentication'])
|
||||
|
||||
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
|
||||
def test_update_with_ca_cert_negative_cases(self, mock_cert_data):
|
||||
def test_update_with_ca_cert_missing(self, mock_cert_data):
|
||||
# update a listener, no ca cert exist
|
||||
cert1 = data_models.TLSContainer(certificate='cert 1')
|
||||
mock_cert_data.return_value = {'tls_cert': cert1}
|
||||
@ -1256,16 +1364,42 @@ class TestListener(base.BaseAPITest):
|
||||
default_tls_container_ref=tls_uuid,
|
||||
default_pool_id=None).get(self.root_tag)
|
||||
self.set_lb_status(self.lb_id)
|
||||
lb_listener = {
|
||||
'client_authentication': constants.CLIENT_AUTH_OPTIONAL}
|
||||
body = self._build_body(lb_listener)
|
||||
for opt in [{'client_authentication': constants.CLIENT_AUTH_OPTIONAL,
|
||||
'client_crl_container_ref': uuidutils.generate_uuid()},
|
||||
{'client_authentication': constants.CLIENT_AUTH_MANDATORY,
|
||||
'client_crl_container_ref': uuidutils.generate_uuid()}]:
|
||||
body = self._build_body(opt)
|
||||
listener_path = self.LISTENER_PATH.format(
|
||||
listener_id=listener['id'])
|
||||
response = self.put(listener_path, body, status=400).json
|
||||
self.assertEqual(
|
||||
"Validation failure: Client authentication setting %s "
|
||||
"requires a client CA container reference." %
|
||||
opt['client_authentication'], response['faultstring'])
|
||||
|
||||
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
|
||||
def test_update_with_crl_but_ca_cert_missing(self, mock_cert_data):
|
||||
# update a listener, no ca cert exist
|
||||
cert1 = data_models.TLSContainer(certificate='cert 1')
|
||||
mock_cert_data.return_value = {'tls_cert': cert1,
|
||||
'client_ca_cert': None}
|
||||
tls_uuid = uuidutils.generate_uuid()
|
||||
listener = self.create_listener(
|
||||
constants.PROTOCOL_TERMINATED_HTTPS, 80, self.lb_id,
|
||||
name='listener1', description='desc1',
|
||||
admin_state_up=False, connection_limit=10,
|
||||
default_tls_container_ref=tls_uuid,
|
||||
default_pool_id=None).get(self.root_tag)
|
||||
self.set_lb_status(self.lb_id)
|
||||
body = self._build_body(
|
||||
{'client_crl_container_ref': uuidutils.generate_uuid()})
|
||||
listener_path = self.LISTENER_PATH.format(
|
||||
listener_id=listener['id'])
|
||||
response = self.put(listener_path, body, status=400).json
|
||||
self.assertEqual(
|
||||
"Validation failure: Client authentication setting %s "
|
||||
"requires a client CA container reference." %
|
||||
constants.CLIENT_AUTH_OPTIONAL, response['faultstring'])
|
||||
"Validation failure: A client authentication CA reference is "
|
||||
"required to specify a client authentication revocation list.",
|
||||
response['faultstring'])
|
||||
|
||||
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
|
||||
def test_update_unset_ca_cert(self, mock_cert_data):
|
||||
@ -1290,6 +1424,41 @@ class TestListener(base.BaseAPITest):
|
||||
api_listener = self.put(listener_path, body).json.get(self.root_tag)
|
||||
self.assertIsNone(api_listener.get('client_ca_tls_container_ref'))
|
||||
self.assertIsNone(api_listener.get('client_auth_option'))
|
||||
self.assertIsNone(api_listener.get('client_crl_container_ref'))
|
||||
|
||||
@mock.patch(
|
||||
'octavia.common.tls_utils.cert_parser.load_certificates_data')
|
||||
def test_update_unset_crl(self, mock_cert_data):
|
||||
# Load up sample certs to test the validation
|
||||
self.cert_manager_mock().get_secret.side_effect = [
|
||||
sample_certs.X509_CA_CERT, sample_certs.X509_CA_CRL,
|
||||
sample_certs.X509_CA_CERT, sample_certs.X509_CA_CRL,
|
||||
sample_certs.X509_CA_CERT, sample_certs.X509_CA_CRL,
|
||||
sample_certs.X509_CA_CERT, sample_certs.X509_CA_CRL]
|
||||
|
||||
cert1 = data_models.TLSContainer(certificate='cert 1')
|
||||
mock_cert_data.return_value = {'tls_cert': cert1}
|
||||
listener = self.create_listener(
|
||||
constants.PROTOCOL_TERMINATED_HTTPS, 80, self.lb_id,
|
||||
name='listener1', description='desc1',
|
||||
admin_state_up=False, connection_limit=10,
|
||||
default_tls_container_ref=uuidutils.generate_uuid(),
|
||||
default_pool_id=None,
|
||||
client_ca_tls_container_ref=uuidutils.generate_uuid(),
|
||||
client_crl_container_ref=uuidutils.generate_uuid(),
|
||||
client_authentication=constants.CLIENT_AUTH_MANDATORY).get(
|
||||
self.root_tag)
|
||||
self.set_lb_status(self.lb_id)
|
||||
lb_listener = {'client_crl_container_ref': None}
|
||||
body = self._build_body(lb_listener)
|
||||
listener_path = self.LISTENER_PATH.format(
|
||||
listener_id=listener['id'])
|
||||
api_listener = self.put(listener_path, body).json.get(self.root_tag)
|
||||
self.assertEqual(listener.get('client_ca_tls_container_ref'),
|
||||
api_listener.get('client_ca_tls_container_ref'))
|
||||
self.assertEqual(listener.get('client_authentication'),
|
||||
api_listener.get('client_authentication'))
|
||||
self.assertIsNone(api_listener.get('client_crl_container_ref'))
|
||||
|
||||
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
|
||||
def test_update_with_bad_ca_cert(self, mock_cert_data):
|
||||
@ -1310,6 +1479,8 @@ class TestListener(base.BaseAPITest):
|
||||
self.set_lb_status(self.lb_id)
|
||||
self.cert_manager_mock().get_secret.side_effect = Exception(
|
||||
'bad ca cert')
|
||||
self.cert_manager_mock().get_secret.side_effect = Exception(
|
||||
'bad secret')
|
||||
lb_listener = {
|
||||
'client_ca_tls_container_ref': uuidutils.generate_uuid()}
|
||||
body = self._build_body(lb_listener)
|
||||
@ -1319,6 +1490,72 @@ class TestListener(base.BaseAPITest):
|
||||
self.assertIn(lb_listener['client_ca_tls_container_ref'],
|
||||
response['faultstring'])
|
||||
|
||||
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
|
||||
def test_update_with_unreachable_crl(self, mock_cert_data):
|
||||
# Load up sample certs to test the validation
|
||||
tls_cert_mock = mock.MagicMock()
|
||||
tls_cert_mock.get_certificate.return_value = sample_certs.X509_CA_CERT
|
||||
self.cert_manager_mock().get_cert.return_value = tls_cert_mock
|
||||
self.cert_manager_mock().get_secret.side_effect = [
|
||||
sample_certs.X509_CA_CERT, sample_certs.X509_CA_CRL,
|
||||
sample_certs.X509_CA_CERT, sample_certs.X509_CA_CRL,
|
||||
sample_certs.X509_CA_CERT, Exception('bad CRL ref')]
|
||||
|
||||
cert1 = data_models.TLSContainer(certificate='cert 1')
|
||||
mock_cert_data.return_value = {'tls_cert': cert1}
|
||||
listener = self.create_listener(
|
||||
constants.PROTOCOL_TERMINATED_HTTPS, 80, self.lb_id,
|
||||
name='listener1', description='desc1',
|
||||
admin_state_up=False, connection_limit=10,
|
||||
default_tls_container_ref=uuidutils.generate_uuid(),
|
||||
default_pool_id=None,
|
||||
client_ca_tls_container_ref=uuidutils.generate_uuid(),
|
||||
client_crl_container_ref=uuidutils.generate_uuid()).get(
|
||||
self.root_tag)
|
||||
self.set_lb_status(self.lb_id)
|
||||
lb_listener = {
|
||||
'client_crl_container_ref': uuidutils.generate_uuid()}
|
||||
body = self._build_body(lb_listener)
|
||||
listener_path = self.LISTENER_PATH.format(
|
||||
listener_id=listener['id'])
|
||||
response = self.put(listener_path, body, status=400).json
|
||||
self.assertIn(lb_listener['client_crl_container_ref'],
|
||||
response['faultstring'])
|
||||
|
||||
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
|
||||
def test_update_with_bad_crl(self, mock_cert_data):
|
||||
# Load up sample certs to test the validation
|
||||
tls_cert_mock = mock.MagicMock()
|
||||
tls_cert_mock.get_certificate.return_value = sample_certs.X509_CA_CERT
|
||||
self.cert_manager_mock().get_cert.return_value = tls_cert_mock
|
||||
self.cert_manager_mock().get_secret.side_effect = [
|
||||
sample_certs.X509_CA_CERT, sample_certs.X509_CA_CRL,
|
||||
sample_certs.X509_CA_CERT, sample_certs.X509_CA_CRL,
|
||||
sample_certs.X509_CA_CERT, 'bad CRL']
|
||||
|
||||
cert1 = data_models.TLSContainer(certificate='cert 1')
|
||||
mock_cert_data.return_value = {'tls_cert': cert1}
|
||||
listener = self.create_listener(
|
||||
constants.PROTOCOL_TERMINATED_HTTPS, 80, self.lb_id,
|
||||
name='listener1', description='desc1',
|
||||
admin_state_up=False, connection_limit=10,
|
||||
default_tls_container_ref=uuidutils.generate_uuid(),
|
||||
default_pool_id=None,
|
||||
client_ca_tls_container_ref=uuidutils.generate_uuid(),
|
||||
client_crl_container_ref=uuidutils.generate_uuid()).get(
|
||||
self.root_tag)
|
||||
self.set_lb_status(self.lb_id)
|
||||
lb_listener = {
|
||||
'client_crl_container_ref': uuidutils.generate_uuid()}
|
||||
body = self._build_body(lb_listener)
|
||||
listener_path = self.LISTENER_PATH.format(
|
||||
listener_id=listener['id'])
|
||||
response = self.put(listener_path, body, status=400).json
|
||||
self.assertIn("The client authentication certificate revocation list "
|
||||
"is invalid. It must be a valid x509 PEM format "
|
||||
"certificate revocation list.",
|
||||
response['faultstring'])
|
||||
|
||||
def test_update_authorized(self):
|
||||
listener = self.create_listener(
|
||||
constants.PROTOCOL_TCP, 80, self.lb_id,
|
||||
@ -1586,6 +1823,8 @@ class TestListener(base.BaseAPITest):
|
||||
listener_id=api_listener['id'])
|
||||
self.cert_manager_mock().get_cert.side_effect = [
|
||||
Exception("bad cert"), None, Exception("bad cert")]
|
||||
self.cert_manager_mock().get_secret.side_effect = [
|
||||
Exception("bad secret"), Exception("bad secret")]
|
||||
response = self.put(listener_path, body, status=400).json
|
||||
self.assertIn(tls_ref2, response['faultstring'])
|
||||
self.assertIn(sni1, response['faultstring'])
|
||||
|
@ -2358,7 +2358,9 @@ class TestLoadBalancerGraph(base.BaseAPITest):
|
||||
expected_client_ca_tls_container=None,
|
||||
create_protocol=constants.PROTOCOL_HTTP,
|
||||
create_client_authentication=None,
|
||||
expected_client_authentication=constants.CLIENT_AUTH_NONE):
|
||||
expected_client_authentication=constants.CLIENT_AUTH_NONE,
|
||||
create_client_crl_container=None,
|
||||
expected_client_crl_container=None):
|
||||
create_listener = {
|
||||
'name': name,
|
||||
'protocol_port': protocol_port,
|
||||
@ -2380,7 +2382,8 @@ class TestLoadBalancerGraph(base.BaseAPITest):
|
||||
'timeout_tcp_inspect': constants.DEFAULT_TIMEOUT_TCP_INSPECT,
|
||||
'tags': [],
|
||||
'client_ca_tls_container_ref': None,
|
||||
'client_authentication': constants.CLIENT_AUTH_NONE
|
||||
'client_authentication': constants.CLIENT_AUTH_NONE,
|
||||
'client_crl_container_ref': None
|
||||
}
|
||||
if create_sni_containers:
|
||||
create_listener['sni_container_refs'] = create_sni_containers
|
||||
@ -2402,6 +2405,9 @@ class TestLoadBalancerGraph(base.BaseAPITest):
|
||||
if create_client_authentication:
|
||||
create_listener['client_authentication'] = (
|
||||
create_client_authentication)
|
||||
if create_client_crl_container:
|
||||
create_listener['client_crl_container_ref'] = (
|
||||
create_client_crl_container)
|
||||
if expected_sni_containers:
|
||||
expected_listener['sni_container_refs'] = expected_sni_containers
|
||||
if expected_l7policies:
|
||||
@ -2416,6 +2422,9 @@ class TestLoadBalancerGraph(base.BaseAPITest):
|
||||
if expected_client_authentication:
|
||||
expected_listener[
|
||||
'client_authentication'] = expected_client_authentication
|
||||
if expected_client_crl_container:
|
||||
expected_listener['client_crl_container_ref'] = (
|
||||
expected_client_crl_container)
|
||||
return create_listener, expected_listener
|
||||
|
||||
def _get_pool_bodies(self, name='pool1', create_members=None,
|
||||
@ -2664,23 +2673,27 @@ class TestLoadBalancerGraph(base.BaseAPITest):
|
||||
self._assert_graphs_equal(expected_lb, api_lb)
|
||||
|
||||
@mock.patch('cryptography.hazmat.backends.default_backend')
|
||||
@mock.patch('cryptography.x509.load_pem_x509_crl')
|
||||
@mock.patch('cryptography.x509.load_pem_x509_certificate')
|
||||
@mock.patch('octavia.api.drivers.utils._get_secret_data')
|
||||
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
|
||||
def test_with_full_listener_certs(self, mock_cert_data, mock_get_secret,
|
||||
mock_x509_cert, mock_backend):
|
||||
mock_x509_cert, mock_x509_crl,
|
||||
mock_backend):
|
||||
cert1 = data_models.TLSContainer(certificate='cert 1')
|
||||
cert2 = data_models.TLSContainer(certificate='cert 2')
|
||||
cert3 = data_models.TLSContainer(certificate='cert 3')
|
||||
mock_get_secret.side_effect = ['ca cert', 'X509 CRL FILE']
|
||||
mock_cert_data.return_value = {'tls_cert': cert1,
|
||||
'sni_certs': [cert2, cert3]}
|
||||
mock_get_secret.side_effect = ['ca cert']
|
||||
cert_mock = mock.MagicMock()
|
||||
mock_x509_cert.return_value = cert_mock
|
||||
create_client_ca_tls_container = uuidutils.generate_uuid()
|
||||
create_client_ca_tls_container, create_client_crl_container = (
|
||||
uuidutils.generate_uuid(), uuidutils.generate_uuid())
|
||||
expected_client_ca_tls_container = create_client_ca_tls_container
|
||||
create_client_authentication = constants.CLIENT_AUTH_MANDATORY
|
||||
expected_client_authentication = constants.CLIENT_AUTH_MANDATORY
|
||||
expected_client_crl_container = create_client_crl_container
|
||||
create_sni_containers, expected_sni_containers = (
|
||||
self._get_sni_container_bodies())
|
||||
create_listener, expected_listener = self._get_listener_bodies(
|
||||
@ -2690,7 +2703,9 @@ class TestLoadBalancerGraph(base.BaseAPITest):
|
||||
create_client_ca_tls_container=create_client_ca_tls_container,
|
||||
expected_client_ca_tls_container=expected_client_ca_tls_container,
|
||||
create_client_authentication=create_client_authentication,
|
||||
expected_client_authentication=expected_client_authentication)
|
||||
expected_client_authentication=expected_client_authentication,
|
||||
create_client_crl_container=create_client_crl_container,
|
||||
expected_client_crl_container=expected_client_crl_container)
|
||||
create_lb, expected_lb = self._get_lb_bodies(
|
||||
create_listeners=[create_listener],
|
||||
expected_listeners=[expected_listener])
|
||||
|
@ -66,7 +66,7 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
|
||||
|
||||
# Build sample Listener and VIP configs
|
||||
self.sl = sample_configs.sample_listener_tuple(
|
||||
tls=True, sni=True, client_ca_cert=True)
|
||||
tls=True, sni=True, client_ca_cert=True, client_crl_cert=True)
|
||||
self.sl_udp = sample_configs.sample_listener_tuple(
|
||||
proto=constants.PROTOCOL_UDP,
|
||||
persistence_type=constants.SESSION_PERSISTENCE_SOURCE_IP,
|
||||
@ -148,7 +148,7 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
|
||||
@mock.patch('octavia.common.tls_utils.cert_parser.get_host_names')
|
||||
def test_update(self, mock_cert, mock_load_crt, mock_secret):
|
||||
mock_cert.return_value = {'cn': sample_certs.X509_CERT_CN}
|
||||
mock_secret.return_value = 'filename.pem'
|
||||
mock_secret.side_effect = ['filename.pem', 'crl-filename.pem']
|
||||
sconts = []
|
||||
for sni_container in self.sl.sni_containers:
|
||||
sconts.append(sni_container.tls_container)
|
||||
@ -168,7 +168,7 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
|
||||
self.driver.update(self.sl, self.sv)
|
||||
|
||||
# verify result
|
||||
# this is called 4 times
|
||||
# this is called 5 times
|
||||
gcm_calls = [
|
||||
mock.call(self.amp, self.sl.id,
|
||||
self.sl.default_tls_container.id + '.pem',
|
||||
@ -209,6 +209,11 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
|
||||
# start should be called once
|
||||
self.driver.client.reload_listener.assert_called_once_with(
|
||||
self.amp, self.sl.id)
|
||||
secret_calls = [
|
||||
mock.call(self.sl, self.sl.client_ca_tls_certificate_id),
|
||||
mock.call(self.sl, self.sl.client_crl_container_id)
|
||||
]
|
||||
mock_secret.assert_has_calls(secret_calls)
|
||||
|
||||
def test_udp_update(self):
|
||||
self.driver.udp_jinja.build_config.side_effect = ['fake_udp_config']
|
||||
|
@ -38,6 +38,7 @@ class SampleDriverDataModels(object):
|
||||
self.sni_container_ref_1 = uuidutils.generate_uuid()
|
||||
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.pool1_id = uuidutils.generate_uuid()
|
||||
self.pool2_id = uuidutils.generate_uuid()
|
||||
@ -383,7 +384,8 @@ class SampleDriverDataModels(object):
|
||||
'timeout_member_data': 3000,
|
||||
'timeout_tcp_inspect': 4000,
|
||||
'client_ca_tls_certificate_id': self.client_ca_tls_certificate_ref,
|
||||
'client_authentication': constants.CLIENT_AUTH_NONE
|
||||
'client_authentication': constants.CLIENT_AUTH_NONE,
|
||||
'client_crl_container_id': self.client_crl_container_ref
|
||||
}
|
||||
|
||||
self.test_listener1_dict.update(self._common_test_dict)
|
||||
@ -397,6 +399,7 @@ class SampleDriverDataModels(object):
|
||||
del self.test_listener2_dict['l7policies']
|
||||
del self.test_listener2_dict['sni_containers']
|
||||
del self.test_listener2_dict['client_ca_tls_certificate_id']
|
||||
del self.test_listener2_dict['client_crl_container_id']
|
||||
|
||||
self.test_listeners = [self.test_listener1_dict,
|
||||
self.test_listener2_dict]
|
||||
@ -416,6 +419,7 @@ class SampleDriverDataModels(object):
|
||||
cert2 = data_models.TLSContainer(certificate='cert 2')
|
||||
cert3 = data_models.TLSContainer(certificate='cert 3')
|
||||
ca_cert = 'ca cert'
|
||||
crl_file_content = 'X509 CRL FILE'
|
||||
|
||||
self.provider_listener1_dict = {
|
||||
'admin_state_up': True,
|
||||
@ -441,7 +445,9 @@ class SampleDriverDataModels(object):
|
||||
'timeout_tcp_inspect': 4000,
|
||||
'client_ca_tls_container_ref': self.client_ca_tls_certificate_ref,
|
||||
'client_ca_tls_container_data': ca_cert,
|
||||
'client_authentication': constants.CLIENT_AUTH_NONE
|
||||
'client_authentication': constants.CLIENT_AUTH_NONE,
|
||||
'client_crl_container_ref': self.client_crl_container_ref,
|
||||
'client_crl_container_data': crl_file_content
|
||||
}
|
||||
|
||||
self.provider_listener2_dict = copy.deepcopy(
|
||||
@ -456,6 +462,8 @@ class SampleDriverDataModels(object):
|
||||
del self.provider_listener2_dict['client_ca_tls_container_data']
|
||||
self.provider_listener2_dict['client_authentication'] = (
|
||||
constants.CLIENT_AUTH_NONE)
|
||||
self.provider_listener2_dict['client_crl_container_ref'] = None
|
||||
del self.provider_listener2_dict['client_crl_container_data']
|
||||
|
||||
self.provider_listener1 = driver_dm.Listener(
|
||||
**self.provider_listener1_dict)
|
||||
|
@ -90,7 +90,7 @@ 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_secret.return_value = 'ca cert'
|
||||
mock_secret.side_effect = ['ca cert', 'X509 CRL FILE']
|
||||
mock_load_cert.return_value = {'tls_cert': cert1,
|
||||
'sni_certs': [cert2, cert3]}
|
||||
test_lb_dict = {'name': 'lb1',
|
||||
@ -162,7 +162,7 @@ class TestUtils(base.TestCase):
|
||||
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
|
||||
def test_db_listeners_to_provider_listeners(self, mock_load_cert,
|
||||
mock_secret):
|
||||
mock_secret.return_value = 'ca cert'
|
||||
mock_secret.side_effect = ['ca cert', 'X509 CRL FILE']
|
||||
cert1 = data_models.TLSContainer(certificate='cert 1')
|
||||
cert2 = data_models.TLSContainer(certificate='cert 2')
|
||||
cert3 = data_models.TLSContainer(certificate='cert 3')
|
||||
@ -176,16 +176,24 @@ class TestUtils(base.TestCase):
|
||||
@mock.patch('octavia.api.drivers.utils._get_secret_data')
|
||||
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
|
||||
def test_listener_dict_to_provider_dict(self, mock_load_cert, mock_secret):
|
||||
mock_secret.return_value = 'ca cert'
|
||||
mock_secret.side_effect = ['ca cert', 'X509 CRL FILE']
|
||||
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]}
|
||||
# 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
|
||||
# different fields. That's why the test_listener1_dict from sample data
|
||||
# just contain the client_ca_tls_certificate_id for client certificate,
|
||||
# not any other related fields. So we need to delete them.
|
||||
expect_prov = copy.deepcopy(self.sample_data.provider_listener1_dict)
|
||||
provider_listener = utils.listener_dict_to_provider_dict(
|
||||
self.sample_data.test_listener1_dict)
|
||||
self.assertEqual(self.sample_data.provider_listener1_dict,
|
||||
provider_listener)
|
||||
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')
|
||||
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
|
||||
|
@ -48,7 +48,6 @@ class TestBarbicanManager(base.TestCase):
|
||||
self.fake_secret = 'Fake secret'
|
||||
self.secret = secrets.Secret(api=mock.MagicMock(),
|
||||
payload=self.fake_secret)
|
||||
|
||||
self.empty_secret = mock.Mock(spec=secrets.Secret)
|
||||
|
||||
# Mock out the client
|
||||
|
@ -40,7 +40,8 @@ class TestHaproxyCfg(base.TestCase):
|
||||
"sample_listener_id_1/tls_container_id.pem "
|
||||
"crt /var/lib/octavia/certs/sample_listener_id_1 "
|
||||
"ca-file /var/lib/octavia/certs/sample_listener_id_1/"
|
||||
"client_ca.pem verify required\n"
|
||||
"client_ca.pem verify required crl-file /var/lib/octavia/"
|
||||
"certs/sample_listener_id_1/SHA_ID.pem\n"
|
||||
" mode http\n"
|
||||
" default_backend sample_pool_id_1\n"
|
||||
" timeout client 50000\n\n").format(
|
||||
@ -71,8 +72,10 @@ class TestHaproxyCfg(base.TestCase):
|
||||
sample_configs.sample_amphora_tuple(),
|
||||
sample_configs.sample_listener_tuple(proto='TERMINATED_HTTPS',
|
||||
tls=True, sni=True,
|
||||
client_ca_cert=True),
|
||||
tls_tupe, client_ca_filename='client_ca.pem')
|
||||
client_ca_cert=True,
|
||||
client_crl_cert=True),
|
||||
tls_tupe, client_ca_filename='client_ca.pem',
|
||||
client_crl='SHA_ID.pem')
|
||||
self.assertEqual(
|
||||
sample_configs.sample_base_expected_config(
|
||||
frontend=fe, backend=be),
|
||||
|
@ -856,3 +856,22 @@ pDEMmP7TJMJ3dG63RtAzQiGfRO18BIVOrRUfQpR32FkrYd9wCE02cnv0QZzY9NYt
|
||||
6hAlAa6Motve8UFewoO4pNknj3MBEN+64wDzHaP6VPysNJwrAlgaHfGDU6xJffAd
|
||||
uCWDmw==
|
||||
-----END CERTIFICATE-----"""
|
||||
|
||||
X509_CA_CRL = b"""-----BEGIN X509 CRL-----
|
||||
MIIC7zCB2AIBATANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJVUzEPMA0GA1UE
|
||||
CAwGT3JlZ29uMRIwEAYDVQQKDAlPcGVuU3RhY2sxEDAOBgNVBAsMB09jdGF2aWEx
|
||||
FzAVBgNVBAMMDmNhLmV4YW1wbGUub3JnFw0xOTAyMTkwMjAxNTlaFw0xOTAzMjEw
|
||||
MjAxNTlaMBUwEwICEAAXDTE5MDIxOTAyMDAyMlqgMDAuMB8GA1UdIwQYMBaAFN/4
|
||||
bLQKWNMwoLzQ2du9NT33x7+DMAsGA1UdFAQEAgIQADANBgkqhkiG9w0BAQsFAAOC
|
||||
AgEAcPtYSLEkJwvqaAfMGXwI2uTTKWURqtwfcBMYdVF1u2xsBsrKR6ogpBjzc1sX
|
||||
A5WN9Tz5TXPVd38DTEGlCGLQ7wZ8wwYAR2sArHjw/zcsOJcFVTWtpX+2UAbpqis9
|
||||
rBq7K6TF2m1fYb0RJg0AUbja/wfpghoEjfFx8FjIa8WAqqazyWR9vslm7kSoEgr+
|
||||
MDV7agVK+h1n68hdLA9osUyPaAobus5FcVlXePPp5Ab8/vx1b2/Y+VXHaJXTZCin
|
||||
FLQaxaH0PsMCKN/T52GPMRKa2Cc6IEaDFgE1ZlA8nP5t2tA7MFORI8dix6jIzBJD
|
||||
W2CRf1Oxkrd3iqs1IljtlKHKMUTS67lfA9EwKlt8dR+KwH/WT23LSIoC9NnS3DP+
|
||||
aT3t52soCpjXbfl8fgs62bome1/88BoNIa2T1Mj6F0aPvepLsFB/UrXWhADFj+DX
|
||||
7WclP62BNBCTlUNvMF0eC9o7r5xeazo53KH1KI62qlFrz5MbRCG8g0JtTFqsMJld
|
||||
phYuPfZekoNbsOIPDTiPFniuP2saOF4TSRCW4KnpgblRkds6c8X+1ExdlSo5GjNa
|
||||
PftOKlYtE7T7Kw4CI9+O2H38IUOYjDt/c2twy954K4pKe4x9Ud8mImpS/oEzOsoz
|
||||
/Mn++bjO55LdaAUKQ3wa8LZ5WFB+Gs6b2kmBfzGarWEiX64=
|
||||
-----END X509 CRL-----"""
|
||||
|
@ -512,7 +512,7 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True,
|
||||
timeout_member_connect=5000,
|
||||
timeout_member_data=50000,
|
||||
timeout_tcp_inspect=0,
|
||||
client_ca_cert=False):
|
||||
client_ca_cert=False, client_crl_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
|
||||
@ -527,8 +527,9 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True,
|
||||
'sni_containers, load_balancer, peer_port, pools, '
|
||||
'l7policies, enabled, insert_headers, timeout_client_data,'
|
||||
'timeout_member_connect, timeout_member_data, '
|
||||
'timeout_tcp_inspect, client_ca_tls_certificate_id,'
|
||||
'client_ca_tls_certificate, client_authentication')
|
||||
'timeout_tcp_inspect, client_ca_tls_certificate_id, '
|
||||
'client_ca_tls_certificate, client_authentication, '
|
||||
'client_crl_container_id')
|
||||
if l7:
|
||||
pools = [
|
||||
sample_pool_tuple(
|
||||
@ -614,7 +615,8 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True,
|
||||
) if client_ca_cert else '',
|
||||
client_authentication=(
|
||||
constants.CLIENT_AUTH_MANDATORY if client_ca_cert else
|
||||
constants.CLIENT_AUTH_NONE)
|
||||
constants.CLIENT_AUTH_NONE),
|
||||
client_crl_container_id='cont_id_crl' if client_crl_cert else '',
|
||||
)
|
||||
|
||||
|
||||
|
@ -0,0 +1,10 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
You can now provide a certificate revocation list reference for listeners
|
||||
using TLS client authentication.
|
||||
security:
|
||||
- |
|
||||
Note that the amphora provider currently only supports the crl-file
|
||||
provided to check for revocation. Remote revocation lists and/or OCSP
|
||||
will not be used by the amphora provider.
|
Loading…
Reference in New Issue
Block a user