Add support for HTTP Strict Transport Security
Closes-Bug: #2017972 Depends-on: https://review.opendev.org/c/openstack/octavia-lib/+/880821 Change-Id: I0f2f2ff6b8c430b2dd06d707097af74bb608dcc9
This commit is contained in:
parent
db03617acb
commit
c907547512
@ -808,6 +808,62 @@ healthmonitor-url_path-optional:
|
||||
in: body
|
||||
required: false
|
||||
type: string
|
||||
hsts_include_subdomains:
|
||||
description: |
|
||||
Defines whether the ``includeSubDomains`` directive should be
|
||||
added to the Strict-Transport-Security HTTP response
|
||||
header.
|
||||
in: body
|
||||
min_version: 2.27
|
||||
required: true
|
||||
type: bool
|
||||
hsts_include_subdomains-optional:
|
||||
description: |
|
||||
Defines whether the ``includeSubDomains`` directive should be
|
||||
added to the Strict-Transport-Security HTTP response
|
||||
header. This requires setting the ``hsts_max_age`` option as well in
|
||||
order to become effective.
|
||||
in: body
|
||||
min_version: 2.27
|
||||
required: false
|
||||
type: bool
|
||||
hsts_max_age:
|
||||
description: |
|
||||
The value of the ``max_age`` directive for the
|
||||
Strict-Transport-Security HTTP response header.
|
||||
in: body
|
||||
min_version: 2.27
|
||||
required: true
|
||||
type: integer
|
||||
hsts_max_age-optional:
|
||||
description: |
|
||||
The value of the ``max_age`` directive for the
|
||||
Strict-Transport-Security HTTP response header.
|
||||
Setting this enables HTTP Strict Transport
|
||||
Security (HSTS) for the TLS-terminated listener.
|
||||
in: body
|
||||
min_version: 2.27
|
||||
required: false
|
||||
type: integer
|
||||
hsts_preload:
|
||||
description: |
|
||||
Defines whether the ``preload`` directive should be
|
||||
added to the Strict-Transport-Security HTTP response
|
||||
header.
|
||||
in: body
|
||||
min_version: 2.27
|
||||
required: true
|
||||
type: bool
|
||||
hsts_preload-optional:
|
||||
description: |
|
||||
Defines whether the ``preload`` directive should be
|
||||
added to the Strict-Transport-Security HTTP response
|
||||
header. This requires setting the ``hsts_max_age`` option as well in
|
||||
order to become effective.
|
||||
in: body
|
||||
min_version: 2.27
|
||||
required: false
|
||||
type: bool
|
||||
id:
|
||||
description: |
|
||||
The ID of the resource.
|
||||
|
@ -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", "tls_versions": ["TLSv1.2", "TLSv1.3"], "alpn_protocols": ["http/1.1", "http/1.0"]}}' 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"], "alpn_protocols": ["http/1.1", "http/1.0"], "hsts_include_subdomains": true, "hsts_max_age": 31536000, "hsts_preload": true}}' http://198.51.100.10:9876/v2/lbaas/listeners
|
||||
|
@ -30,6 +30,9 @@
|
||||
],
|
||||
"tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256",
|
||||
"tls_versions": ["TLSv1.2", "TLSv1.3"],
|
||||
"alpn_protocols": ["http/1.1", "http/1.0"]
|
||||
"alpn_protocols": ["http/1.1", "http/1.0"],
|
||||
"hsts_include_subdomains": true,
|
||||
"hsts_max_age": 31536000,
|
||||
"hsts_preload": true
|
||||
}
|
||||
}
|
||||
|
@ -44,7 +44,16 @@
|
||||
"198.51.100.0/24"
|
||||
],
|
||||
"tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256",
|
||||
"tls_versions": ["TLSv1.2", "TLSv1.3"],
|
||||
"alpn_protocols": ["http/1.1", "http/1.0"]
|
||||
"tls_versions": [
|
||||
"TLSv1.2",
|
||||
"TLSv1.3"
|
||||
],
|
||||
"alpn_protocols": [
|
||||
"http/1.1",
|
||||
"http/1.0"
|
||||
],
|
||||
"hsts_include_subdomains": true,
|
||||
"hsts_max_age": 31536000,
|
||||
"hsts_preload": true
|
||||
}
|
||||
}
|
||||
|
@ -44,7 +44,16 @@
|
||||
"198.51.100.0/24"
|
||||
],
|
||||
"tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256",
|
||||
"tls_versions": ["TLSv1.2", "TLSv1.3"],
|
||||
"alpn_protocols": ["http/1.1", "http/1.0"]
|
||||
"tls_versions": [
|
||||
"TLSv1.2",
|
||||
"TLSv1.3"
|
||||
],
|
||||
"alpn_protocols": [
|
||||
"http/1.1",
|
||||
"http/1.0"
|
||||
],
|
||||
"hsts_include_subdomains": true,
|
||||
"hsts_max_age": 31536000,
|
||||
"hsts_preload": true
|
||||
}
|
||||
}
|
||||
|
@ -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", "tls_versions": ["TLSv1.2", "TLSv1.3"], "alpn_protocols": ["http/1.1", "http/1.0"]}}' 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"], "alpn_protocols": ["http/1.1", "http/1.0"], "hsts_include_subdomains": true, "hsts_max_age": 31536000, "hsts_preload": true}}' http://198.51.100.10:9876/v2/lbaas/listeners/023f2e34-7806-443b-bfae-16c324569a3d
|
||||
|
@ -18,14 +18,25 @@
|
||||
"timeout_member_connect": 1000,
|
||||
"timeout_member_data": 100000,
|
||||
"timeout_tcp_inspect": 5,
|
||||
"tags": ["updated_tag"],
|
||||
"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"],
|
||||
"alpn_protocols": ["http/1.1", "http/1.0"]
|
||||
"tls_versions": [
|
||||
"TLSv1.2",
|
||||
"TLSv1.3"
|
||||
],
|
||||
"alpn_protocols": [
|
||||
"http/1.1",
|
||||
"http/1.0"
|
||||
],
|
||||
"hsts_include_subdomains": true,
|
||||
"hsts_max_age": 31536000,
|
||||
"hsts_preload": true
|
||||
}
|
||||
}
|
||||
|
@ -44,7 +44,16 @@
|
||||
"198.51.100.0/24"
|
||||
],
|
||||
"tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256",
|
||||
"tls_versions": ["TLSv1.2", "TLSv1.3"],
|
||||
"alpn_protocols": ["http/1.1", "http/1.0"]
|
||||
"tls_versions": [
|
||||
"TLSv1.2",
|
||||
"TLSv1.3"
|
||||
],
|
||||
"alpn_protocols": [
|
||||
"http/1.1",
|
||||
"http/1.0"
|
||||
],
|
||||
"hsts_include_subdomains": true,
|
||||
"hsts_max_age": 31536000,
|
||||
"hsts_preload": true
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,9 @@
|
||||
"timeout_member_connect": 5000,
|
||||
"timeout_member_data": 50000,
|
||||
"timeout_tcp_inspect": 0,
|
||||
"tags": ["test_tag"],
|
||||
"tags": [
|
||||
"test_tag"
|
||||
],
|
||||
"client_ca_tls_container_ref": "http://198.51.100.10:9311/v1/containers/35649991-49f3-4625-81ce-2465fe8932e5",
|
||||
"client_authentication": "NONE",
|
||||
"client_crl_container_ref": "http://198.51.100.10:9311/v1/containers/e222b065-b93b-4e2a-9a02-804b7a118c3c",
|
||||
@ -46,8 +48,17 @@
|
||||
"198.51.100.0/24"
|
||||
],
|
||||
"tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256",
|
||||
"tls_versions": ["TLSv1.2", "TLSv1.3"],
|
||||
"alpn_protocols": ["http/1.1", "http/1.0"]
|
||||
"tls_versions": [
|
||||
"TLSv1.2",
|
||||
"TLSv1.3"
|
||||
],
|
||||
"alpn_protocols": [
|
||||
"http/1.1",
|
||||
"http/1.0"
|
||||
],
|
||||
"hsts_include_subdomains": true,
|
||||
"hsts_max_age": 31536000,
|
||||
"hsts_preload": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -56,6 +56,9 @@ Response Parameters
|
||||
- default_pool_id: default_pool_id
|
||||
- default_tls_container_ref: default_tls_container_ref
|
||||
- description: description
|
||||
- hsts_include_subdomains: hsts_include_subdomains
|
||||
- hsts_max_age: hsts_max_age
|
||||
- hsts_preload: hsts_preload
|
||||
- id: listener-id
|
||||
- insert_headers: insert_headers
|
||||
- l7policies: l7policy-ids
|
||||
@ -153,6 +156,9 @@ Request
|
||||
- default_pool_id: default_pool_id-optional
|
||||
- default_tls_container_ref: default_tls_container_ref-optional
|
||||
- description: description-optional
|
||||
- hsts_include_subdomains: hsts_include_subdomains-optional
|
||||
- hsts_max_age: hsts_max_age-optional
|
||||
- hsts_preload: hsts_preload-optional
|
||||
- insert_headers: insert_headers-optional
|
||||
- l7policies: l7policies-optional
|
||||
- listeners: listener
|
||||
@ -277,6 +283,9 @@ Response Parameters
|
||||
- default_pool_id: default_pool_id
|
||||
- default_tls_container_ref: default_tls_container_ref
|
||||
- description: description
|
||||
- hsts_include_subdomains: hsts_include_subdomains
|
||||
- hsts_max_age: hsts_max_age
|
||||
- hsts_preload: hsts_preload
|
||||
- id: listener-id
|
||||
- insert_headers: insert_headers
|
||||
- l7policies: l7policy-ids
|
||||
@ -358,6 +367,9 @@ Response Parameters
|
||||
- default_pool_id: default_pool_id
|
||||
- default_tls_container_ref: default_tls_container_ref
|
||||
- description: description
|
||||
- hsts_include_subdomains: hsts_include_subdomains
|
||||
- hsts_max_age: hsts_max_age
|
||||
- hsts_preload: hsts_preload
|
||||
- id: listener-id
|
||||
- insert_headers: insert_headers
|
||||
- l7policies: l7policy-ids
|
||||
@ -428,6 +440,9 @@ Request
|
||||
- default_pool_id: default_pool_id-optional
|
||||
- default_tls_container_ref: default_tls_container_ref-optional
|
||||
- description: description-optional
|
||||
- hsts_include_subdomains: hsts_include_subdomains-optional
|
||||
- hsts_max_age: hsts_max_age-optional
|
||||
- hsts_preload: hsts_preload-optional
|
||||
- insert_headers: insert_headers-optional
|
||||
- listener_id: path-listener-id
|
||||
- name: name-optional
|
||||
@ -468,6 +483,9 @@ Response Parameters
|
||||
- default_pool_id: default_pool_id
|
||||
- default_tls_container_ref: default_tls_container_ref
|
||||
- description: description
|
||||
- hsts_include_subdomains: hsts_include_subdomains
|
||||
- hsts_max_age: hsts_max_age
|
||||
- hsts_preload: hsts_preload
|
||||
- id: listener-id
|
||||
- insert_headers: insert_headers
|
||||
- l7policies: l7policy-ids
|
||||
|
@ -440,6 +440,13 @@ balancer features, like Layer 7 features and header manipulation.
|
||||
openstack loadbalancer member create --subnet-id private-subnet --address 192.0.2.10 --protocol-port 80 pool1
|
||||
openstack loadbalancer member create --subnet-id private-subnet --address 192.0.2.11 --protocol-port 80 pool1
|
||||
|
||||
.. note::
|
||||
A good security practise for production servers is to enable
|
||||
HTTP Strict Transport Security (HSTS),
|
||||
which can be configured during listener creation using the
|
||||
``--hsts-max-age`` option and optionally ``--hsts-include-subdomains``
|
||||
``--hsts-prefetch``.
|
||||
|
||||
|
||||
Deploy a TLS-terminated HTTPS load balancer with SNI
|
||||
----------------------------------------------------
|
||||
|
@ -143,6 +143,9 @@ class RootController(object):
|
||||
self._add_a_version(versions, 'v2.25', 'v2', 'SUPPORTED',
|
||||
'2021-10-02T00:00:00Z', host_url)
|
||||
# Additional VIPs
|
||||
self._add_a_version(versions, 'v2.26', 'v2', 'CURRENT',
|
||||
self._add_a_version(versions, 'v2.26', 'v2', 'SUPPORTED',
|
||||
'2022-08-29T00:00:00Z', host_url)
|
||||
# HTTP Strict Transport Security (HSTS)
|
||||
self._add_a_version(versions, 'v2.27', 'v2', 'CURRENT',
|
||||
'2023-05-05T00:00:00Z', host_url)
|
||||
return {'versions': versions}
|
||||
|
@ -324,6 +324,8 @@ class ListenersController(base.BaseController):
|
||||
# Validate ALPN protocol list
|
||||
validate.check_alpn_protocols(listener_dict['alpn_protocols'])
|
||||
|
||||
validate.check_hsts_options(listener_dict)
|
||||
|
||||
try:
|
||||
db_listener = self.repositories.listener.create(
|
||||
lock_session, **listener_dict)
|
||||
@ -345,7 +347,6 @@ class ListenersController(base.BaseController):
|
||||
except odb_exceptions.DBError as e:
|
||||
raise exceptions.InvalidOption(value=listener_dict.get('protocol'),
|
||||
option='protocol') from e
|
||||
return None
|
||||
|
||||
@wsme_pecan.wsexpose(listener_types.ListenerRootResponse,
|
||||
body=listener_types.ListenerRootPOST, status_code=201)
|
||||
@ -557,6 +558,8 @@ class ListenersController(base.BaseController):
|
||||
# Validate ALPN protocol list
|
||||
validate.check_alpn_protocols(listener.alpn_protocols)
|
||||
|
||||
validate.check_hsts_options_put(listener, db_listener)
|
||||
|
||||
def _set_default_on_none(self, listener):
|
||||
"""Reset settings to their default values if None/null was passed in
|
||||
|
||||
@ -592,10 +595,14 @@ class ListenersController(base.BaseController):
|
||||
if listener.alpn_protocols is None:
|
||||
listener.alpn_protocols = (
|
||||
CONF.api_settings.default_listener_alpn_protocols)
|
||||
if listener.hsts_include_subdomains is None:
|
||||
listener.hsts_include_subdomains = False
|
||||
if listener.hsts_preload is None:
|
||||
listener.hsts_preload = False
|
||||
|
||||
@wsme_pecan.wsexpose(listener_types.ListenerRootResponse, wtypes.text,
|
||||
body=listener_types.ListenerRootPUT, status_code=200)
|
||||
def put(self, id, listener_):
|
||||
def put(self, id, listener_: listener_types.ListenerRootPUT):
|
||||
"""Updates a listener on a load balancer."""
|
||||
listener = listener_.listener
|
||||
context = pecan_request.context.get('octavia_context')
|
||||
|
@ -63,6 +63,9 @@ class ListenerResponse(BaseListenerType):
|
||||
tls_ciphers = wtypes.StringType()
|
||||
tls_versions = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType()))
|
||||
alpn_protocols = wtypes.wsattr(wtypes.ArrayType(types.AlpnProtocolType()))
|
||||
hsts_max_age = wtypes.wsattr(wtypes.IntegerType())
|
||||
hsts_include_subdomains = wtypes.wsattr(bool)
|
||||
hsts_preload = wtypes.wsattr(bool)
|
||||
|
||||
@classmethod
|
||||
def from_data_model(cls, data_model, children=False):
|
||||
@ -86,6 +89,9 @@ class ListenerResponse(BaseListenerType):
|
||||
|
||||
listener.tls_versions = data_model.tls_versions
|
||||
listener.alpn_protocols = data_model.alpn_protocols
|
||||
listener.hsts_max_age = data_model.hsts_max_age
|
||||
listener.hsts_include_subdomains = data_model.hsts_include_subdomains
|
||||
listener.hsts_preload = data_model.hsts_preload
|
||||
|
||||
return listener
|
||||
|
||||
@ -155,6 +161,9 @@ class ListenerPOST(BaseListenerType):
|
||||
tls_versions = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType(
|
||||
max_length=32)))
|
||||
alpn_protocols = wtypes.wsattr(wtypes.ArrayType(types.AlpnProtocolType()))
|
||||
hsts_max_age = wtypes.wsattr(wtypes.IntegerType(minimum=0))
|
||||
hsts_include_subdomains = wtypes.wsattr(bool, default=False)
|
||||
hsts_preload = wtypes.wsattr(bool, default=False)
|
||||
|
||||
|
||||
class ListenerRootPOST(types.BaseType):
|
||||
@ -196,6 +205,9 @@ class ListenerPUT(BaseListenerType):
|
||||
tls_versions = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType(
|
||||
max_length=32)))
|
||||
alpn_protocols = wtypes.wsattr(wtypes.ArrayType(types.AlpnProtocolType()))
|
||||
hsts_max_age = wtypes.wsattr(wtypes.IntegerType(minimum=0))
|
||||
hsts_include_subdomains = wtypes.wsattr(bool)
|
||||
hsts_preload = wtypes.wsattr(bool)
|
||||
|
||||
|
||||
class ListenerRootPUT(types.BaseType):
|
||||
@ -247,6 +259,9 @@ class ListenerSingleCreate(BaseListenerType):
|
||||
tls_versions = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType(
|
||||
max_length=32)))
|
||||
alpn_protocols = wtypes.wsattr(wtypes.ArrayType(types.AlpnProtocolType()))
|
||||
hsts_max_age = wtypes.wsattr(wtypes.IntegerType())
|
||||
hsts_include_subdomains = wtypes.wsattr(bool, default=False)
|
||||
hsts_preload = wtypes.wsattr(bool, default=False)
|
||||
|
||||
|
||||
class ListenerStatusResponse(BaseListenerType):
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
import datetime
|
||||
import re
|
||||
import typing as tp
|
||||
|
||||
from oslo_log import log as logging
|
||||
from sqlalchemy.orm import collections
|
||||
@ -419,7 +420,8 @@ class Listener(BaseDataModel):
|
||||
tags=None, client_ca_tls_certificate_id=None,
|
||||
client_authentication=None, client_crl_container_id=None,
|
||||
allowed_cidrs=None, tls_ciphers=None, tls_versions=None,
|
||||
alpn_protocols=None):
|
||||
alpn_protocols=None, hsts_max_age=None,
|
||||
hsts_include_subdomains=None, hsts_preload=None):
|
||||
self.id = id
|
||||
self.project_id = project_id
|
||||
self.name = name
|
||||
@ -455,6 +457,10 @@ class Listener(BaseDataModel):
|
||||
self.tls_ciphers = tls_ciphers
|
||||
self.tls_versions = tls_versions
|
||||
self.alpn_protocols = alpn_protocols
|
||||
self.hsts_max_age: tp.Optional[int] = hsts_max_age
|
||||
self.hsts_include_subdomains: tp.Optional[bool] = (
|
||||
hsts_include_subdomains)
|
||||
self.hsts_preload: tp.Optional[bool] = hsts_preload
|
||||
|
||||
def update(self, update_dict):
|
||||
for key, value in update_dict.items():
|
||||
|
@ -23,6 +23,7 @@ from oslo_utils import versionutils
|
||||
from octavia.common.config import cfg
|
||||
from octavia.common import constants
|
||||
from octavia.common import utils as octavia_utils
|
||||
from octavia.db import models
|
||||
|
||||
PROTOCOL_MAP = {
|
||||
constants.PROTOCOL_TCP: 'tcp',
|
||||
@ -298,7 +299,8 @@ class JinjaTemplater(object):
|
||||
'vrrp_priority': amphora.vrrp_priority
|
||||
}
|
||||
|
||||
def _transform_listener(self, listener, tls_certs, feature_compatibility,
|
||||
def _transform_listener(self, listener: models.Listener, tls_certs,
|
||||
feature_compatibility,
|
||||
loadbalancer):
|
||||
"""Transforms a listener into an object that will
|
||||
|
||||
@ -363,6 +365,13 @@ class JinjaTemplater(object):
|
||||
ret_value['tls_versions'] = listener.tls_versions
|
||||
if listener.alpn_protocols is not None:
|
||||
ret_value['alpn_protocols'] = ",".join(listener.alpn_protocols)
|
||||
if listener.hsts_max_age is not None:
|
||||
hsts_directives = f"max-age={listener.hsts_max_age};"
|
||||
if listener.hsts_include_subdomains:
|
||||
hsts_directives += " includeSubDomains;"
|
||||
if listener.hsts_preload:
|
||||
hsts_directives += " preload;"
|
||||
ret_value['hsts_directives'] = hsts_directives
|
||||
|
||||
pools = []
|
||||
pool_gen = (pool for pool in listener.pools if
|
||||
|
@ -166,6 +166,9 @@ frontend {{ listener.id }}
|
||||
{% if (listener.protocol.lower() ==
|
||||
constants.PROTOCOL_TERMINATED_HTTPS.lower()) %}
|
||||
redirect scheme https if !{ ssl_fc }
|
||||
{% if listener.hsts_directives is defined %}
|
||||
http-response set-header Strict-Transport-Security "{{ listener.hsts_directives }}"
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{{ bind_macro(constants, lib_consts, listener, lb_vip_address)|trim() }}
|
||||
{% for add_vip in additional_vips %}
|
||||
|
@ -20,6 +20,7 @@ from octavia_lib.common import constants as lib_consts
|
||||
from octavia.common.config import cfg
|
||||
from octavia.common import constants
|
||||
from octavia.common import utils as octavia_utils
|
||||
from octavia.db import models
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
@ -59,7 +60,7 @@ class LvsJinjaTemplater(object):
|
||||
self.keepalivedlvs_template = (keepalivedlvs_template or
|
||||
KEEPALIVED_LVS_TEMPLATE)
|
||||
|
||||
def build_config(self, listener, **kwargs):
|
||||
def build_config(self, listener: models.Listener, **kwargs):
|
||||
"""Convert a logical configuration to the Keepalived LVS version
|
||||
|
||||
:param listener: The listener configuration
|
||||
@ -97,7 +98,8 @@ class LvsJinjaTemplater(object):
|
||||
constants=constants,
|
||||
lib_consts=lib_consts)
|
||||
|
||||
def _transform_loadbalancer(self, loadbalancer, listener):
|
||||
def _transform_loadbalancer(self, loadbalancer: models.LoadBalancer,
|
||||
listener: models.Listener):
|
||||
"""Transforms a load balancer into an object that will
|
||||
|
||||
be processed by the templating system
|
||||
|
@ -28,11 +28,13 @@ from rfc3986 import validators
|
||||
from wsme import types as wtypes
|
||||
|
||||
from octavia.common import constants
|
||||
from octavia.common import data_models
|
||||
from octavia.common import exceptions
|
||||
from octavia.common import utils
|
||||
from octavia.i18n import _
|
||||
|
||||
CONF = cfg.CONF
|
||||
_ListenerPUT = 'octavia.api.v2.types.listener.ListenerPUT'
|
||||
|
||||
|
||||
def url(url, require_scheme=True):
|
||||
@ -531,3 +533,36 @@ def check_alpn_protocols(protocols):
|
||||
if invalid_protocols:
|
||||
raise exceptions.ValidationException(
|
||||
detail=_('Invalid ALPN protocol: ' + ', '.join(invalid_protocols)))
|
||||
|
||||
|
||||
def check_hsts_options(listener: dict):
|
||||
if ((listener.get('hsts_include_subdomains') or
|
||||
listener.get('hsts_preload')) and
|
||||
not isinstance(listener.get('hsts_max_age'), int)):
|
||||
raise exceptions.ValidationException(
|
||||
detail=_('HSTS configuration options hsts_include_subdomains and '
|
||||
'hsts_preload only make sense if hsts_max_age is '
|
||||
'set as well.'))
|
||||
|
||||
if (isinstance(listener.get('hsts_max_age'), int) and
|
||||
listener['protocol'] != constants.PROTOCOL_TERMINATED_HTTPS):
|
||||
raise exceptions.ValidationException(
|
||||
detail=_('The HSTS feature can only be used for listeners using '
|
||||
'the TERMINATED_HTTPS protocol.'))
|
||||
|
||||
|
||||
def check_hsts_options_put(listener: _ListenerPUT,
|
||||
db_listener: data_models.Listener):
|
||||
hsts_disabled = all(obj.hsts_max_age in [None, wtypes.Unset] for obj
|
||||
in (db_listener, listener))
|
||||
if ((listener.hsts_include_subdomains or listener.hsts_preload) and
|
||||
hsts_disabled):
|
||||
raise exceptions.ValidationException(
|
||||
detail=_('Cannot enable hsts_include_subdomains or hsts_preload '
|
||||
'if hsts_max_age was not set as well.'))
|
||||
|
||||
if (isinstance(listener.hsts_max_age, int) and
|
||||
db_listener.protocol != constants.PROTOCOL_TERMINATED_HTTPS):
|
||||
raise exceptions.ValidationException(
|
||||
detail=_('The HSTS feature can only be used for listeners using '
|
||||
'the TERMINATED_HTTPS protocol.'))
|
||||
|
@ -0,0 +1,42 @@
|
||||
# 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 HTTP Strict Transport Security support
|
||||
|
||||
Revision ID: 632152d2d32e
|
||||
Revises: 0995c26fc506
|
||||
Create Date: 2023-04-19 13:36:44.015581
|
||||
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '632152d2d32e'
|
||||
down_revision = '0995c26fc506'
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column(
|
||||
'listener',
|
||||
sa.Column('hsts_max_age', sa.Integer, nullable=True)
|
||||
)
|
||||
op.add_column(
|
||||
'listener',
|
||||
sa.Column('hsts_include_subdomains', sa.Boolean, nullable=True)
|
||||
)
|
||||
op.add_column(
|
||||
'listener',
|
||||
sa.Column('hsts_preload', sa.Boolean, nullable=True)
|
||||
)
|
@ -599,6 +599,9 @@ class Listener(base_models.BASE, base_models.IdMixin,
|
||||
tls_ciphers = sa.Column(sa.String(2048), nullable=True)
|
||||
tls_versions = sa.Column(ScalarListType(), nullable=True)
|
||||
alpn_protocols = sa.Column(ScalarListType(), nullable=True)
|
||||
hsts_max_age = sa.Column(sa.Integer, nullable=True)
|
||||
hsts_include_subdomains = sa.Column(sa.Boolean, nullable=True)
|
||||
hsts_preload = sa.Column(sa.Boolean, nullable=True)
|
||||
|
||||
_tags = orm.relationship(
|
||||
'Tags',
|
||||
|
@ -476,7 +476,10 @@ class SampleDriverDataModels(object):
|
||||
lib_consts.TLS_CIPHERS: constants.CIPHERS_OWASP_SUITE_B,
|
||||
lib_consts.TLS_VERSIONS: constants.TLS_VERSIONS_OWASP_SUITE_B,
|
||||
lib_consts.ALPN_PROTOCOLS:
|
||||
constants.AMPHORA_SUPPORTED_ALPN_PROTOCOLS
|
||||
constants.AMPHORA_SUPPORTED_ALPN_PROTOCOLS,
|
||||
lib_consts.HSTS_INCLUDE_SUBDOMAINS: False,
|
||||
lib_consts.HSTS_MAX_AGE: None,
|
||||
lib_consts.HSTS_PRELOAD: False,
|
||||
}
|
||||
|
||||
self.test_listener1_dict.update(self._common_test_dict)
|
||||
@ -488,6 +491,9 @@ class SampleDriverDataModels(object):
|
||||
self.test_listener2_dict[lib_consts.DEFAULT_POOL_ID] = self.pool2_id
|
||||
self.test_listener2_dict[
|
||||
lib_consts.DEFAULT_POOL] = self.test_pool2_dict
|
||||
self.test_listener2_dict[lib_consts.HSTS_INCLUDE_SUBDOMAINS] = True
|
||||
self.test_listener2_dict[lib_consts.HSTS_MAX_AGE] = 10
|
||||
self.test_listener2_dict[lib_consts.HSTS_PRELOAD] = False
|
||||
del self.test_listener2_dict[lib_consts.L7POLICIES]
|
||||
del self.test_listener2_dict[constants.SNI_CONTAINERS]
|
||||
del self.test_listener2_dict[constants.CLIENT_CA_TLS_CERTIFICATE_ID]
|
||||
@ -524,6 +530,9 @@ class SampleDriverDataModels(object):
|
||||
lib_consts.DEFAULT_TLS_CONTAINER_REF:
|
||||
self.default_tls_container_ref,
|
||||
lib_consts.DESCRIPTION: 'Listener 1',
|
||||
lib_consts.HSTS_INCLUDE_SUBDOMAINS: False,
|
||||
lib_consts.HSTS_MAX_AGE: None,
|
||||
lib_consts.HSTS_PRELOAD: False,
|
||||
lib_consts.INSERT_HEADERS: {},
|
||||
lib_consts.L7POLICIES: self.provider_l7policies_dict,
|
||||
lib_consts.LISTENER_ID: self.listener1_id,
|
||||
@ -571,6 +580,9 @@ class SampleDriverDataModels(object):
|
||||
self.provider_listener2_dict[
|
||||
lib_consts.CLIENT_CRL_CONTAINER_REF] = None
|
||||
del self.provider_listener2_dict[lib_consts.CLIENT_CRL_CONTAINER_DATA]
|
||||
self.provider_listener2_dict[lib_consts.HSTS_INCLUDE_SUBDOMAINS] = True
|
||||
self.provider_listener2_dict[lib_consts.HSTS_MAX_AGE] = 10
|
||||
self.provider_listener2_dict[lib_consts.HSTS_PRELOAD] = False
|
||||
|
||||
self.provider_listener1 = driver_dm.Listener(
|
||||
**self.provider_listener1_dict)
|
||||
|
@ -45,34 +45,9 @@ 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(27, len(version_ids))
|
||||
self.assertIn('v2.0', version_ids)
|
||||
self.assertIn('v2.1', version_ids)
|
||||
self.assertIn('v2.2', version_ids)
|
||||
self.assertIn('v2.3', version_ids)
|
||||
self.assertIn('v2.4', version_ids)
|
||||
self.assertIn('v2.5', version_ids)
|
||||
self.assertIn('v2.6', version_ids)
|
||||
self.assertIn('v2.7', version_ids)
|
||||
self.assertIn('v2.8', version_ids)
|
||||
self.assertIn('v2.9', version_ids)
|
||||
self.assertIn('v2.10', version_ids)
|
||||
self.assertIn('v2.11', version_ids)
|
||||
self.assertIn('v2.12', version_ids)
|
||||
self.assertIn('v2.13', version_ids)
|
||||
self.assertIn('v2.14', version_ids)
|
||||
self.assertIn('v2.15', version_ids)
|
||||
self.assertIn('v2.16', version_ids)
|
||||
self.assertIn('v2.17', version_ids)
|
||||
self.assertIn('v2.18', version_ids)
|
||||
self.assertIn('v2.19', version_ids)
|
||||
self.assertIn('v2.20', version_ids)
|
||||
self.assertIn('v2.21', version_ids)
|
||||
self.assertIn('v2.22', version_ids)
|
||||
self.assertIn('v2.23', version_ids)
|
||||
self.assertIn('v2.24', version_ids)
|
||||
self.assertIn('v2.25', version_ids)
|
||||
self.assertIn('v2.26', version_ids)
|
||||
expected_versions = (f"v2.{i}" for i in range(28))
|
||||
for version in expected_versions:
|
||||
self.assertIn(version, version_ids)
|
||||
|
||||
# Each version should have a 'self' 'href' to the API version URL
|
||||
# [{u'rel': u'self', u'href': u'http://localhost/v2'}]
|
||||
|
@ -1894,7 +1894,10 @@ class TestListener(base.BaseAPITest):
|
||||
client_ca_tls_container_ref=ca_tls_uuid,
|
||||
tls_versions=[lib_consts.TLS_VERSION_1_3],
|
||||
tls_ciphers='TLS_AES_256_GCM_SHA384',
|
||||
alpn_protocols=['http/1.0']).get(self.root_tag)
|
||||
alpn_protocols=['http/1.0'],
|
||||
hsts_max_age=20, hsts_include_subdomains=True,
|
||||
hsts_preload=True,
|
||||
).get(self.root_tag)
|
||||
self.set_lb_status(self.lb_id)
|
||||
unset_params = {
|
||||
'name': None, 'description': None, 'connection_limit': None,
|
||||
@ -1904,7 +1907,10 @@ class TestListener(base.BaseAPITest):
|
||||
'timeout_tcp_inspect': None, 'client_ca_tls_container_ref': None,
|
||||
'client_authentication': None, 'default_pool_id': None,
|
||||
'client_crl_container_ref': None, 'tls_versions': None,
|
||||
'tls_ciphers': None, 'alpn_protocols': None}
|
||||
'tls_ciphers': None, 'alpn_protocols': None,
|
||||
'hsts_max_age': None, 'hsts_include_subdomains': None,
|
||||
'hsts_preload': None,
|
||||
}
|
||||
body = self._build_body(unset_params)
|
||||
listener_path = self.LISTENER_PATH.format(
|
||||
listener_id=listener['id'])
|
||||
@ -1931,6 +1937,9 @@ class TestListener(base.BaseAPITest):
|
||||
self.assertEqual(constants.CIPHERS_OWASP_SUITE_B,
|
||||
api_listener['tls_ciphers'])
|
||||
self.assertEqual(['http/1.1'], api_listener['alpn_protocols'])
|
||||
self.assertIsNone(api_listener['hsts_max_age'])
|
||||
self.assertFalse(api_listener['hsts_include_subdomains'])
|
||||
self.assertFalse(api_listener['hsts_preload'])
|
||||
|
||||
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
|
||||
def test_update_with_bad_ca_cert(self, mock_cert_data):
|
||||
|
@ -2879,7 +2879,10 @@ class TestLoadBalancerGraph(base.BaseAPITest):
|
||||
'allowed_cidrs': None,
|
||||
'tls_ciphers': None,
|
||||
'tls_versions': None,
|
||||
'alpn_protocols': None
|
||||
'alpn_protocols': None,
|
||||
'hsts_include_subdomains': False,
|
||||
'hsts_max_age': None,
|
||||
'hsts_preload': False,
|
||||
}
|
||||
if create_sni_containers:
|
||||
create_listener['sni_container_refs'] = create_sni_containers
|
||||
|
@ -49,6 +49,8 @@ class TestHaproxyCfg(base.TestCase):
|
||||
fe = ("frontend sample_listener_id_1\n"
|
||||
" maxconn {maxconn}\n"
|
||||
" redirect scheme https if !{{ ssl_fc }}\n"
|
||||
" http-response set-header Strict-Transport-Security "
|
||||
"\"max-age=10000000; includeSubDomains; preload;\"\n"
|
||||
" bind 10.0.0.2:443 "
|
||||
"ssl crt-list {crt_list} "
|
||||
"ca-file /var/lib/octavia/certs/sample_loadbalancer_id_1/"
|
||||
@ -107,6 +109,8 @@ class TestHaproxyCfg(base.TestCase):
|
||||
fe = ("frontend sample_listener_id_1\n"
|
||||
" maxconn {maxconn}\n"
|
||||
" redirect scheme https if !{{ ssl_fc }}\n"
|
||||
" http-response set-header Strict-Transport-Security "
|
||||
"\"max-age=10000000; includeSubDomains; preload;\"\n"
|
||||
" bind 10.0.0.2:443 ssl crt-list {crt_list}"
|
||||
" ciphers {ciphers} no-sslv3 no-tlsv10 no-tlsv11 alpn {alpn}\n"
|
||||
" mode http\n"
|
||||
@ -158,6 +162,8 @@ class TestHaproxyCfg(base.TestCase):
|
||||
fe = ("frontend sample_listener_id_1\n"
|
||||
" maxconn {maxconn}\n"
|
||||
" redirect scheme https if !{{ ssl_fc }}\n"
|
||||
" http-response set-header Strict-Transport-Security "
|
||||
"\"max-age=10000000; includeSubDomains; preload;\"\n"
|
||||
" bind 10.0.0.2:443 ssl crt-list {crt_list} "
|
||||
"no-sslv3 no-tlsv10 no-tlsv11 alpn {alpn}\n"
|
||||
" mode http\n"
|
||||
@ -208,6 +214,8 @@ class TestHaproxyCfg(base.TestCase):
|
||||
fe = ("frontend sample_listener_id_1\n"
|
||||
" maxconn {maxconn}\n"
|
||||
" redirect scheme https if !{{ ssl_fc }}\n"
|
||||
" http-response set-header Strict-Transport-Security "
|
||||
"\"max-age=10000000; includeSubDomains; preload;\"\n"
|
||||
" bind 10.0.0.2:443 "
|
||||
"ssl crt-list {crt_list} "
|
||||
"ca-file /var/lib/octavia/certs/sample_loadbalancer_id_1/"
|
||||
@ -266,6 +274,8 @@ class TestHaproxyCfg(base.TestCase):
|
||||
fe = ("frontend sample_listener_id_1\n"
|
||||
" maxconn {maxconn}\n"
|
||||
" redirect scheme https if !{{ ssl_fc }}\n"
|
||||
" http-response set-header Strict-Transport-Security "
|
||||
"\"max-age=10000000; includeSubDomains; preload;\"\n"
|
||||
" bind 10.0.0.2:443 ssl crt-list {crt_list} "
|
||||
"alpn {alpn}\n"
|
||||
" mode http\n"
|
||||
@ -318,6 +328,8 @@ class TestHaproxyCfg(base.TestCase):
|
||||
fe = ("frontend sample_listener_id_1\n"
|
||||
" maxconn {maxconn}\n"
|
||||
" redirect scheme https if !{{ ssl_fc }}\n"
|
||||
" http-response set-header Strict-Transport-Security "
|
||||
"\"max-age=10000000; includeSubDomains; preload;\"\n"
|
||||
" bind 10.0.0.2:443 ssl crt-list {crt_list} "
|
||||
"ciphers {ciphers} no-sslv3 no-tlsv10 no-tlsv11 alpn {alpn}\n"
|
||||
" mode http\n"
|
||||
@ -370,6 +382,8 @@ class TestHaproxyCfg(base.TestCase):
|
||||
fe = ("frontend sample_listener_id_1\n"
|
||||
" maxconn {maxconn}\n"
|
||||
" redirect scheme https if !{{ ssl_fc }}\n"
|
||||
" http-response set-header Strict-Transport-Security "
|
||||
"\"max-age=10000000; includeSubDomains; preload;\"\n"
|
||||
" bind 10.0.0.2:443 ssl crt-list {crt_list} "
|
||||
"ciphers {ciphers} no-sslv3 no-tlsv10 no-tlsv11\n"
|
||||
" mode http\n"
|
||||
@ -412,6 +426,119 @@ class TestHaproxyCfg(base.TestCase):
|
||||
frontend=fe, backend=be),
|
||||
rendered_obj)
|
||||
|
||||
def test_render_template_tls_no_alpn_hsts_max_age_only(self):
|
||||
conf = self.useFixture(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"
|
||||
" http-response set-header Strict-Transport-Security "
|
||||
"\"max-age=10000000;\"\n"
|
||||
" bind 10.0.0.2:443 ssl crt-list {crt_list} "
|
||||
"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(
|
||||
maxconn=constants.HAPROXY_DEFAULT_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_DEFAULT_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,
|
||||
alpn_protocols=None, hsts_include_subdomains=False,
|
||||
hsts_preload=False)],
|
||||
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_tls_no_hsts(self):
|
||||
conf = self.useFixture(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} "
|
||||
"no-sslv3 no-tlsv10 no-tlsv11 alpn {alpn}\n"
|
||||
" mode http\n"
|
||||
" default_backend sample_pool_id_1:sample_listener_id_1\n"
|
||||
" timeout client 50000\n").format(
|
||||
maxconn=constants.HAPROXY_DEFAULT_MAXCONN,
|
||||
crt_list=FAKE_CRT_LIST_FILENAME,
|
||||
ciphers=constants.CIPHERS_OWASP_SUITE_B,
|
||||
alpn=",".join(constants.AMPHORA_SUPPORTED_ALPN_PROTOCOLS))
|
||||
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_DEFAULT_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,
|
||||
hsts_max_age=None)],
|
||||
tls_tupe)
|
||||
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"
|
||||
@ -1758,6 +1885,8 @@ class TestHaproxyCfg(base.TestCase):
|
||||
fe = ("frontend sample_listener_id_1\n"
|
||||
" maxconn {maxconn}\n"
|
||||
" redirect scheme https if !{{ ssl_fc }}\n"
|
||||
" http-response set-header Strict-Transport-Security "
|
||||
"\"max-age=10000000; includeSubDomains; preload;\"\n"
|
||||
" bind 10.0.0.2:443 ciphers {ciphers} "
|
||||
"no-sslv3 no-tlsv10 no-tlsv11 alpn {alpn}\n"
|
||||
" mode http\n"
|
||||
|
@ -707,7 +707,9 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True,
|
||||
backend_alpn_protocols=constants.
|
||||
AMPHORA_SUPPORTED_ALPN_PROTOCOLS,
|
||||
include_pools=True,
|
||||
additional_vips=False):
|
||||
additional_vips=False,
|
||||
hsts_max_age=10_000_000,
|
||||
hsts_include_subdomains=True, hsts_preload=True):
|
||||
proto = 'HTTP' if proto is None else proto
|
||||
if be_proto is None:
|
||||
be_proto = 'HTTP' if proto == 'TERMINATED_HTTPS' else proto
|
||||
@ -731,7 +733,9 @@ 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_versions, alpn_protocols')
|
||||
'tls_ciphers, tls_versions, alpn_protocols, '
|
||||
'hsts_max_age, hsts_include_subdomains, hsts_preload'
|
||||
)
|
||||
if l7:
|
||||
pools = [
|
||||
sample_pool_tuple(
|
||||
@ -859,7 +863,10 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True,
|
||||
provisioning_status=provisioning_status,
|
||||
tls_ciphers=tls_ciphers,
|
||||
tls_versions=tls_versions,
|
||||
alpn_protocols=alpn_protocols
|
||||
alpn_protocols=alpn_protocols,
|
||||
hsts_max_age=hsts_max_age,
|
||||
hsts_include_subdomains=hsts_include_subdomains,
|
||||
hsts_preload=hsts_preload,
|
||||
)
|
||||
if recursive_nest:
|
||||
listener.load_balancer.listeners.append(listener)
|
||||
|
@ -16,6 +16,7 @@ from unittest import mock
|
||||
from oslo_config import cfg
|
||||
from oslo_config import fixture as oslo_fixture
|
||||
from oslo_utils import uuidutils
|
||||
from wsme import types as wtypes
|
||||
|
||||
import octavia.common.constants as constants
|
||||
import octavia.common.exceptions as exceptions
|
||||
@ -536,3 +537,69 @@ class TestValidations(base.TestCase):
|
||||
'2001:db8::/32'))
|
||||
self.assertFalse(validate.is_ip_member_of_cidr('::ffff:0:203.0.113.5',
|
||||
'2001:db8::/32'))
|
||||
|
||||
def test_check_hsts_options(self):
|
||||
self.assertRaises(
|
||||
exceptions.ValidationException,
|
||||
validate.check_hsts_options,
|
||||
{'hsts_include_subdomains': True,
|
||||
'hsts_preload': wtypes.Unset,
|
||||
'hsts_max_age': wtypes.Unset}
|
||||
)
|
||||
self.assertRaises(
|
||||
exceptions.ValidationException,
|
||||
validate.check_hsts_options,
|
||||
{'hsts_include_subdomains': wtypes.Unset,
|
||||
'hsts_preload': True,
|
||||
'hsts_max_age': wtypes.Unset}
|
||||
)
|
||||
self.assertRaises(
|
||||
exceptions.ValidationException,
|
||||
validate.check_hsts_options,
|
||||
{'protocol': constants.PROTOCOL_UDP,
|
||||
'hsts_include_subdomains': wtypes.Unset,
|
||||
'hsts_preload': wtypes.Unset,
|
||||
'hsts_max_age': 1}
|
||||
)
|
||||
self.assertIsNone(
|
||||
validate.check_hsts_options(
|
||||
{'protocol': constants.PROTOCOL_TERMINATED_HTTPS,
|
||||
'hsts_include_subdomains': wtypes.Unset,
|
||||
'hsts_preload': wtypes.Unset,
|
||||
'hsts_max_age': 1})
|
||||
)
|
||||
|
||||
def test_check_hsts_options_put(self):
|
||||
listener = mock.MagicMock()
|
||||
db_listener = mock.MagicMock()
|
||||
db_listener.protocol = constants.PROTOCOL_TERMINATED_HTTPS
|
||||
|
||||
listener.hsts_max_age = wtypes.Unset
|
||||
db_listener.hsts_max_age = None
|
||||
for obj in (listener, db_listener):
|
||||
obj.hsts_include_subdomains = False
|
||||
obj.hsts_preload = False
|
||||
self.assertIsNone(validate.check_hsts_options_put(
|
||||
listener, db_listener))
|
||||
|
||||
for i in range(2):
|
||||
listener.hsts_include_subdomains = bool(i % 2)
|
||||
listener.hsts_preload = not bool(i % 2)
|
||||
self.assertRaises(
|
||||
exceptions.ValidationException,
|
||||
validate.check_hsts_options_put,
|
||||
listener, db_listener)
|
||||
|
||||
listener.hsts_max_age, db_listener.hsts_max_age = wtypes.Unset, 0
|
||||
self.assertIsNone(validate.check_hsts_options_put(
|
||||
listener, db_listener))
|
||||
|
||||
listener.hsts_max_age, db_listener.hsts_max_age = 3, None
|
||||
self.assertIsNone(validate.check_hsts_options_put(
|
||||
listener, db_listener))
|
||||
|
||||
db_listener.protocol = constants.PROTOCOL_HTTP
|
||||
self.assertRaises(
|
||||
exceptions.ValidationException,
|
||||
validate.check_hsts_options_put,
|
||||
listener, db_listener)
|
||||
|
@ -0,0 +1,9 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Added support for HTTP Strict Transport Security (HSTS) for TLS-terminated
|
||||
listeners. The API for creating and updating listeners has been extended
|
||||
by the optional fields `hsts_max_age`, `hsts_include_subdomains` and
|
||||
`hsts_preload`. By default this feature is disabled.
|
||||
In order to activate this feature the `hsts_max_age`
|
||||
option needs to be set.
|
@ -46,7 +46,7 @@ castellan>=0.16.0 # Apache-2.0
|
||||
tenacity>=5.0.4 # Apache-2.0
|
||||
distro>=1.2.0 # Apache-2.0
|
||||
jsonschema>=3.2.0 # MIT
|
||||
octavia-lib>=3.1.0 # Apache-2.0
|
||||
octavia-lib>=3.3.0 # Apache-2.0
|
||||
simplejson>=3.13.2 # MIT
|
||||
setproctitle>=1.1.10 # BSD
|
||||
python-dateutil>=2.7.0 # BSD
|
||||
|
Loading…
Reference in New Issue
Block a user