From 6aad5d8b9f9414b729ffff5cb0524c1368a06d3b Mon Sep 17 00:00:00 2001 From: Dawson Coleman Date: Mon, 20 Apr 2020 17:47:26 -0500 Subject: [PATCH] Add TLS version configuration for listeners Add field tls_versions to listeners for restricting TLS versions used. This is a list of versions to be used. Available values (as defined in octavia-lib): SSLv3, TLSv1, TLSv1.1, TLSv1.2, TLSv1.3 Add default_listener_tls_versions in octavia.conf. Note that at this time TLS 1.3 ciphersuites are not impelemented, so any TLS 1.3 connections will use haproxy's default ciphers instead of what's specified by tls_ciphers. Change-Id: Ic33d9b9a256490ae1b048cdfd2475d6340509fdb Story: 2006733 Task: 37170 Task: 37169 --- api-ref/source/parameters.yaml | 16 +++ .../source/v2/examples/listener-create-curl | 2 +- .../v2/examples/listener-create-request.json | 3 +- .../v2/examples/listener-create-response.json | 3 +- .../v2/examples/listener-show-response.json | 3 +- .../source/v2/examples/listener-update-curl | 2 +- .../v2/examples/listener-update-request.json | 3 +- .../v2/examples/listener-update-response.json | 3 +- .../v2/examples/listeners-list-response.json | 3 +- api-ref/source/v2/listener.inc | 6 + etc/octavia.conf | 5 + octavia/api/root_controller.py | 7 +- octavia/api/v2/controllers/listener.py | 8 ++ octavia/api/v2/types/listener.py | 9 ++ octavia/common/config.py | 6 +- octavia/common/constants.py | 11 ++ octavia/common/data_models.py | 3 +- .../haproxy/combined_listeners/jinja_cfg.py | 11 +- .../templates/haproxy.cfg.j2 | 2 +- .../combined_listeners/templates/macros.j2 | 26 +++- octavia/common/validate.py | 12 ++ ...e5f9a7_add_listener_tls_versions_column.py | 35 ++++++ octavia/db/models.py | 2 + octavia/db/prepare.py | 15 ++- octavia/tests/common/sample_data_models.py | 5 +- .../functional/api/test_root_controller.py | 3 +- .../functional/api/v2/test_load_balancer.py | 5 +- octavia/tests/unit/api/drivers/test_utils.py | 7 -- .../combined_listeners/test_jinja_cfg.py | 116 +++++++++++++++++- .../sample_configs/sample_configs_combined.py | 9 +- octavia/tests/unit/common/test_validations.py | 14 +++ ...s-versions-listeners-59cecde77e0238a0.yaml | 14 +++ 32 files changed, 325 insertions(+), 44 deletions(-) create mode 100644 octavia/db/migration/alembic_migrations/versions/e5493ae5f9a7_add_listener_tls_versions_column.py create mode 100644 releasenotes/notes/tls-versions-listeners-59cecde77e0238a0.yaml diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index 87559ea072..6e3d97ad46 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -1567,6 +1567,22 @@ tls_enabled-optional: min_version: 2.8 required: false type: boolean +tls_versions: + description: | + A list of TLS protocol versions. + Available versions: SSLv3, TLSv1, TLSv1.1, TLSv1.2, TLSv1.3 + in: body + min_version: 2.17 + required: true + type: array +tls_versions-optional: + description: | + A list of TLS protocol versions. + Available versions: SSLv3, TLSv1, TLSv1.1, TLSv1.2, TLSv1.3 + in: body + min_version: 2.17 + required: false + type: array total_connections: description: | The total connections handled. diff --git a/api-ref/source/v2/examples/listener-create-curl b/api-ref/source/v2/examples/listener-create-curl index b3c18022dc..8b2c044235 100644 --- a/api-ref/source/v2/examples/listener-create-curl +++ b/api-ref/source/v2/examples/listener-create-curl @@ -1 +1 @@ -curl -X POST -H "Content-Type: application/json" -H "X-Auth-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", "allowed_cidrs": ["192.0.2.0/24", "198.51.100.0/24"], "tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256"}}' http://198.51.100.10:9876/v2/lbaas/listeners +curl -X POST -H "Content-Type: application/json" -H "X-Auth-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", "allowed_cidrs": ["192.0.2.0/24", "198.51.100.0/24"], "tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256", "tls_versions": ["TLSv1.2", "TLSv1.3"]}}' http://198.51.100.10:9876/v2/lbaas/listeners diff --git a/api-ref/source/v2/examples/listener-create-request.json b/api-ref/source/v2/examples/listener-create-request.json index bd1477b85d..1df83d565a 100644 --- a/api-ref/source/v2/examples/listener-create-request.json +++ b/api-ref/source/v2/examples/listener-create-request.json @@ -28,6 +28,7 @@ "192.0.2.0/24", "198.51.100.0/24" ], - "tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256" + "tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256", + "tls_versions": ["TLSv1.2", "TLSv1.3"] } } diff --git a/api-ref/source/v2/examples/listener-create-response.json b/api-ref/source/v2/examples/listener-create-response.json index 73253441d1..2e21dcca07 100644 --- a/api-ref/source/v2/examples/listener-create-response.json +++ b/api-ref/source/v2/examples/listener-create-response.json @@ -43,6 +43,7 @@ "192.0.2.0/24", "198.51.100.0/24" ], - "tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256" + "tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256", + "tls_versions": ["TLSv1.2", "TLSv1.3"] } } diff --git a/api-ref/source/v2/examples/listener-show-response.json b/api-ref/source/v2/examples/listener-show-response.json index 537c6c2ab8..4bcc4cf090 100644 --- a/api-ref/source/v2/examples/listener-show-response.json +++ b/api-ref/source/v2/examples/listener-show-response.json @@ -43,6 +43,7 @@ "192.0.2.0/24", "198.51.100.0/24" ], - "tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256" + "tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256", + "tls_versions": ["TLSv1.2", "TLSv1.3"] } } diff --git a/api-ref/source/v2/examples/listener-update-curl b/api-ref/source/v2/examples/listener-update-curl index e59a220394..b80a2f395c 100644 --- a/api-ref/source/v2/examples/listener-update-curl +++ b/api-ref/source/v2/examples/listener-update-curl @@ -1 +1 @@ -curl -X PUT -H "Content-Type: application/json" -H "X-Auth-Token: " -d '{"listener": {"description": "An updated great TLS listener", "admin_state_up": true, "connection_limit": 200, "name": "great_updated_tls_listener", "insert_headers": {"X-Forwarded-For": "false", "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": 100000, "timeout_member_connect": 1000, "timeout_member_data": 100000, "timeout_tcp_inspect": 5, "tags": ["updated_tag"], "client_ca_tls_container_ref": null, "allowed_cidrs": ["192.0.2.0/24", "198.51.100.0/24"], "tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256"}}' http://198.51.100.10:9876/v2/lbaas/listeners/023f2e34-7806-443b-bfae-16c324569a3d +curl -X PUT -H "Content-Type: application/json" -H "X-Auth-Token: " -d '{"listener": {"description": "An updated great TLS listener", "admin_state_up": true, "connection_limit": 200, "name": "great_updated_tls_listener", "insert_headers": {"X-Forwarded-For": "false", "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": 100000, "timeout_member_connect": 1000, "timeout_member_data": 100000, "timeout_tcp_inspect": 5, "tags": ["updated_tag"], "client_ca_tls_container_ref": null, "allowed_cidrs": ["192.0.2.0/24", "198.51.100.0/24"], "tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256", "tls_versions": ["TLSv1.2", "TLSv1.3"]}}' http://198.51.100.10:9876/v2/lbaas/listeners/023f2e34-7806-443b-bfae-16c324569a3d diff --git a/api-ref/source/v2/examples/listener-update-request.json b/api-ref/source/v2/examples/listener-update-request.json index 8c16d7a910..0708b2eb76 100644 --- a/api-ref/source/v2/examples/listener-update-request.json +++ b/api-ref/source/v2/examples/listener-update-request.json @@ -24,6 +24,7 @@ "192.0.2.0/24", "198.51.100.0/24" ], - "tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256" + "tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256", + "tls_versions": ["TLSv1.2", "TLSv1.3"] } } diff --git a/api-ref/source/v2/examples/listener-update-response.json b/api-ref/source/v2/examples/listener-update-response.json index 9f5a7d714c..fe48622b88 100644 --- a/api-ref/source/v2/examples/listener-update-response.json +++ b/api-ref/source/v2/examples/listener-update-response.json @@ -43,6 +43,7 @@ "192.0.2.0/24", "198.51.100.0/24" ], - "tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256" + "tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256", + "tls_versions": ["TLSv1.2", "TLSv1.3"] } } diff --git a/api-ref/source/v2/examples/listeners-list-response.json b/api-ref/source/v2/examples/listeners-list-response.json index cdcfa4ccc0..4531a73ff7 100644 --- a/api-ref/source/v2/examples/listeners-list-response.json +++ b/api-ref/source/v2/examples/listeners-list-response.json @@ -45,7 +45,8 @@ "192.0.2.0/24", "198.51.100.0/24" ], - "tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256" + "tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256", + "tls_versions": ["TLSv1.2", "TLSv1.3"] } ] } diff --git a/api-ref/source/v2/listener.inc b/api-ref/source/v2/listener.inc index 4aab8ef9c1..3ac23902ca 100644 --- a/api-ref/source/v2/listener.inc +++ b/api-ref/source/v2/listener.inc @@ -73,6 +73,7 @@ Response Parameters - timeout_member_data: timeout_member_data - timeout_tcp_inspect: timeout_tcp_inspect - tls_ciphers: tls_ciphers + - tls_versions: tls_versions - updated_at: updated_at Response Example @@ -165,6 +166,7 @@ Request - timeout_member_data: timeout_member_data-optional - timeout_tcp_inspect: timeout_tcp_inspect-optional - tls_ciphers: tls_ciphers-optional + - tls_versions: tls_versions-optional .. _header_insertions: @@ -290,6 +292,7 @@ Response Parameters - timeout_member_data: timeout_member_data - timeout_tcp_inspect: timeout_tcp_inspect - tls_ciphers: tls_ciphers + - tls_versions: tls_versions - updated_at: updated_at Response Example @@ -369,6 +372,7 @@ Response Parameters - timeout_member_data: timeout_member_data - timeout_tcp_inspect: timeout_tcp_inspect - tls_ciphers: tls_ciphers + - tls_versions: tls_versions - updated_at: updated_at Response Example @@ -429,6 +433,7 @@ Request - timeout_member_data: timeout_member_data-optional - timeout_tcp_inspect: timeout_tcp_inspect-optional - tls_ciphers: tls_ciphers-optional + - tls_versions: tls_versions-optional Request Example --------------- @@ -475,6 +480,7 @@ Response Parameters - timeout_member_data: timeout_member_data - timeout_tcp_inspect: timeout_tcp_inspect - tls_ciphers: tls_ciphers + - tls_versions: tls_versions - updated_at: updated_at Response Example diff --git a/etc/octavia.conf b/etc/octavia.conf index c82ca21ac3..7992976f23 100644 --- a/etc/octavia.conf +++ b/etc/octavia.conf @@ -72,6 +72,11 @@ # allowed on listeners, pools, or the default values for either. # tls_cipher_blacklist = +# List of default TLS versions to be used on new TLS-terminated +# listeners. Available versions: SSLv3, TLSv1, TLSv1.1, TLSv1.2, TLSv1.3 +# default_listener_tls_versions = TLSv1.2, TLSv1.3 + + [database] # This line MUST be changed to actually run the plugin. # Example: diff --git a/octavia/api/root_controller.py b/octavia/api/root_controller.py index 6ae62e7df3..7ff0fe14af 100644 --- a/octavia/api/root_controller.py +++ b/octavia/api/root_controller.py @@ -109,10 +109,13 @@ class RootController(object): # Availability Zones self._add_a_version(versions, 'v2.14', 'v2', 'SUPPORTED', '2019-11-10T00:00:00Z', host_url) - # TLS version and cipher options + # TLS cipher options self._add_a_version(versions, 'v2.15', 'v2', 'SUPPORTED', '2020-03-10T00:00:00Z', host_url) # Additional UDP Healthcheck Types (HTTP/TCP) - self._add_a_version(versions, 'v2.16', 'v2', 'CURRENT', + self._add_a_version(versions, 'v2.16', 'v2', 'SUPPORTED', '2020-03-15T00:00:00Z', host_url) + # Listener TLS versions + self._add_a_version(versions, 'v2.17', 'v2', 'CURRENT', + '2020-04-29T00:00:00Z', host_url) return {'versions': versions} diff --git a/octavia/api/v2/controllers/listener.py b/octavia/api/v2/controllers/listener.py index 62ca98b68c..ee13bf490c 100644 --- a/octavia/api/v2/controllers/listener.py +++ b/octavia/api/v2/controllers/listener.py @@ -288,6 +288,10 @@ class ListenersController(base.BaseController): vip_address = vip_db.ip_address self._validate_cidr_compatible_with_vip(vip_address, allowed_cidrs) + # Validate TLS version list + if listener_protocol == constants.PROTOCOL_TERMINATED_HTTPS: + validate.check_tls_version_list(listener_dict['tls_versions']) + try: db_listener = self.repositories.listener.create( lock_session, **listener_dict) @@ -494,6 +498,10 @@ class ListenersController(base.BaseController): 'The following ciphers have been blacklisted by an ' 'administrator: ' + ', '.join(rejected_ciphers))) + # Validate TLS version list + if listener.tls_versions is not wtypes.Unset: + validate.check_tls_version_list(listener.tls_versions) + def _set_default_on_none(self, listener): """Reset settings to their default values if None/null was passed in diff --git a/octavia/api/v2/types/listener.py b/octavia/api/v2/types/listener.py index 4f2a6d0c4a..d88bbf466a 100644 --- a/octavia/api/v2/types/listener.py +++ b/octavia/api/v2/types/listener.py @@ -63,6 +63,7 @@ class ListenerResponse(BaseListenerType): client_crl_container_ref = wtypes.wsattr(wtypes.StringType()) allowed_cidrs = wtypes.wsattr([types.CidrType()]) tls_ciphers = wtypes.StringType() + tls_versions = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType())) @classmethod def from_data_model(cls, data_model, children=False): @@ -84,6 +85,8 @@ class ListenerResponse(BaseListenerType): listener.l7policies = [ l7policy_type.from_data_model(i) for i in data_model.l7policies] + listener.tls_versions = data_model.tls_versions + return listener @@ -152,6 +155,8 @@ class ListenerPOST(BaseListenerType): client_crl_container_ref = wtypes.StringType(max_length=255) allowed_cidrs = wtypes.wsattr([types.CidrType()]) tls_ciphers = wtypes.StringType(max_length=2048) + tls_versions = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType( + max_length=32))) class ListenerRootPOST(types.BaseType): @@ -190,6 +195,8 @@ class ListenerPUT(BaseListenerType): client_crl_container_ref = wtypes.StringType(max_length=255) allowed_cidrs = wtypes.wsattr([types.CidrType()]) tls_ciphers = wtypes.StringType(max_length=2048) + tls_versions = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType( + max_length=32))) class ListenerRootPUT(types.BaseType): @@ -241,6 +248,8 @@ class ListenerSingleCreate(BaseListenerType): client_crl_container_ref = wtypes.StringType(max_length=255) allowed_cidrs = wtypes.wsattr([types.CidrType()]) tls_ciphers = wtypes.StringType(max_length=2048) + tls_versions = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType( + max_length=32))) class ListenerStatusResponse(BaseListenerType): diff --git a/octavia/common/config.py b/octavia/common/config.py index 502f451ffd..3cef6a3c67 100644 --- a/octavia/common/config.py +++ b/octavia/common/config.py @@ -115,7 +115,11 @@ api_opts = [ "new TLS-enabled pools.")), cfg.StrOpt('tls_cipher_blacklist', default='', help=_("Colon separated list of OpenSSL ciphers. " - "Usage of these ciphers will be blocked.")) + "Usage of these ciphers will be blocked.")), + cfg.ListOpt('default_listener_tls_versions', + default=constants.TLS_VERSIONS_OWASP_SUITE_B, + help=_('List of TLS versions to use for new TLS-enabled ' + 'listeners.')) ] # Options only used by the amphora agent diff --git a/octavia/common/constants.py b/octavia/common/constants.py index d23a2e2d08..6cb14ba160 100644 --- a/octavia/common/constants.py +++ b/octavia/common/constants.py @@ -785,3 +785,14 @@ CIPHERS_OWASP_SUITE_B = ('TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:' 'ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-SHA256:' 'DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:' 'ECDHE-RSA-AES128-SHA256') +TLS_VERSIONS_OWASP_SUITE_B = [lib_consts.TLS_VERSION_1_2, + lib_consts.TLS_VERSION_1_3] + +# All supported TLS versions in ascending order (oldest to newest) +TLS_ALL_VERSIONS = [ + lib_consts.SSL_VERSION_3, + lib_consts.TLS_VERSION_1, + lib_consts.TLS_VERSION_1_1, + lib_consts.TLS_VERSION_1_2, + lib_consts.TLS_VERSION_1_3 +] diff --git a/octavia/common/data_models.py b/octavia/common/data_models.py index 0ad31090c7..3ee8bde849 100644 --- a/octavia/common/data_models.py +++ b/octavia/common/data_models.py @@ -390,7 +390,7 @@ class Listener(BaseDataModel): timeout_member_data=None, timeout_tcp_inspect=None, tags=None, client_ca_tls_certificate_id=None, client_authentication=None, client_crl_container_id=None, - allowed_cidrs=None, tls_ciphers=None): + allowed_cidrs=None, tls_ciphers=None, tls_versions=None): self.id = id self.project_id = project_id self.name = name @@ -424,6 +424,7 @@ class Listener(BaseDataModel): self.client_crl_container_id = client_crl_container_id self.allowed_cidrs = allowed_cidrs or [] self.tls_ciphers = tls_ciphers + self.tls_versions = tls_versions def update(self, update_dict): for key, value in update_dict.items(): diff --git a/octavia/common/jinja/haproxy/combined_listeners/jinja_cfg.py b/octavia/common/jinja/haproxy/combined_listeners/jinja_cfg.py index 9e59df0bbf..37d938f3ae 100644 --- a/octavia/common/jinja/haproxy/combined_listeners/jinja_cfg.py +++ b/octavia/common/jinja/haproxy/combined_listeners/jinja_cfg.py @@ -16,6 +16,7 @@ import os import re import jinja2 +from octavia_lib.common import constants as lib_consts from octavia.common.config import cfg from octavia.common import constants @@ -167,7 +168,7 @@ class JinjaTemplater(object): CONF.amphora_agent.administrative_log_facility, 'user_log_facility': CONF.amphora_agent.user_log_facility, 'connection_logging': self.connection_logging}, - constants=constants) + constants=constants, lib_consts=lib_consts) def _transform_loadbalancer(self, host_amphora, loadbalancer, listeners, tls_certs, feature_compatibility): @@ -282,9 +283,11 @@ class JinjaTemplater(object): os.path.join(self.base_crt_dir, loadbalancer.id, tls_certs[listener.client_crl_container_id])) - if (listener.protocol == constants.PROTOCOL_TERMINATED_HTTPS and - listener.tls_ciphers is not None): - ret_value['tls_ciphers'] = listener.tls_ciphers + if listener.protocol == constants.PROTOCOL_TERMINATED_HTTPS: + if listener.tls_ciphers is not None: + ret_value['tls_ciphers'] = listener.tls_ciphers + if listener.tls_versions is not None: + ret_value['tls_versions'] = listener.tls_versions pools = [] pool_gen = (pool for pool in listener.pools if diff --git a/octavia/common/jinja/haproxy/combined_listeners/templates/haproxy.cfg.j2 b/octavia/common/jinja/haproxy/combined_listeners/templates/haproxy.cfg.j2 index 3df3dcecc0..573b98ad1f 100644 --- a/octavia/common/jinja/haproxy/combined_listeners/templates/haproxy.cfg.j2 +++ b/octavia/common/jinja/haproxy/combined_listeners/templates/haproxy.cfg.j2 @@ -31,7 +31,7 @@ {% block proxies %} {% if loadbalancer.enabled %} {% for listener in loadbalancer.listeners if listener.enabled %} - {{- frontend_macro(constants, listener, loadbalancer.vip_address) }} + {{- frontend_macro(constants, lib_consts, listener, loadbalancer.vip_address) }} {% for pool in listener.pools if pool.enabled %} {{- backend_macro(constants, listener, pool, loadbalancer) }} {% endfor %} diff --git a/octavia/common/jinja/haproxy/combined_listeners/templates/macros.j2 b/octavia/common/jinja/haproxy/combined_listeners/templates/macros.j2 index 5d6d02a29e..f228d19a5e 100644 --- a/octavia/common/jinja/haproxy/combined_listeners/templates/macros.j2 +++ b/octavia/common/jinja/haproxy/combined_listeners/templates/macros.j2 @@ -26,7 +26,7 @@ peers {{ "%s_peers"|format(loadbalancer.id.replace("-", ""))|trim() }} {% endmacro %} -{% macro bind_macro(constants, listener, lb_vip_address) %} +{% macro bind_macro(constants, lib_consts, listener, lb_vip_address) %} {% if listener.crt_list_filename is defined %} {% set def_crt_opt = ("ssl crt-list %s"|format( listener.crt_list_filename)|trim()) %} @@ -48,8 +48,26 @@ peers {{ "%s_peers"|format(loadbalancer.id.replace("-", ""))|trim() }} {% else %} {% set ciphers_opt = "" %} {% endif %} + {% set tls_versions_opt = "" %} + {% if listener.tls_versions is defined %} + {% if lib_consts.SSL_VERSION_3 not in listener.tls_versions %} + {% set tls_versions_opt = tls_versions_opt + " no-sslv3" %} + {% endif %} + {% if lib_consts.TLS_VERSION_1 not in listener.tls_versions %} + {% set tls_versions_opt = tls_versions_opt + " no-tlsv10" %} + {% endif %} + {% if lib_consts.TLS_VERSION_1_1 not in listener.tls_versions %} + {% set tls_versions_opt = tls_versions_opt + " no-tlsv11" %} + {% endif %} + {% if lib_consts.TLS_VERSION_1_2 not in listener.tls_versions %} + {% set tls_versions_opt = tls_versions_opt + " no-tlsv12" %} + {% endif %} + {% if lib_consts.TLS_VERSION_1_3 not in listener.tls_versions %} + {% set tls_versions_opt = tls_versions_opt + " no-tlsv13" %} + {% endif %} + {% endif %} bind {{ lb_vip_address }}:{{ listener.protocol_port }} {{ -"%s %s %s %s"|format(def_crt_opt, client_ca_opt, ca_crl_opt, ciphers_opt)|trim() }} +"%s %s %s %s%s"|format(def_crt_opt, client_ca_opt, ca_crl_opt, ciphers_opt, tls_versions_opt)|trim() }} {% endmacro %} @@ -134,7 +152,7 @@ bind {{ lb_vip_address }}:{{ listener.protocol_port }} {{ {% endmacro %} -{% macro frontend_macro(constants, listener, lb_vip_address) %} +{% macro frontend_macro(constants, lib_consts, listener, lb_vip_address) %} frontend {{ listener.id }} {% if listener.connection_limit is defined %} maxconn {{ listener.connection_limit }} @@ -143,7 +161,7 @@ frontend {{ listener.id }} constants.PROTOCOL_TERMINATED_HTTPS.lower()) %} redirect scheme https if !{ ssl_fc } {% endif %} - {{ bind_macro(constants, listener, lb_vip_address)|trim() }} + {{ bind_macro(constants, lib_consts, listener, lb_vip_address)|trim() }} mode {{ listener.protocol_mode }} {% for l7policy in listener.l7policies if (l7policy.enabled and l7policy.l7rules|length > 0) %} diff --git a/octavia/common/validate.py b/octavia/common/validate.py index 60e458aee8..2dd83f24d1 100644 --- a/octavia/common/validate.py +++ b/octavia/common/validate.py @@ -459,3 +459,15 @@ def check_default_ciphers_blacklist_conflict(): raise exceptions.ValidationException( detail=_('Default pool ciphers conflict with blacklist. ' 'Conflicting ciphers: ' + ', '.join(pool_rejected))) + + +def check_tls_version_list(versions): + if versions == []: + raise exceptions.ValidationException( + detail=_('Empty TLS version list. Either specify at least one TLS ' + 'version or remove this parameter to use the default.')) + invalid_versions = [v for v in versions + if v not in constants.TLS_ALL_VERSIONS] + if invalid_versions: + raise exceptions.ValidationException( + detail=_('Invalid TLS versions: ' + ', '.join(invalid_versions))) diff --git a/octavia/db/migration/alembic_migrations/versions/e5493ae5f9a7_add_listener_tls_versions_column.py b/octavia/db/migration/alembic_migrations/versions/e5493ae5f9a7_add_listener_tls_versions_column.py new file mode 100644 index 0000000000..d39f3527c4 --- /dev/null +++ b/octavia/db/migration/alembic_migrations/versions/e5493ae5f9a7_add_listener_tls_versions_column.py @@ -0,0 +1,35 @@ +# Copyright 2020 Dawson Coleman +# +# 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 listener tls versions column + +Revision ID: e5493ae5f9a7 +Revises: fbd705961c3a +Create Date: 2020-04-19 02:35:28.502424 + +""" + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = 'e5493ae5f9a7' +down_revision = 'fbd705961c3a' + + +def upgrade(): + op.add_column( + 'listener', + sa.Column('tls_versions', sa.String(512), nullable=True) + ) diff --git a/octavia/db/models.py b/octavia/db/models.py index 410ea156ae..d1ae136f51 100644 --- a/octavia/db/models.py +++ b/octavia/db/models.py @@ -21,6 +21,7 @@ from sqlalchemy.ext import orderinglist from sqlalchemy import orm from sqlalchemy.orm import validates from sqlalchemy.sql import func +from sqlalchemy_utils import ScalarListType from octavia.api.v2.types import amphora from octavia.api.v2.types import availability_zone_profile @@ -522,6 +523,7 @@ class Listener(base_models.BASE, base_models.IdMixin, nullable=False, default=constants.CLIENT_AUTH_NONE) client_crl_container_id = sa.Column(sa.String(255), nullable=True) tls_ciphers = sa.Column(sa.String(2048), nullable=True) + tls_versions = sa.Column(ScalarListType(), nullable=True) _tags = orm.relationship( 'Tags', diff --git a/octavia/db/prepare.py b/octavia/db/prepare.py index 6a85230454..4e6903f42f 100644 --- a/octavia/db/prepare.py +++ b/octavia/db/prepare.py @@ -104,11 +104,16 @@ def create_listener(listener_dict, lb_id): if 'client_authentication' not in listener_dict: listener_dict['client_authentication'] = constants.CLIENT_AUTH_NONE - if (listener_dict['protocol'] == constants.PROTOCOL_TERMINATED_HTTPS and - ('tls_ciphers' not in listener_dict or - listener_dict['tls_ciphers'] is None)): - listener_dict['tls_ciphers'] = ( - CONF.api_settings.default_listener_ciphers) + if listener_dict['protocol'] == constants.PROTOCOL_TERMINATED_HTTPS: + if ('tls_ciphers' not in listener_dict or + listener_dict['tls_ciphers'] is None): + listener_dict['tls_ciphers'] = ( + CONF.api_settings.default_listener_ciphers) + if ('tls_versions' not in listener_dict or + listener_dict['tls_versions'] is None): + listener_dict['tls_versions'] = ( + CONF.api_settings.default_listener_tls_versions) + return listener_dict diff --git a/octavia/tests/common/sample_data_models.py b/octavia/tests/common/sample_data_models.py index bed8c33a20..61875c88d9 100644 --- a/octavia/tests/common/sample_data_models.py +++ b/octavia/tests/common/sample_data_models.py @@ -467,7 +467,8 @@ class SampleDriverDataModels(object): lib_consts.CLIENT_AUTHENTICATION: constants.CLIENT_AUTH_NONE, constants.CLIENT_CRL_CONTAINER_ID: self.client_crl_container_ref, lib_consts.ALLOWED_CIDRS: ['192.0.2.0/24', '198.51.100.0/24'], - lib_consts.TLS_CIPHERS: constants.CIPHERS_OWASP_SUITE_B + lib_consts.TLS_CIPHERS: constants.CIPHERS_OWASP_SUITE_B, + lib_consts.TLS_VERSIONS: constants.TLS_VERSIONS_OWASP_SUITE_B } self.test_listener1_dict.update(self._common_test_dict) @@ -536,7 +537,7 @@ class SampleDriverDataModels(object): lib_consts.CLIENT_CRL_CONTAINER_REF: self.client_crl_container_ref, lib_consts.CLIENT_CRL_CONTAINER_DATA: crl_file_content, lib_consts.TLS_CIPHERS: constants.CIPHERS_OWASP_SUITE_B, - lib_consts.TLS_VERSIONS: None + lib_consts.TLS_VERSIONS: constants.TLS_VERSIONS_OWASP_SUITE_B } self.provider_listener2_dict = copy.deepcopy( diff --git a/octavia/tests/functional/api/test_root_controller.py b/octavia/tests/functional/api/test_root_controller.py index 7e1af13789..0fcaa82895 100644 --- a/octavia/tests/functional/api/test_root_controller.py +++ b/octavia/tests/functional/api/test_root_controller.py @@ -45,7 +45,7 @@ class TestRootController(base_db_test.OctaviaDBTestBase): def test_api_versions(self): versions = self._get_versions_with_config() version_ids = tuple(v.get('id') for v in versions) - self.assertEqual(17, len(version_ids)) + self.assertEqual(18, len(version_ids)) self.assertIn('v2.0', version_ids) self.assertIn('v2.1', version_ids) self.assertIn('v2.2', version_ids) @@ -63,6 +63,7 @@ class TestRootController(base_db_test.OctaviaDBTestBase): self.assertIn('v2.14', version_ids) self.assertIn('v2.15', version_ids) self.assertIn('v2.16', version_ids) + self.assertIn('v2.17', version_ids) # Each version should have a 'self' 'href' to the API version URL # [{u'rel': u'self', u'href': u'http://localhost/v2'}] diff --git a/octavia/tests/functional/api/v2/test_load_balancer.py b/octavia/tests/functional/api/v2/test_load_balancer.py index dcf64c7009..474e41b6ad 100644 --- a/octavia/tests/functional/api/v2/test_load_balancer.py +++ b/octavia/tests/functional/api/v2/test_load_balancer.py @@ -2622,7 +2622,8 @@ class TestLoadBalancerGraph(base.BaseAPITest): 'client_authentication': constants.CLIENT_AUTH_NONE, 'client_crl_container_ref': None, 'allowed_cidrs': None, - 'tls_ciphers': None + 'tls_ciphers': None, + 'tls_versions': None } if create_sni_containers: create_listener['sni_container_refs'] = create_sni_containers @@ -2670,6 +2671,8 @@ class TestLoadBalancerGraph(base.BaseAPITest): expected_listener['allowed_cidrs'] = expected_allowed_cidrs if create_protocol == constants.PROTOCOL_TERMINATED_HTTPS: expected_listener['tls_ciphers'] = constants.CIPHERS_OWASP_SUITE_B + expected_listener['tls_versions'] = ( + constants.TLS_VERSIONS_OWASP_SUITE_B) return create_listener, expected_listener diff --git a/octavia/tests/unit/api/drivers/test_utils.py b/octavia/tests/unit/api/drivers/test_utils.py index 485076f71d..b960b6d874 100644 --- a/octavia/tests/unit/api/drivers/test_utils.py +++ b/octavia/tests/unit/api/drivers/test_utils.py @@ -127,8 +127,6 @@ class TestUtils(base.TestCase): 'provider': 'noop_driver'} ref_listeners = copy.deepcopy(self.sample_data.provider_listeners) # TODO(johnsom) Remove when versions implemented - for listener in ref_listeners: - delattr(listener, lib_constants.TLS_VERSIONS) expect_pools = copy.deepcopy(self.sample_data.provider_pools,) for pool in expect_pools: delattr(pool, lib_constants.TLS_VERSIONS) @@ -204,9 +202,6 @@ class TestUtils(base.TestCase): provider_listeners = utils.db_listeners_to_provider_listeners( self.sample_data.test_db_listeners) ref_listeners = copy.deepcopy(self.sample_data.provider_listeners) - # TODO(johnsom) Remove when versions implemented - for listener in ref_listeners: - delattr(listener, lib_constants.TLS_VERSIONS) self.assertEqual(ref_listeners, provider_listeners) @mock.patch('oslo_context.context.RequestContext', return_value=None) @@ -248,7 +243,6 @@ class TestUtils(base.TestCase): expect_pool_prov = copy.deepcopy(self.sample_data.provider_pool1_dict) # TODO(johnsom) Remove when versions and ciphers are implemented expect_pool_prov.pop(lib_constants.TLS_VERSIONS) - expect_prov.pop(lib_constants.TLS_VERSIONS) expect_prov['default_pool'] = expect_pool_prov provider_listener = utils.listener_dict_to_provider_dict( self.sample_data.test_listener1_dict) @@ -284,7 +278,6 @@ class TestUtils(base.TestCase): del expect_pool_prov['tls_container_data'] # TODO(johnsom) Remove when versions and ciphers are implemented expect_pool_prov.pop(lib_constants.TLS_VERSIONS) - expect_prov.pop(lib_constants.TLS_VERSIONS) expect_prov['default_pool'] = expect_pool_prov del expect_prov['default_tls_container_data'] del expect_prov['sni_container_data'] diff --git a/octavia/tests/unit/common/jinja/haproxy/combined_listeners/test_jinja_cfg.py b/octavia/tests/unit/common/jinja/haproxy/combined_listeners/test_jinja_cfg.py index 6acad643b9..9e8ae5d0eb 100644 --- a/octavia/tests/unit/common/jinja/haproxy/combined_listeners/test_jinja_cfg.py +++ b/octavia/tests/unit/common/jinja/haproxy/combined_listeners/test_jinja_cfg.py @@ -51,7 +51,8 @@ class TestHaproxyCfg(base.TestCase): "ssl crt-list {crt_list} " "ca-file /var/lib/octavia/certs/sample_loadbalancer_id_1/" "client_ca.pem verify required crl-file /var/lib/octavia/" - "certs/sample_loadbalancer_id_1/SHA_ID.pem ciphers {ciphers}\n" + "certs/sample_loadbalancer_id_1/SHA_ID.pem ciphers {ciphers} " + "no-sslv3 no-tlsv10 no-tlsv11\n" " mode http\n" " default_backend sample_pool_id_1:sample_listener_id_1\n" " timeout client 50000\n").format( @@ -104,7 +105,7 @@ class TestHaproxyCfg(base.TestCase): " maxconn {maxconn}\n" " redirect scheme https if !{{ ssl_fc }}\n" " bind 10.0.0.2:443 ssl crt-list {crt_list}" - " ciphers {ciphers}\n" + " ciphers {ciphers} no-sslv3 no-tlsv10 no-tlsv11\n" " mode http\n" " default_backend sample_pool_id_1:sample_listener_id_1\n" " timeout client 50000\n").format( @@ -153,7 +154,8 @@ class TestHaproxyCfg(base.TestCase): fe = ("frontend sample_listener_id_1\n" " maxconn {maxconn}\n" " redirect scheme https if !{{ ssl_fc }}\n" - " bind 10.0.0.2:443 ssl crt-list {crt_list}\n" + " bind 10.0.0.2:443 ssl crt-list {crt_list} " + "no-sslv3 no-tlsv10 no-tlsv11\n" " mode http\n" " default_backend sample_pool_id_1:sample_listener_id_1\n" " timeout client 50000\n").format( @@ -192,6 +194,111 @@ class TestHaproxyCfg(base.TestCase): frontend=fe, backend=be), rendered_obj) + def test_render_template_tls_no_versions(self): + conf = oslo_fixture.Config(cfg.CONF) + conf.config(group="haproxy_amphora", base_cert_dir='/fake_cert_dir') + FAKE_CRT_LIST_FILENAME = os.path.join( + CONF.haproxy_amphora.base_cert_dir, + 'sample_loadbalancer_id_1/sample_listener_id_1.pem') + fe = ("frontend sample_listener_id_1\n" + " maxconn {maxconn}\n" + " redirect scheme https if !{{ ssl_fc }}\n" + " bind 10.0.0.2:443 " + "ssl crt-list {crt_list} " + "ca-file /var/lib/octavia/certs/sample_loadbalancer_id_1/" + "client_ca.pem verify required crl-file /var/lib/octavia/" + "certs/sample_loadbalancer_id_1/SHA_ID.pem ciphers {ciphers}\n" + " mode http\n" + " default_backend sample_pool_id_1:sample_listener_id_1\n" + " timeout client 50000\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN, + crt_list=FAKE_CRT_LIST_FILENAME, + ciphers=constants.CIPHERS_OWASP_SUITE_B) + be = ("backend sample_pool_id_1:sample_listener_id_1\n" + " mode http\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " timeout check 31s\n" + " option httpchk GET /index.html HTTP/1.0\\r\\n\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\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\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + tls_tupe = {'cont_id_1': + sample_configs_combined.sample_tls_container_tuple( + id='tls_container_id', + certificate='imaCert1', private_key='imaPrivateKey1', + primary_cn='FakeCN'), + 'cont_id_ca': 'client_ca.pem', + 'cont_id_crl': 'SHA_ID.pem'} + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple( + proto='TERMINATED_HTTPS', tls=True, sni=True, + client_ca_cert=True, client_crl_cert=True, tls_versions=None)], + tls_tupe) + self.assertEqual( + sample_configs_combined.sample_base_expected_config( + frontend=fe, backend=be), + rendered_obj) + + def test_render_template_tls_no_ciphers_or_versions(self): + conf = oslo_fixture.Config(cfg.CONF) + conf.config(group="haproxy_amphora", base_cert_dir='/fake_cert_dir') + FAKE_CRT_LIST_FILENAME = os.path.join( + CONF.haproxy_amphora.base_cert_dir, + 'sample_loadbalancer_id_1/sample_listener_id_1.pem') + fe = ("frontend sample_listener_id_1\n" + " maxconn {maxconn}\n" + " redirect scheme https if !{{ ssl_fc }}\n" + " bind 10.0.0.2:443 ssl crt-list {crt_list}\n" + " mode http\n" + " default_backend sample_pool_id_1:sample_listener_id_1\n" + " timeout client 50000\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN, + crt_list=FAKE_CRT_LIST_FILENAME) + be = ("backend sample_pool_id_1:sample_listener_id_1\n" + " mode http\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " timeout check 31s\n" + " option httpchk GET /index.html HTTP/1.0\\r\\n\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\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\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN) + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple( + proto='TERMINATED_HTTPS', tls=True, tls_ciphers=None, + tls_versions=None)], + tls_certs={'cont_id_1': + sample_configs_combined.sample_tls_container_tuple( + id='tls_container_id', + certificate='ImAalsdkfjCert', + private_key='ImAsdlfksdjPrivateKey', + primary_cn="FakeCN")}) + self.assertEqual( + sample_configs_combined.sample_base_expected_config( + frontend=fe, backend=be), + rendered_obj) + def test_render_template_http(self): be = ("backend sample_pool_id_1:sample_listener_id_1\n" " mode http\n" @@ -1180,7 +1287,8 @@ class TestHaproxyCfg(base.TestCase): " maxconn 1000000\n" " redirect scheme https if !{ ssl_fc }\n" " bind 10.0.0.2:443 ciphers " + - constants.CIPHERS_OWASP_SUITE_B + "\n" + constants.CIPHERS_OWASP_SUITE_B + + " no-sslv3 no-tlsv10 no-tlsv11\n" " mode http\n" " acl sample_l7rule_id_1 path -m beg /api\n" " use_backend sample_pool_id_2:sample_listener_id_1" diff --git a/octavia/tests/unit/common/sample_configs/sample_configs_combined.py b/octavia/tests/unit/common/sample_configs/sample_configs_combined.py index c0483894c3..dcb3986189 100644 --- a/octavia/tests/unit/common/sample_configs/sample_configs_combined.py +++ b/octavia/tests/unit/common/sample_configs/sample_configs_combined.py @@ -602,12 +602,14 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True, id='sample_listener_id_1', recursive_nest=False, provisioning_status=constants.ACTIVE, tls_ciphers=constants.CIPHERS_OWASP_SUITE_B, - backend_tls_ciphers=None): + backend_tls_ciphers=None, + tls_versions=constants.TLS_VERSIONS_OWASP_SUITE_B): proto = 'HTTP' if proto is None else proto if be_proto is None: be_proto = 'HTTP' if proto == 'TERMINATED_HTTPS' else proto if proto != constants.PROTOCOL_TERMINATED_HTTPS: tls_ciphers = None + tls_versions = None topology = 'SINGLE' if topology is None else topology port = '443' if proto in ['HTTPS', 'TERMINATED_HTTPS'] else '80' peer_port = 1024 if peer_port is None else peer_port @@ -622,7 +624,7 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True, 'timeout_tcp_inspect, client_ca_tls_certificate_id, ' 'client_ca_tls_certificate, client_authentication, ' 'client_crl_container_id, provisioning_status, ' - 'tls_ciphers') + 'tls_ciphers, tls_versions') if l7: pools = [ sample_pool_tuple( @@ -737,7 +739,8 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True, constants.CLIENT_AUTH_NONE), client_crl_container_id='cont_id_crl' if client_crl_cert else '', provisioning_status=provisioning_status, - tls_ciphers=tls_ciphers + tls_ciphers=tls_ciphers, + tls_versions=tls_versions ) if recursive_nest: listener.load_balancer.listeners.append(listener) diff --git a/octavia/tests/unit/common/test_validations.py b/octavia/tests/unit/common/test_validations.py index d7d2e1d8f3..383014743c 100644 --- a/octavia/tests/unit/common/test_validations.py +++ b/octavia/tests/unit/common/test_validations.py @@ -470,3 +470,17 @@ class TestValidations(base.TestCase): self.assertRaises(exceptions.ValidationException, validate.check_default_ciphers_blacklist_conflict) + + def test_check_tls_version_list(self): + # Test valid list + validate.check_tls_version_list(['TLSv1.1', 'TLSv1.2', 'TLSv1.3']) + # Test invalid list + self.assertRaises( + exceptions.ValidationException, + validate.check_tls_version_list, + ['SSLv3', 'TLSv1.0']) + # Test empty list + self.assertRaises( + exceptions.ValidationException, + validate.check_tls_version_list, + []) diff --git a/releasenotes/notes/tls-versions-listeners-59cecde77e0238a0.yaml b/releasenotes/notes/tls-versions-listeners-59cecde77e0238a0.yaml new file mode 100644 index 0000000000..947d55ab94 --- /dev/null +++ b/releasenotes/notes/tls-versions-listeners-59cecde77e0238a0.yaml @@ -0,0 +1,14 @@ +--- +features: + - | + HTTPS-terminated listeners can now be configured to use only specified + versions of TLS. Default TLS versions for new listeners can be set with + ``default_listener_tls_versions`` in ``octavia.conf``. Existing listeners + will continue to use the old defaults. +upgrade: + - | + HTTPS-terminated listeners will now only allow TLS1.2 and TLS1.3 by + default. If no TLS versions are specified at listener create time, the + listener will only accept TLS1.2 and TLS1.3 connections. Previously TLS + listeners would accept any TLS version. Existing listeners will not be + changed.