diff --git a/api-ref/source/v2/examples/pool-create-curl b/api-ref/source/v2/examples/pool-create-curl index d15b8fbb5d..191e9d5e9a 100644 --- a/api-ref/source/v2/examples/pool-create-curl +++ b/api-ref/source/v2/examples/pool-create-curl @@ -1 +1 @@ -curl -X POST -H "Content-Type: application/json" -H "X-Auth-Token: " -d '{"pool":{"lb_algorithm":"ROUND_ROBIN","protocol":"HTTP","description":"Super Round Robin Pool","admin_state_up":true,"session_persistence":{"cookie_name":"ChocolateChip","type":"APP_COOKIE"},"listener_id":"023f2e34-7806-443b-bfae-16c324569a3d","name":"super-pool","tags":["test_tag"],"tls_container_ref":"http://198.51.100.10:9311/v1/containers/4073846f-1d5e-42e1-a4cf-a7046419d0e6","ca_tls_container_ref":"http://198.51.100.10:9311/v1/containers/5f0d5540-fae6-4646-85d6-8a84883807fb","crl_container_ref":"http://198.51.100.10:9311/v1/containers/6faf0a01-6892-454c-aaac-650282820c0b","tls_enabled":true,"tls_ciphers":"ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256"}}' http://198.51.100.10:9876/v2/lbaas/pools +curl -X POST -H "Content-Type: application/json" -H "X-Auth-Token: " -d '{"pool":{"lb_algorithm":"ROUND_ROBIN","protocol":"HTTP","description":"Super Round Robin Pool","admin_state_up":true,"session_persistence":{"cookie_name":"ChocolateChip","type":"APP_COOKIE"},"listener_id":"023f2e34-7806-443b-bfae-16c324569a3d","name":"super-pool","tags":["test_tag"],"tls_container_ref":"http://198.51.100.10:9311/v1/containers/4073846f-1d5e-42e1-a4cf-a7046419d0e6","ca_tls_container_ref":"http://198.51.100.10:9311/v1/containers/5f0d5540-fae6-4646-85d6-8a84883807fb","crl_container_ref":"http://198.51.100.10:9311/v1/containers/6faf0a01-6892-454c-aaac-650282820c0b","tls_enabled":true,"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/pools diff --git a/api-ref/source/v2/examples/pool-create-request.json b/api-ref/source/v2/examples/pool-create-request.json index 0c5dbef43b..47cbdcfb02 100644 --- a/api-ref/source/v2/examples/pool-create-request.json +++ b/api-ref/source/v2/examples/pool-create-request.json @@ -15,6 +15,7 @@ "ca_tls_container_ref": "http://198.51.100.10:9311/v1/containers/5f0d5540-fae6-4646-85d6-8a84883807fb", "crl_container_ref": "http://198.51.100.10:9311/v1/containers/6faf0a01-6892-454c-aaac-650282820c0b", "tls_enabled": true, - "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/pool-create-response.json b/api-ref/source/v2/examples/pool-create-response.json index a73dc155c6..9d7ff8f2cd 100644 --- a/api-ref/source/v2/examples/pool-create-response.json +++ b/api-ref/source/v2/examples/pool-create-response.json @@ -32,6 +32,7 @@ "ca_tls_container_ref": "http://198.51.100.10:9311/v1/containers/5f0d5540-fae6-4646-85d6-8a84883807fb", "crl_container_ref": "http://198.51.100.10:9311/v1/containers/6faf0a01-6892-454c-aaac-650282820c0b", "tls_enabled": true, - "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/pool-show-response.json b/api-ref/source/v2/examples/pool-show-response.json index 14c3b666f8..ae4295e7ae 100644 --- a/api-ref/source/v2/examples/pool-show-response.json +++ b/api-ref/source/v2/examples/pool-show-response.json @@ -32,6 +32,7 @@ "ca_tls_container_ref": "http://198.51.100.10:9311/v1/containers/5f0d5540-fae6-4646-85d6-8a84883807fb", "crl_container_ref": "http://198.51.100.10:9311/v1/containers/6faf0a01-6892-454c-aaac-650282820c0b", "tls_enabled": false, - "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/pool-update-curl b/api-ref/source/v2/examples/pool-update-curl index df2de5a6a5..6a9df83ee3 100644 --- a/api-ref/source/v2/examples/pool-update-curl +++ b/api-ref/source/v2/examples/pool-update-curl @@ -1 +1 @@ -curl -X PUT -H "Content-Type: application/json" -H "X-Auth-Token: " -d '{"pool":{"lb_algorithm":"LEAST_CONNECTIONS","session_persistence":{"type":"SOURCE_IP"},"description":"second description","name":"second_name","tags":["updated_tag"],"tls_container_ref":"http://198.51.100.10:9311/v1/containers/c1cd501d-3cf9-4873-a11b-a74bebcde929","ca_tls_container_ref":null,"crl_container_ref":null,"tls_enabled":false,"tls_ciphers":"ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256"}}' http://198.51.100.10:9876/v2/lbaas/pools/4029d267-3983-4224-a3d0-afb3fe16a2cd +curl -X PUT -H "Content-Type: application/json" -H "X-Auth-Token: " -d '{"pool":{"lb_algorithm":"LEAST_CONNECTIONS","session_persistence":{"type":"SOURCE_IP"},"description":"second description","name":"second_name","tags":["updated_tag"],"tls_container_ref":"http://198.51.100.10:9311/v1/containers/c1cd501d-3cf9-4873-a11b-a74bebcde929","ca_tls_container_ref":null,"crl_container_ref":null,"tls_enabled":false,"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/pools/4029d267-3983-4224-a3d0-afb3fe16a2cd diff --git a/api-ref/source/v2/examples/pool-update-request.json b/api-ref/source/v2/examples/pool-update-request.json index 335f06f84d..519e99d378 100644 --- a/api-ref/source/v2/examples/pool-update-request.json +++ b/api-ref/source/v2/examples/pool-update-request.json @@ -11,6 +11,7 @@ "ca_tls_container_ref": null, "crl_container_ref": null, "tls_enabled": false, - "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/pool-update-response.json b/api-ref/source/v2/examples/pool-update-response.json index dc60e2876a..0e8b4faa57 100644 --- a/api-ref/source/v2/examples/pool-update-response.json +++ b/api-ref/source/v2/examples/pool-update-response.json @@ -32,6 +32,7 @@ "ca_tls_container_ref": null, "crl_container_ref": null, "tls_enabled": false, - "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/pools-list-response.json b/api-ref/source/v2/examples/pools-list-response.json index 0519d26264..0809580b80 100644 --- a/api-ref/source/v2/examples/pools-list-response.json +++ b/api-ref/source/v2/examples/pools-list-response.json @@ -38,7 +38,8 @@ "ca_tls_container_ref": "http://198.51.100.10:9311/v1/containers/5f0d5540-fae6-4646-85d6-8a84883807fb", "crl_container_ref": "http://198.51.100.10:9311/v1/containers/6faf0a01-6892-454c-aaac-650282820c0b", "tls_enabled": true, - "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/pool.inc b/api-ref/source/v2/pool.inc index e992dd4fa9..fa60721a2a 100644 --- a/api-ref/source/v2/pool.inc +++ b/api-ref/source/v2/pool.inc @@ -66,6 +66,7 @@ Response Parameters - tls_ciphers: tls_ciphers - tls_container_ref: tls_container_ref - tls_enabled: tls_enabled + - tls_versions: tls_versions - updated_at: updated_at Response Example @@ -182,6 +183,7 @@ Request - tls_enabled: tls_enabled-optional - tls_ciphers: tls_ciphers-optional - tls_container_ref: tls_container_ref-optional + - tls_versions: tls_versions-optional .. _session_persistence: @@ -264,6 +266,7 @@ Response Parameters - tls_enabled: tls_enabled - tls_ciphers: tls_ciphers - tls_container_ref: tls_container_ref + - tls_versions: tls_versions - updated_at: updated_at Response Example @@ -336,6 +339,7 @@ Response Parameters - tls_enabled: tls_enabled - tls_ciphers: tls_ciphers - tls_container_ref: tls_container_ref + - tls_versions: tls_versions - updated_at: updated_at Response Example @@ -389,6 +393,7 @@ Request - tls_enabled: tls_enabled-optional - tls_ciphers: tls_ciphers-optional - tls_container_ref: tls_container_ref-optional + - tls_versions: tls_versions-optional Request Example --------------- @@ -428,6 +433,7 @@ Response Parameters - tls_enabled: tls_enabled - tls_ciphers: tls_ciphers - tls_container_ref: tls_container_ref + - tls_versions: tls_versions - updated_at: updated_at Response Example diff --git a/etc/octavia.conf b/etc/octavia.conf index 7992976f23..906198913d 100644 --- a/etc/octavia.conf +++ b/etc/octavia.conf @@ -76,6 +76,9 @@ # listeners. Available versions: SSLv3, TLSv1, TLSv1.1, TLSv1.2, TLSv1.3 # default_listener_tls_versions = TLSv1.2, TLSv1.3 +# List of default TLS versions to be used on new TLS-enabled +# pools. Available versions: SSLv3, TLSv1, TLSv1.1, TLSv1.2, TLSv1.3 +# default_pool_tls_versions = TLSv1.2, TLSv1.3 [database] # This line MUST be changed to actually run the plugin. diff --git a/octavia/api/root_controller.py b/octavia/api/root_controller.py index 7ff0fe14af..1b407143ab 100644 --- a/octavia/api/root_controller.py +++ b/octavia/api/root_controller.py @@ -116,6 +116,9 @@ class RootController(object): 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', + self._add_a_version(versions, 'v2.17', 'v2', 'SUPPORTED', '2020-04-29T00:00:00Z', host_url) + # Pool TLS versions + self._add_a_version(versions, 'v2.18', 'v2', 'CURRENT', + '2020-04-29T01:00:00Z', host_url) return {'versions': versions} diff --git a/octavia/api/v2/controllers/pool.py b/octavia/api/v2/controllers/pool.py index b000c61f3e..dd209b50c4 100644 --- a/octavia/api/v2/controllers/pool.py +++ b/octavia/api/v2/controllers/pool.py @@ -131,6 +131,10 @@ class PoolsController(base.BaseController): 'The following ciphers have been blacklisted by an ' 'administrator: ' + ', '.join(rejected_ciphers))) + # Validate TLS version list + if pool_dict['tls_enabled']: + validate.check_tls_version_list(pool_dict['tls_versions']) + try: return self.repositories.create_pool_on_load_balancer( lock_session, pool_dict, @@ -395,6 +399,10 @@ class PoolsController(base.BaseController): "The following ciphers have been blacklisted by an " "administrator: " + ', '.join(rejected_ciphers))) + # Validate TLS version list + if pool.tls_versions is not wtypes.Unset: + validate.check_tls_version_list(pool.tls_versions) + @wsme_pecan.wsexpose(pool_types.PoolRootResponse, wtypes.text, body=pool_types.PoolRootPut, status_code=200) def put(self, id, pool_): diff --git a/octavia/api/v2/types/pool.py b/octavia/api/v2/types/pool.py index 2503f9daa8..b32aba6e48 100644 --- a/octavia/api/v2/types/pool.py +++ b/octavia/api/v2/types/pool.py @@ -84,6 +84,7 @@ class PoolResponse(BasePoolType): crl_container_ref = wtypes.wsattr(wtypes.StringType()) tls_enabled = wtypes.wsattr(bool) tls_ciphers = wtypes.StringType() + tls_versions = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType())) @classmethod def from_data_model(cls, data_model, children=False): @@ -115,6 +116,8 @@ class PoolResponse(BasePoolType): pool.members = [ member_model.from_data_model(i) for i in data_model.members] + pool.tls_versions = data_model.tls_versions + return pool @@ -160,6 +163,8 @@ class PoolPOST(BasePoolType): crl_container_ref = wtypes.wsattr(wtypes.StringType(max_length=255)) tls_enabled = wtypes.wsattr(bool, default=False) tls_ciphers = wtypes.StringType(max_length=2048) + tls_versions = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType( + max_length=32))) class PoolRootPOST(types.BaseType): @@ -180,6 +185,8 @@ class PoolPUT(BasePoolType): crl_container_ref = wtypes.wsattr(wtypes.StringType(max_length=255)) tls_enabled = wtypes.wsattr(bool) tls_ciphers = wtypes.StringType(max_length=2048) + tls_versions = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType( + max_length=32))) class PoolRootPut(types.BaseType): @@ -203,6 +210,8 @@ class PoolSingleCreate(BasePoolType): crl_container_ref = wtypes.wsattr(wtypes.StringType(max_length=255)) tls_enabled = wtypes.wsattr(bool, default=False) tls_ciphers = wtypes.StringType(max_length=2048) + tls_versions = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType( + max_length=32))) class PoolStatusResponse(BasePoolType): diff --git a/octavia/common/config.py b/octavia/common/config.py index 3cef6a3c67..a0c6a1c4e9 100644 --- a/octavia/common/config.py +++ b/octavia/common/config.py @@ -119,7 +119,11 @@ api_opts = [ 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.')) + 'listeners.')), + cfg.ListOpt('default_pool_tls_versions', + default=constants.TLS_VERSIONS_OWASP_SUITE_B, + help=_('List of TLS versions to use for new TLS-enabled ' + 'pools.')) ] # Options only used by the amphora agent diff --git a/octavia/common/data_models.py b/octavia/common/data_models.py index 3ee8bde849..0e465d7c2f 100644 --- a/octavia/common/data_models.py +++ b/octavia/common/data_models.py @@ -278,7 +278,7 @@ class Pool(BaseDataModel): created_at=None, updated_at=None, provisioning_status=None, tags=None, tls_certificate_id=None, ca_tls_certificate_id=None, crl_container_id=None, - tls_enabled=None, tls_ciphers=None): + tls_enabled=None, tls_ciphers=None, tls_versions=None): self.id = id self.project_id = project_id self.name = name @@ -303,6 +303,7 @@ class Pool(BaseDataModel): self.crl_container_id = crl_container_id self.tls_enabled = tls_enabled 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 37d938f3ae..3de83931dc 100644 --- a/octavia/common/jinja/haproxy/combined_listeners/jinja_cfg.py +++ b/octavia/common/jinja/haproxy/combined_listeners/jinja_cfg.py @@ -353,8 +353,11 @@ class JinjaTemplater(object): if (pool.tls_certificate_id and pool_tls_certs and pool_tls_certs.get('client_cert')): ret_value['client_cert'] = pool_tls_certs.get('client_cert') - if pool.tls_enabled is True and pool.tls_ciphers is not None: - ret_value['tls_ciphers'] = pool.tls_ciphers + if pool.tls_enabled is True: + if pool.tls_ciphers is not None: + ret_value['tls_ciphers'] = pool.tls_ciphers + if pool.tls_versions is not None: + ret_value['tls_versions'] = pool.tls_versions if (pool.ca_tls_certificate_id and pool_tls_certs and pool_tls_certs.get('ca_cert')): ret_value['ca_cert'] = pool_tls_certs.get('ca_cert') 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 573b98ad1f..c6cba438b2 100644 --- a/octavia/common/jinja/haproxy/combined_listeners/templates/haproxy.cfg.j2 +++ b/octavia/common/jinja/haproxy/combined_listeners/templates/haproxy.cfg.j2 @@ -33,7 +33,7 @@ {% for listener in loadbalancer.listeners if listener.enabled %} {{- frontend_macro(constants, lib_consts, listener, loadbalancer.vip_address) }} {% for pool in listener.pools if pool.enabled %} - {{- backend_macro(constants, listener, pool, loadbalancer) }} + {{- backend_macro(constants, lib_consts, listener, pool, loadbalancer) }} {% endfor %} {% endfor %} {% endif %} diff --git a/octavia/common/jinja/haproxy/combined_listeners/templates/macros.j2 b/octavia/common/jinja/haproxy/combined_listeners/templates/macros.j2 index f228d19a5e..bf44261c2f 100644 --- a/octavia/common/jinja/haproxy/combined_listeners/templates/macros.j2 +++ b/octavia/common/jinja/haproxy/combined_listeners/templates/macros.j2 @@ -180,7 +180,7 @@ frontend {{ listener.id }} {% endmacro %} -{% macro member_macro(constants, pool, member) %} +{% macro member_macro(constants, lib_consts, pool, member) %} {% if pool.health_monitor and pool.health_monitor.enabled %} {% if member.monitor_address %} {% set monitor_addr_opt = " addr %s"|format(member.monitor_address) %} @@ -254,15 +254,33 @@ frontend {{ listener.id }} {% else %} {% set ciphers_opt = "" %} {% endif %} - {{ "server %s %s:%d weight %s%s%s%s%s%s%s%s%s%s%s%s%s"|e|format( + {% set tls_versions_opt = "" %} + {% if pool.tls_versions is defined %} + {% if lib_consts.SSL_VERSION_3 not in pool.tls_versions %} + {% set tls_versions_opt = tls_versions_opt + " no-sslv3" %} + {% endif %} + {% if lib_consts.TLS_VERSION_1 not in pool.tls_versions %} + {% set tls_versions_opt = tls_versions_opt + " no-tlsv10" %} + {% endif %} + {% if lib_consts.TLS_VERSION_1_1 not in pool.tls_versions %} + {% set tls_versions_opt = tls_versions_opt + " no-tlsv11" %} + {% endif %} + {% if lib_consts.TLS_VERSION_1_2 not in pool.tls_versions %} + {% set tls_versions_opt = tls_versions_opt + " no-tlsv12" %} + {% endif %} + {% if lib_consts.TLS_VERSION_1_3 not in pool.tls_versions %} + {% set tls_versions_opt = tls_versions_opt + " no-tlsv13" %} + {% endif %} + {% endif %} + {{ "server %s %s:%d weight %s%s%s%s%s%s%s%s%s%s%s%s%s%s"|e|format( member.id, member.address, member.protocol_port, member.weight, hm_opt, persistence_opt, proxy_protocol_opt, member_backup_opt, member_enabled_opt, def_opt_prefix, def_crt_opt, ca_opt, crl_opt, - def_verify_opt, def_sni_opt, ciphers_opt)|trim() }} + def_verify_opt, def_sni_opt, ciphers_opt, tls_versions_opt)|trim() }} {% endmacro %} -{% macro backend_macro(constants, listener, pool, loadbalancer) %} +{% macro backend_macro(constants, lib_consts, listener, pool, loadbalancer) %} backend {{ pool.id }}:{{ listener.id }} {% if pool.protocol.lower() == constants.PROTOCOL_PROXY.lower() %} mode {{ listener.protocol_mode }} @@ -390,6 +408,6 @@ backend {{ pool.id }}:{{ listener.id }} timeout connect {{ listener.timeout_member_connect }} timeout server {{ listener.timeout_member_data }} {% for member in pool.members %} - {{- member_macro(constants, pool, member) -}} + {{- member_macro(constants, lib_consts, pool, member) -}} {% endfor %} {% endmacro %} diff --git a/octavia/db/migration/alembic_migrations/versions/d3c8a090f3de_add_pool_tls_versions_column.py b/octavia/db/migration/alembic_migrations/versions/d3c8a090f3de_add_pool_tls_versions_column.py new file mode 100644 index 0000000000..7bbc659009 --- /dev/null +++ b/octavia/db/migration/alembic_migrations/versions/d3c8a090f3de_add_pool_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 pool tls versions column + +Revision ID: d3c8a090f3de +Revises: e5493ae5f9a7 +Create Date: 2020-04-21 13:17:10.861932 + +""" + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = 'd3c8a090f3de' +down_revision = 'e5493ae5f9a7' + + +def upgrade(): + op.add_column( + 'pool', + sa.Column('tls_versions', sa.String(512), nullable=True) + ) diff --git a/octavia/db/models.py b/octavia/db/models.py index d1ae136f51..8b889c7512 100644 --- a/octavia/db/models.py +++ b/octavia/db/models.py @@ -338,6 +338,7 @@ class Pool(base_models.BASE, base_models.IdMixin, base_models.ProjectMixin, crl_container_id = sa.Column(sa.String(255), nullable=True) tls_enabled = sa.Column(sa.Boolean, default=False, nullable=False) tls_ciphers = sa.Column(sa.String(2048), nullable=True) + tls_versions = sa.Column(ScalarListType(), nullable=True) # This property should be a unique list of any listeners that reference # this pool as its default_pool and any listeners referenced by enabled diff --git a/octavia/db/prepare.py b/octavia/db/prepare.py index 4e6903f42f..b2bc6ec704 100644 --- a/octavia/db/prepare.py +++ b/octavia/db/prepare.py @@ -174,8 +174,12 @@ def create_pool(pool_dict, lb_id=None): prepped_members = [] for member_dict in pool_dict.get('members'): prepped_members.append(create_member(member_dict, pool_dict['id'])) - if pool_dict['tls_enabled'] is True and pool_dict['tls_ciphers'] is None: - pool_dict['tls_ciphers'] = CONF.api_settings.default_pool_ciphers + if pool_dict['tls_enabled'] is True: + if pool_dict['tls_ciphers'] is None: + pool_dict['tls_ciphers'] = CONF.api_settings.default_pool_ciphers + if pool_dict['tls_versions'] is None: + pool_dict['tls_versions'] = ( + CONF.api_settings.default_pool_tls_versions) pool_dict[constants.PROVISIONING_STATUS] = constants.PENDING_CREATE pool_dict[constants.OPERATING_STATUS] = constants.OFFLINE return pool_dict diff --git a/octavia/tests/common/sample_data_models.py b/octavia/tests/common/sample_data_models.py index 61875c88d9..d3f0b6b8c0 100644 --- a/octavia/tests/common/sample_data_models.py +++ b/octavia/tests/common/sample_data_models.py @@ -248,7 +248,9 @@ class SampleDriverDataModels(object): constants.CA_TLS_CERTIFICATE_ID: self.pool_ca_container_ref, constants.CRL_CONTAINER_ID: self.pool_crl_container_ref, lib_consts.TLS_ENABLED: True, - lib_consts.TLS_CIPHERS: None} + lib_consts.TLS_CIPHERS: None, + lib_consts.TLS_VERSIONS: None + } self.test_pool1_dict.update(self._common_test_dict) diff --git a/octavia/tests/functional/api/test_root_controller.py b/octavia/tests/functional/api/test_root_controller.py index 0fcaa82895..1bf058c7b0 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(18, len(version_ids)) + self.assertEqual(19, len(version_ids)) self.assertIn('v2.0', version_ids) self.assertIn('v2.1', version_ids) self.assertIn('v2.2', version_ids) @@ -64,6 +64,7 @@ class TestRootController(base_db_test.OctaviaDBTestBase): self.assertIn('v2.15', version_ids) self.assertIn('v2.16', version_ids) self.assertIn('v2.17', version_ids) + self.assertIn('v2.18', 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/db/test_repositories.py b/octavia/tests/functional/db/test_repositories.py index 6a6043c1c1..7892e7b1b2 100644 --- a/octavia/tests/functional/db/test_repositories.py +++ b/octavia/tests/functional/db/test_repositories.py @@ -185,7 +185,8 @@ class AllRepositoriesTest(base.OctaviaDBTestBase): 'provisioning_status': constants.ACTIVE, 'tags': ['test_tag'], 'tls_certificate_id': uuidutils.generate_uuid(), - 'tls_enabled': False, 'tls_ciphers': None} + 'tls_enabled': False, 'tls_ciphers': None, + 'tls_versions': None} pool_dm = self.repos.create_pool_on_load_balancer( self.session, pool, listener_id=self.listener.id) pool_dm_dict = pool_dm.to_dict() @@ -218,7 +219,8 @@ class AllRepositoriesTest(base.OctaviaDBTestBase): 'tags': ['test_tag'], 'tls_certificate_id': uuidutils.generate_uuid(), 'tls_enabled': False, - 'tls_ciphers': None} + 'tls_ciphers': None, + 'tls_versions': None} sp = {'type': constants.SESSION_PERSISTENCE_HTTP_COOKIE, 'cookie_name': 'cookie_monster', 'pool_id': pool['id'], @@ -262,7 +264,8 @@ class AllRepositoriesTest(base.OctaviaDBTestBase): 'id': uuidutils.generate_uuid(), 'provisioning_status': constants.ACTIVE, 'tags': ['test_tag'], 'tls_enabled': False, - 'tls_ciphers': None} + 'tls_ciphers': None, + 'tls_versions': None} pool_dm = self.repos.create_pool_on_load_balancer( self.session, pool, listener_id=self.listener.id) update_pool = {'protocol': constants.PROTOCOL_TCP, 'name': 'up_pool'} @@ -297,7 +300,8 @@ class AllRepositoriesTest(base.OctaviaDBTestBase): 'provisioning_status': constants.ACTIVE, 'tags': ['test_tag'], 'tls_certificate_id': uuidutils.generate_uuid(), - 'tls_enabled': False, 'tls_ciphers': None} + 'tls_enabled': False, 'tls_ciphers': None, + 'tls_versions': None} sp = {'type': constants.SESSION_PERSISTENCE_HTTP_COOKIE, 'cookie_name': 'cookie_monster', 'pool_id': pool['id'], @@ -401,7 +405,8 @@ class AllRepositoriesTest(base.OctaviaDBTestBase): 'project_id': uuidutils.generate_uuid(), 'id': uuidutils.generate_uuid(), 'provisioning_status': constants.ACTIVE, - 'tls_enabled': False, 'tls_ciphers': None} + 'tls_enabled': False, 'tls_ciphers': None, + 'tls_versions': None} pool_dm = self.repos.create_pool_on_load_balancer( self.session, pool, listener_id=self.listener.id) update_pool = {'tls_certificate_id': uuidutils.generate_uuid()} diff --git a/octavia/tests/unit/api/drivers/test_utils.py b/octavia/tests/unit/api/drivers/test_utils.py index b960b6d874..2a18616def 100644 --- a/octavia/tests/unit/api/drivers/test_utils.py +++ b/octavia/tests/unit/api/drivers/test_utils.py @@ -16,7 +16,6 @@ from unittest import mock from octavia_lib.api.drivers import data_models as driver_dm from octavia_lib.api.drivers import exceptions as lib_exceptions -from octavia_lib.common import constants as lib_constants from octavia.api.drivers import utils from octavia.common import constants @@ -126,10 +125,6 @@ class TestUtils(base.TestCase): 'flavor_id': 'flavor_id', 'provider': 'noop_driver'} ref_listeners = copy.deepcopy(self.sample_data.provider_listeners) - # TODO(johnsom) Remove when versions implemented - expect_pools = copy.deepcopy(self.sample_data.provider_pools,) - for pool in expect_pools: - delattr(pool, lib_constants.TLS_VERSIONS) ref_prov_lb_dict = { 'vip_address': self.sample_data.ip_address, 'admin_state_up': True, @@ -141,7 +136,7 @@ class TestUtils(base.TestCase): 'vip_port_id': self.sample_data.port_id, 'vip_qos_policy_id': self.sample_data.qos_policy_id, 'vip_network_id': self.sample_data.network_id, - 'pools': expect_pools, + 'pools': self.sample_data.provider_pools, 'flavor': {'shaved_ice': 'cherry'}, 'name': 'lb1'} vip = data_models.Vip(ip_address=self.sample_data.ip_address, @@ -241,8 +236,6 @@ class TestUtils(base.TestCase): # not any other related fields. So we need to delete them. expect_prov = copy.deepcopy(self.sample_data.provider_listener1_dict) 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['default_pool'] = expect_pool_prov provider_listener = utils.listener_dict_to_provider_dict( self.sample_data.test_listener1_dict) @@ -276,8 +269,6 @@ class TestUtils(base.TestCase): expect_prov = copy.deepcopy(self.sample_data.provider_listener1_dict) expect_pool_prov = copy.deepcopy(self.sample_data.provider_pool1_dict) 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['default_pool'] = expect_pool_prov del expect_prov['default_tls_container_data'] del expect_prov['sni_container_data'] @@ -313,10 +304,7 @@ class TestUtils(base.TestCase): 'X509 POOL CRL FILE'] provider_pool = utils.db_pool_to_provider_pool( self.sample_data.db_pool1) - # TODO(johnsom) Remove when versions and ciphers are implemented - expect_prov_pool = copy.deepcopy(self.sample_data.provider_pool1) - delattr(expect_prov_pool, lib_constants.TLS_VERSIONS) - self.assertEqual(expect_prov_pool, provider_pool) + self.assertEqual(self.sample_data.provider_pool1, provider_pool) @mock.patch('octavia.api.drivers.utils._get_secret_data') @mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data') @@ -331,10 +319,7 @@ class TestUtils(base.TestCase): test_db_pool = self.sample_data.db_pool1 test_db_pool.members = [self.sample_data.db_member1] provider_pool = utils.db_pool_to_provider_pool(test_db_pool) - # TODO(johnsom) Remove when versions and ciphers are implemented - expect_prov_pool = copy.deepcopy(self.sample_data.provider_pool1) - delattr(expect_prov_pool, lib_constants.TLS_VERSIONS) - self.assertEqual(expect_prov_pool, provider_pool) + self.assertEqual(self.sample_data.provider_pool1, provider_pool) @mock.patch('octavia.api.drivers.utils._get_secret_data') @mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data') @@ -347,11 +332,7 @@ class TestUtils(base.TestCase): 'X509 POOL CRL FILE'] provider_pools = utils.db_pools_to_provider_pools( self.sample_data.test_db_pools) - # TODO(johnsom) Remove when versions and ciphers are implemented - expect_prov_pools = copy.deepcopy(self.sample_data.provider_pools) - for prov_pool in expect_prov_pools: - delattr(prov_pool, lib_constants.TLS_VERSIONS) - self.assertEqual(expect_prov_pools, provider_pools) + self.assertEqual(self.sample_data.provider_pools, provider_pools) @mock.patch('octavia.api.drivers.utils._get_secret_data') @mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data') @@ -367,8 +348,6 @@ class TestUtils(base.TestCase): provider_pool_dict = utils.pool_dict_to_provider_dict( self.sample_data.test_pool1_dict) provider_pool_dict.pop('crl_container_ref') - # TODO(johnsom) Remove when versions and ciphers are implemented - expect_prov.pop(lib_constants.TLS_VERSIONS) self.assertEqual(expect_prov, provider_pool_dict) @mock.patch('octavia.api.drivers.utils._get_secret_data') @@ -400,8 +379,6 @@ class TestUtils(base.TestCase): provider_pool_dict = utils.pool_dict_to_provider_dict( self.sample_data.test_pool1_dict, for_delete=True) provider_pool_dict.pop('crl_container_ref') - # TODO(johnsom) Remove when versions and ciphers are implemented - expect_prov.pop(lib_constants.TLS_VERSIONS) self.assertEqual(expect_prov, provider_pool_dict) def test_db_HM_to_provider_HM(self): 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 9e8ae5d0eb..e4fd9614b4 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 @@ -993,7 +993,8 @@ class TestHaproxyCfg(base.TestCase): "{opts}\n\n").format( maxconn=constants.HAPROXY_MAX_MAXCONN, opts="ssl crt %s verify none sni ssl_fc_sni" % cert_file_path + - " ciphers " + constants.CIPHERS_OWASP_SUITE_B) + " ciphers " + constants.CIPHERS_OWASP_SUITE_B + + " no-sslv3 no-tlsv10 no-tlsv11") rendered_obj = self.jinja_cfg.render_loadbalancer_obj( sample_configs_combined.sample_amphora_tuple(), [sample_configs_combined.sample_listener_tuple( @@ -1007,6 +1008,43 @@ class TestHaproxyCfg(base.TestCase): sample_configs_combined.sample_base_expected_config(backend=be), rendered_obj) + def test_render_template_pool_cert_no_versions(self): + cert_file_path = os.path.join(self.jinja_cfg.base_crt_dir, + 'sample_listener_id_1', 'fake path') + be = ("backend sample_pool_id_1: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 " + "{opts}\n" + " server sample_member_id_2 10.0.0.98:82 weight 13 " + "check inter 30s fall 3 rise 2 cookie sample_member_id_2 " + "{opts}\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN, + opts="ssl crt %s verify none sni ssl_fc_sni" % cert_file_path + + " ciphers " + constants.CIPHERS_OWASP_SUITE_B) + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple( + pool_cert=True, tls_enabled=True, + backend_tls_ciphers=constants.CIPHERS_OWASP_SUITE_B, + backend_tls_versions=None)], + tls_certs={ + 'sample_pool_id_1': + {'client_cert': cert_file_path, + 'ca_cert': None, 'crl': None}}) + self.assertEqual( + sample_configs_combined.sample_base_expected_config(backend=be), + rendered_obj) + def test_render_template_pool_cert_no_ciphers(self): cert_file_path = os.path.join(self.jinja_cfg.base_crt_dir, 'sample_listener_id_1', 'fake path') @@ -1028,7 +1066,8 @@ class TestHaproxyCfg(base.TestCase): "check inter 30s fall 3 rise 2 cookie sample_member_id_2 " "{opts}\n\n").format( maxconn=constants.HAPROXY_MAX_MAXCONN, - opts="ssl crt %s verify none sni ssl_fc_sni" % cert_file_path) + opts="ssl crt %s verify none sni ssl_fc_sni" % cert_file_path + + " no-sslv3 no-tlsv10 no-tlsv11") rendered_obj = self.jinja_cfg.render_loadbalancer_obj( sample_configs_combined.sample_amphora_tuple(), [sample_configs_combined.sample_listener_tuple( @@ -1041,6 +1080,40 @@ class TestHaproxyCfg(base.TestCase): sample_configs_combined.sample_base_expected_config(backend=be), rendered_obj) + def test_render_template_pool_cert_no_ciphers_or_versions(self): + cert_file_path = os.path.join(self.jinja_cfg.base_crt_dir, + 'sample_listener_id_1', 'fake path') + be = ("backend sample_pool_id_1: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 " + "{opts}\n" + " server sample_member_id_2 10.0.0.98:82 weight 13 " + "check inter 30s fall 3 rise 2 cookie sample_member_id_2 " + "{opts}\n\n").format( + maxconn=constants.HAPROXY_MAX_MAXCONN, + opts="ssl crt %s verify none sni ssl_fc_sni" % cert_file_path) + rendered_obj = self.jinja_cfg.render_loadbalancer_obj( + sample_configs_combined.sample_amphora_tuple(), + [sample_configs_combined.sample_listener_tuple( + pool_cert=True, tls_enabled=True, backend_tls_versions=None)], + tls_certs={ + 'sample_pool_id_1': + {'client_cert': cert_file_path, + 'ca_cert': None, 'crl': None}}) + self.assertEqual( + sample_configs_combined.sample_base_expected_config(backend=be), + rendered_obj) + def test_render_template_with_full_pool_cert(self): pool_client_cert = '/foo/cert.pem' pool_ca_cert = '/foo/ca.pem' @@ -1067,7 +1140,7 @@ class TestHaproxyCfg(base.TestCase): "ssl", "crt", pool_client_cert, "ca-file %s" % pool_ca_cert, "crl-file %s" % pool_crl, - "verify required sni ssl_fc_sni")) + "verify required sni ssl_fc_sni no-sslv3 no-tlsv10 no-tlsv11")) rendered_obj = self.jinja_cfg.render_loadbalancer_obj( sample_configs_combined.sample_amphora_tuple(), [sample_configs_combined.sample_listener_tuple( 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 dcb3986189..8edf5d0f29 100644 --- a/octavia/tests/unit/common/sample_configs/sample_configs_combined.py +++ b/octavia/tests/unit/common/sample_configs/sample_configs_combined.py @@ -603,13 +603,17 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True, provisioning_status=constants.ACTIVE, tls_ciphers=constants.CIPHERS_OWASP_SUITE_B, backend_tls_ciphers=None, - tls_versions=constants.TLS_VERSIONS_OWASP_SUITE_B): + tls_versions=constants.TLS_VERSIONS_OWASP_SUITE_B, + backend_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 + if pool_cert is False: + backend_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 @@ -636,7 +640,8 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True, pool_crl=pool_crl, tls_enabled=tls_enabled, hm_host_http_check=hm_host_http_check, listener_id='sample_listener_id_1', - tls_ciphers=backend_tls_ciphers), + tls_ciphers=backend_tls_ciphers, + tls_versions=backend_tls_versions), sample_pool_tuple( proto=be_proto, monitor=monitor, persistence=persistence, persistence_type=persistence_type, @@ -646,7 +651,8 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True, pool_crl=pool_crl, tls_enabled=tls_enabled, hm_host_http_check=hm_host_http_check, listener_id='sample_listener_id_1', - tls_ciphers=backend_tls_ciphers)] + tls_ciphers=backend_tls_ciphers, + tls_versions=None)] l7policies = [ sample_l7policy_tuple('sample_l7policy_id_1', sample_policy=1), sample_l7policy_tuple('sample_l7policy_id_2', sample_policy=2), @@ -670,7 +676,8 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True, pool_crl=pool_crl, tls_enabled=tls_enabled, hm_host_http_check=hm_host_http_check, listener_id='sample_listener_id_1', - tls_ciphers=backend_tls_ciphers)] + tls_ciphers=backend_tls_ciphers, + tls_versions=backend_tls_versions)] l7policies = [] listener = in_listener( id=id, @@ -780,17 +787,19 @@ def sample_pool_tuple(listener_id=None, proto=None, monitor=True, pool_crl=False, tls_enabled=False, hm_host_http_check=False, provisioning_status=constants.ACTIVE, - tls_ciphers=constants.CIPHERS_OWASP_SUITE_B): + tls_ciphers=constants.CIPHERS_OWASP_SUITE_B, + tls_versions=constants.TLS_VERSIONS_OWASP_SUITE_B): proto = 'HTTP' if proto is None else proto if not tls_enabled: tls_ciphers = None + tls_versions = None monitor_proto = proto if monitor_proto is None else monitor_proto in_pool = collections.namedtuple( 'pool', 'id, protocol, lb_algorithm, members, health_monitor, ' 'session_persistence, enabled, operating_status, ' 'tls_certificate_id, ca_tls_certificate_id, ' 'crl_container_id, tls_enabled, tls_ciphers, ' - 'provisioning_status, ' + + 'tls_versions, provisioning_status, ' + constants.HTTP_REUSE) if (proto == constants.PROTOCOL_UDP and persistence_type == constants.SESSION_PERSISTENCE_SOURCE_IP): @@ -838,6 +847,7 @@ def sample_pool_tuple(listener_id=None, proto=None, monitor=True, crl_container_id='pool_crl' if pool_crl else None, tls_enabled=tls_enabled, tls_ciphers=tls_ciphers, + tls_versions=tls_versions, provisioning_status=provisioning_status) diff --git a/releasenotes/notes/pool-tls-versions-37f8036eb29ffeee.yaml b/releasenotes/notes/pool-tls-versions-37f8036eb29ffeee.yaml new file mode 100644 index 0000000000..6a71d22d86 --- /dev/null +++ b/releasenotes/notes/pool-tls-versions-37f8036eb29ffeee.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + TLS-enabled pools can now be configured to use only specified versions of + TLS. Default TLS versions for new pools can be set with + ``default_pool_tls_versions`` in ``octavia.conf``. Existing pools + will continue to use the old defaults.