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
changes/62/721362/12
Dawson Coleman 2 years ago committed by Michael Johnson
parent 8ef7d60c91
commit 6aad5d8b9f
  1. 16
      api-ref/source/parameters.yaml
  2. 2
      api-ref/source/v2/examples/listener-create-curl
  3. 3
      api-ref/source/v2/examples/listener-create-request.json
  4. 3
      api-ref/source/v2/examples/listener-create-response.json
  5. 3
      api-ref/source/v2/examples/listener-show-response.json
  6. 2
      api-ref/source/v2/examples/listener-update-curl
  7. 3
      api-ref/source/v2/examples/listener-update-request.json
  8. 3
      api-ref/source/v2/examples/listener-update-response.json
  9. 3
      api-ref/source/v2/examples/listeners-list-response.json
  10. 6
      api-ref/source/v2/listener.inc
  11. 5
      etc/octavia.conf
  12. 7
      octavia/api/root_controller.py
  13. 8
      octavia/api/v2/controllers/listener.py
  14. 9
      octavia/api/v2/types/listener.py
  15. 6
      octavia/common/config.py
  16. 11
      octavia/common/constants.py
  17. 3
      octavia/common/data_models.py
  18. 11
      octavia/common/jinja/haproxy/combined_listeners/jinja_cfg.py
  19. 2
      octavia/common/jinja/haproxy/combined_listeners/templates/haproxy.cfg.j2
  20. 26
      octavia/common/jinja/haproxy/combined_listeners/templates/macros.j2
  21. 12
      octavia/common/validate.py
  22. 35
      octavia/db/migration/alembic_migrations/versions/e5493ae5f9a7_add_listener_tls_versions_column.py
  23. 2
      octavia/db/models.py
  24. 15
      octavia/db/prepare.py
  25. 5
      octavia/tests/common/sample_data_models.py
  26. 3
      octavia/tests/functional/api/test_root_controller.py
  27. 5
      octavia/tests/functional/api/v2/test_load_balancer.py
  28. 7
      octavia/tests/unit/api/drivers/test_utils.py
  29. 116
      octavia/tests/unit/common/jinja/haproxy/combined_listeners/test_jinja_cfg.py
  30. 9
      octavia/tests/unit/common/sample_configs/sample_configs_combined.py
  31. 14
      octavia/tests/unit/common/test_validations.py
  32. 14
      releasenotes/notes/tls-versions-listeners-59cecde77e0238a0.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.

@ -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", "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: <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

@ -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"]
}
}

@ -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"]
}
}

@ -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"]
}
}

@ -1 +1 @@
curl -X PUT -H "Content-Type: application/json" -H "X-Auth-Token: <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: <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

@ -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"]
}
}

@ -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"]
}
}

@ -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"]
}
]
}

@ -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

@ -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:

@ -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}

@ -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

@ -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):

@ -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

@ -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
]

@ -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():

@ -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

@ -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 %}

@ -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) %}

@ -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)))

@ -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)
)

@ -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',

@ -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

@ -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(

@ -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'}]

@ -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

@ -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']

@ -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"

@ -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)

@ -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,
[])

@ -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.
Loading…
Cancel
Save