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
|
in: body
|
||||||
required: false
|
required: false
|
||||||
type: string
|
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:
|
id:
|
||||||
description: |
|
description: |
|
||||||
The ID of the resource.
|
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_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256",
|
||||||
"tls_versions": ["TLSv1.2", "TLSv1.3"],
|
"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"
|
"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"],
|
"tls_versions": [
|
||||||
"alpn_protocols": ["http/1.1", "http/1.0"]
|
"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"
|
"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"],
|
"tls_versions": [
|
||||||
"alpn_protocols": ["http/1.1", "http/1.0"]
|
"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_connect": 1000,
|
||||||
"timeout_member_data": 100000,
|
"timeout_member_data": 100000,
|
||||||
"timeout_tcp_inspect": 5,
|
"timeout_tcp_inspect": 5,
|
||||||
"tags": ["updated_tag"],
|
"tags": [
|
||||||
|
"updated_tag"
|
||||||
|
],
|
||||||
"client_ca_tls_container_ref": null,
|
"client_ca_tls_container_ref": null,
|
||||||
"allowed_cidrs": [
|
"allowed_cidrs": [
|
||||||
"192.0.2.0/24",
|
"192.0.2.0/24",
|
||||||
"198.51.100.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"],
|
"tls_versions": [
|
||||||
"alpn_protocols": ["http/1.1", "http/1.0"]
|
"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"
|
"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"],
|
"tls_versions": [
|
||||||
"alpn_protocols": ["http/1.1", "http/1.0"]
|
"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_connect": 5000,
|
||||||
"timeout_member_data": 50000,
|
"timeout_member_data": 50000,
|
||||||
"timeout_tcp_inspect": 0,
|
"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_ca_tls_container_ref": "http://198.51.100.10:9311/v1/containers/35649991-49f3-4625-81ce-2465fe8932e5",
|
||||||
"client_authentication": "NONE",
|
"client_authentication": "NONE",
|
||||||
"client_crl_container_ref": "http://198.51.100.10:9311/v1/containers/e222b065-b93b-4e2a-9a02-804b7a118c3c",
|
"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"
|
"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"],
|
"tls_versions": [
|
||||||
"alpn_protocols": ["http/1.1", "http/1.0"]
|
"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_pool_id: default_pool_id
|
||||||
- default_tls_container_ref: default_tls_container_ref
|
- default_tls_container_ref: default_tls_container_ref
|
||||||
- description: description
|
- description: description
|
||||||
|
- hsts_include_subdomains: hsts_include_subdomains
|
||||||
|
- hsts_max_age: hsts_max_age
|
||||||
|
- hsts_preload: hsts_preload
|
||||||
- id: listener-id
|
- id: listener-id
|
||||||
- insert_headers: insert_headers
|
- insert_headers: insert_headers
|
||||||
- l7policies: l7policy-ids
|
- l7policies: l7policy-ids
|
||||||
@ -153,6 +156,9 @@ Request
|
|||||||
- default_pool_id: default_pool_id-optional
|
- default_pool_id: default_pool_id-optional
|
||||||
- default_tls_container_ref: default_tls_container_ref-optional
|
- default_tls_container_ref: default_tls_container_ref-optional
|
||||||
- description: description-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
|
- insert_headers: insert_headers-optional
|
||||||
- l7policies: l7policies-optional
|
- l7policies: l7policies-optional
|
||||||
- listeners: listener
|
- listeners: listener
|
||||||
@ -277,6 +283,9 @@ Response Parameters
|
|||||||
- default_pool_id: default_pool_id
|
- default_pool_id: default_pool_id
|
||||||
- default_tls_container_ref: default_tls_container_ref
|
- default_tls_container_ref: default_tls_container_ref
|
||||||
- description: description
|
- description: description
|
||||||
|
- hsts_include_subdomains: hsts_include_subdomains
|
||||||
|
- hsts_max_age: hsts_max_age
|
||||||
|
- hsts_preload: hsts_preload
|
||||||
- id: listener-id
|
- id: listener-id
|
||||||
- insert_headers: insert_headers
|
- insert_headers: insert_headers
|
||||||
- l7policies: l7policy-ids
|
- l7policies: l7policy-ids
|
||||||
@ -358,6 +367,9 @@ Response Parameters
|
|||||||
- default_pool_id: default_pool_id
|
- default_pool_id: default_pool_id
|
||||||
- default_tls_container_ref: default_tls_container_ref
|
- default_tls_container_ref: default_tls_container_ref
|
||||||
- description: description
|
- description: description
|
||||||
|
- hsts_include_subdomains: hsts_include_subdomains
|
||||||
|
- hsts_max_age: hsts_max_age
|
||||||
|
- hsts_preload: hsts_preload
|
||||||
- id: listener-id
|
- id: listener-id
|
||||||
- insert_headers: insert_headers
|
- insert_headers: insert_headers
|
||||||
- l7policies: l7policy-ids
|
- l7policies: l7policy-ids
|
||||||
@ -428,6 +440,9 @@ Request
|
|||||||
- default_pool_id: default_pool_id-optional
|
- default_pool_id: default_pool_id-optional
|
||||||
- default_tls_container_ref: default_tls_container_ref-optional
|
- default_tls_container_ref: default_tls_container_ref-optional
|
||||||
- description: description-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
|
- insert_headers: insert_headers-optional
|
||||||
- listener_id: path-listener-id
|
- listener_id: path-listener-id
|
||||||
- name: name-optional
|
- name: name-optional
|
||||||
@ -468,6 +483,9 @@ Response Parameters
|
|||||||
- default_pool_id: default_pool_id
|
- default_pool_id: default_pool_id
|
||||||
- default_tls_container_ref: default_tls_container_ref
|
- default_tls_container_ref: default_tls_container_ref
|
||||||
- description: description
|
- description: description
|
||||||
|
- hsts_include_subdomains: hsts_include_subdomains
|
||||||
|
- hsts_max_age: hsts_max_age
|
||||||
|
- hsts_preload: hsts_preload
|
||||||
- id: listener-id
|
- id: listener-id
|
||||||
- insert_headers: insert_headers
|
- insert_headers: insert_headers
|
||||||
- l7policies: l7policy-ids
|
- 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.10 --protocol-port 80 pool1
|
||||||
openstack loadbalancer member create --subnet-id private-subnet --address 192.0.2.11 --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
|
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',
|
self._add_a_version(versions, 'v2.25', 'v2', 'SUPPORTED',
|
||||||
'2021-10-02T00:00:00Z', host_url)
|
'2021-10-02T00:00:00Z', host_url)
|
||||||
# Additional VIPs
|
# 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)
|
'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}
|
return {'versions': versions}
|
||||||
|
@ -324,6 +324,8 @@ class ListenersController(base.BaseController):
|
|||||||
# Validate ALPN protocol list
|
# Validate ALPN protocol list
|
||||||
validate.check_alpn_protocols(listener_dict['alpn_protocols'])
|
validate.check_alpn_protocols(listener_dict['alpn_protocols'])
|
||||||
|
|
||||||
|
validate.check_hsts_options(listener_dict)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
db_listener = self.repositories.listener.create(
|
db_listener = self.repositories.listener.create(
|
||||||
lock_session, **listener_dict)
|
lock_session, **listener_dict)
|
||||||
@ -345,7 +347,6 @@ class ListenersController(base.BaseController):
|
|||||||
except odb_exceptions.DBError as e:
|
except odb_exceptions.DBError as e:
|
||||||
raise exceptions.InvalidOption(value=listener_dict.get('protocol'),
|
raise exceptions.InvalidOption(value=listener_dict.get('protocol'),
|
||||||
option='protocol') from e
|
option='protocol') from e
|
||||||
return None
|
|
||||||
|
|
||||||
@wsme_pecan.wsexpose(listener_types.ListenerRootResponse,
|
@wsme_pecan.wsexpose(listener_types.ListenerRootResponse,
|
||||||
body=listener_types.ListenerRootPOST, status_code=201)
|
body=listener_types.ListenerRootPOST, status_code=201)
|
||||||
@ -557,6 +558,8 @@ class ListenersController(base.BaseController):
|
|||||||
# Validate ALPN protocol list
|
# Validate ALPN protocol list
|
||||||
validate.check_alpn_protocols(listener.alpn_protocols)
|
validate.check_alpn_protocols(listener.alpn_protocols)
|
||||||
|
|
||||||
|
validate.check_hsts_options_put(listener, db_listener)
|
||||||
|
|
||||||
def _set_default_on_none(self, listener):
|
def _set_default_on_none(self, listener):
|
||||||
"""Reset settings to their default values if None/null was passed in
|
"""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:
|
if listener.alpn_protocols is None:
|
||||||
listener.alpn_protocols = (
|
listener.alpn_protocols = (
|
||||||
CONF.api_settings.default_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,
|
@wsme_pecan.wsexpose(listener_types.ListenerRootResponse, wtypes.text,
|
||||||
body=listener_types.ListenerRootPUT, status_code=200)
|
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."""
|
"""Updates a listener on a load balancer."""
|
||||||
listener = listener_.listener
|
listener = listener_.listener
|
||||||
context = pecan_request.context.get('octavia_context')
|
context = pecan_request.context.get('octavia_context')
|
||||||
|
@ -63,6 +63,9 @@ class ListenerResponse(BaseListenerType):
|
|||||||
tls_ciphers = wtypes.StringType()
|
tls_ciphers = wtypes.StringType()
|
||||||
tls_versions = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType()))
|
tls_versions = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType()))
|
||||||
alpn_protocols = wtypes.wsattr(wtypes.ArrayType(types.AlpnProtocolType()))
|
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
|
@classmethod
|
||||||
def from_data_model(cls, data_model, children=False):
|
def from_data_model(cls, data_model, children=False):
|
||||||
@ -86,6 +89,9 @@ class ListenerResponse(BaseListenerType):
|
|||||||
|
|
||||||
listener.tls_versions = data_model.tls_versions
|
listener.tls_versions = data_model.tls_versions
|
||||||
listener.alpn_protocols = data_model.alpn_protocols
|
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
|
return listener
|
||||||
|
|
||||||
@ -155,6 +161,9 @@ class ListenerPOST(BaseListenerType):
|
|||||||
tls_versions = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType(
|
tls_versions = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType(
|
||||||
max_length=32)))
|
max_length=32)))
|
||||||
alpn_protocols = wtypes.wsattr(wtypes.ArrayType(types.AlpnProtocolType()))
|
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):
|
class ListenerRootPOST(types.BaseType):
|
||||||
@ -196,6 +205,9 @@ class ListenerPUT(BaseListenerType):
|
|||||||
tls_versions = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType(
|
tls_versions = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType(
|
||||||
max_length=32)))
|
max_length=32)))
|
||||||
alpn_protocols = wtypes.wsattr(wtypes.ArrayType(types.AlpnProtocolType()))
|
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):
|
class ListenerRootPUT(types.BaseType):
|
||||||
@ -247,6 +259,9 @@ class ListenerSingleCreate(BaseListenerType):
|
|||||||
tls_versions = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType(
|
tls_versions = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType(
|
||||||
max_length=32)))
|
max_length=32)))
|
||||||
alpn_protocols = wtypes.wsattr(wtypes.ArrayType(types.AlpnProtocolType()))
|
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):
|
class ListenerStatusResponse(BaseListenerType):
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import re
|
import re
|
||||||
|
import typing as tp
|
||||||
|
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from sqlalchemy.orm import collections
|
from sqlalchemy.orm import collections
|
||||||
@ -419,7 +420,8 @@ class Listener(BaseDataModel):
|
|||||||
tags=None, client_ca_tls_certificate_id=None,
|
tags=None, client_ca_tls_certificate_id=None,
|
||||||
client_authentication=None, client_crl_container_id=None,
|
client_authentication=None, client_crl_container_id=None,
|
||||||
allowed_cidrs=None, tls_ciphers=None, tls_versions=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.id = id
|
||||||
self.project_id = project_id
|
self.project_id = project_id
|
||||||
self.name = name
|
self.name = name
|
||||||
@ -455,6 +457,10 @@ class Listener(BaseDataModel):
|
|||||||
self.tls_ciphers = tls_ciphers
|
self.tls_ciphers = tls_ciphers
|
||||||
self.tls_versions = tls_versions
|
self.tls_versions = tls_versions
|
||||||
self.alpn_protocols = alpn_protocols
|
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):
|
def update(self, update_dict):
|
||||||
for key, value in update_dict.items():
|
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.config import cfg
|
||||||
from octavia.common import constants
|
from octavia.common import constants
|
||||||
from octavia.common import utils as octavia_utils
|
from octavia.common import utils as octavia_utils
|
||||||
|
from octavia.db import models
|
||||||
|
|
||||||
PROTOCOL_MAP = {
|
PROTOCOL_MAP = {
|
||||||
constants.PROTOCOL_TCP: 'tcp',
|
constants.PROTOCOL_TCP: 'tcp',
|
||||||
@ -298,7 +299,8 @@ class JinjaTemplater(object):
|
|||||||
'vrrp_priority': amphora.vrrp_priority
|
'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):
|
loadbalancer):
|
||||||
"""Transforms a listener into an object that will
|
"""Transforms a listener into an object that will
|
||||||
|
|
||||||
@ -363,6 +365,13 @@ class JinjaTemplater(object):
|
|||||||
ret_value['tls_versions'] = listener.tls_versions
|
ret_value['tls_versions'] = listener.tls_versions
|
||||||
if listener.alpn_protocols is not None:
|
if listener.alpn_protocols is not None:
|
||||||
ret_value['alpn_protocols'] = ",".join(listener.alpn_protocols)
|
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 = []
|
pools = []
|
||||||
pool_gen = (pool for pool in listener.pools if
|
pool_gen = (pool for pool in listener.pools if
|
||||||
|
@ -166,6 +166,9 @@ frontend {{ listener.id }}
|
|||||||
{% if (listener.protocol.lower() ==
|
{% if (listener.protocol.lower() ==
|
||||||
constants.PROTOCOL_TERMINATED_HTTPS.lower()) %}
|
constants.PROTOCOL_TERMINATED_HTTPS.lower()) %}
|
||||||
redirect scheme https if !{ ssl_fc }
|
redirect scheme https if !{ ssl_fc }
|
||||||
|
{% if listener.hsts_directives is defined %}
|
||||||
|
http-response set-header Strict-Transport-Security "{{ listener.hsts_directives }}"
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ bind_macro(constants, lib_consts, listener, lb_vip_address)|trim() }}
|
{{ bind_macro(constants, lib_consts, listener, lb_vip_address)|trim() }}
|
||||||
{% for add_vip in additional_vips %}
|
{% 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.config import cfg
|
||||||
from octavia.common import constants
|
from octavia.common import constants
|
||||||
from octavia.common import utils as octavia_utils
|
from octavia.common import utils as octavia_utils
|
||||||
|
from octavia.db import models
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
@ -59,7 +60,7 @@ class LvsJinjaTemplater(object):
|
|||||||
self.keepalivedlvs_template = (keepalivedlvs_template or
|
self.keepalivedlvs_template = (keepalivedlvs_template or
|
||||||
KEEPALIVED_LVS_TEMPLATE)
|
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
|
"""Convert a logical configuration to the Keepalived LVS version
|
||||||
|
|
||||||
:param listener: The listener configuration
|
:param listener: The listener configuration
|
||||||
@ -97,7 +98,8 @@ class LvsJinjaTemplater(object):
|
|||||||
constants=constants,
|
constants=constants,
|
||||||
lib_consts=lib_consts)
|
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
|
"""Transforms a load balancer into an object that will
|
||||||
|
|
||||||
be processed by the templating system
|
be processed by the templating system
|
||||||
|
@ -28,11 +28,13 @@ from rfc3986 import validators
|
|||||||
from wsme import types as wtypes
|
from wsme import types as wtypes
|
||||||
|
|
||||||
from octavia.common import constants
|
from octavia.common import constants
|
||||||
|
from octavia.common import data_models
|
||||||
from octavia.common import exceptions
|
from octavia.common import exceptions
|
||||||
from octavia.common import utils
|
from octavia.common import utils
|
||||||
from octavia.i18n import _
|
from octavia.i18n import _
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
_ListenerPUT = 'octavia.api.v2.types.listener.ListenerPUT'
|
||||||
|
|
||||||
|
|
||||||
def url(url, require_scheme=True):
|
def url(url, require_scheme=True):
|
||||||
@ -531,3 +533,36 @@ def check_alpn_protocols(protocols):
|
|||||||
if invalid_protocols:
|
if invalid_protocols:
|
||||||
raise exceptions.ValidationException(
|
raise exceptions.ValidationException(
|
||||||
detail=_('Invalid ALPN protocol: ' + ', '.join(invalid_protocols)))
|
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_ciphers = sa.Column(sa.String(2048), nullable=True)
|
||||||
tls_versions = sa.Column(ScalarListType(), nullable=True)
|
tls_versions = sa.Column(ScalarListType(), nullable=True)
|
||||||
alpn_protocols = 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 = orm.relationship(
|
||||||
'Tags',
|
'Tags',
|
||||||
|
@ -476,7 +476,10 @@ class SampleDriverDataModels(object):
|
|||||||
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,
|
lib_consts.TLS_VERSIONS: constants.TLS_VERSIONS_OWASP_SUITE_B,
|
||||||
lib_consts.ALPN_PROTOCOLS:
|
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)
|
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_ID] = self.pool2_id
|
||||||
self.test_listener2_dict[
|
self.test_listener2_dict[
|
||||||
lib_consts.DEFAULT_POOL] = self.test_pool2_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[lib_consts.L7POLICIES]
|
||||||
del self.test_listener2_dict[constants.SNI_CONTAINERS]
|
del self.test_listener2_dict[constants.SNI_CONTAINERS]
|
||||||
del self.test_listener2_dict[constants.CLIENT_CA_TLS_CERTIFICATE_ID]
|
del self.test_listener2_dict[constants.CLIENT_CA_TLS_CERTIFICATE_ID]
|
||||||
@ -524,6 +530,9 @@ class SampleDriverDataModels(object):
|
|||||||
lib_consts.DEFAULT_TLS_CONTAINER_REF:
|
lib_consts.DEFAULT_TLS_CONTAINER_REF:
|
||||||
self.default_tls_container_ref,
|
self.default_tls_container_ref,
|
||||||
lib_consts.DESCRIPTION: 'Listener 1',
|
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.INSERT_HEADERS: {},
|
||||||
lib_consts.L7POLICIES: self.provider_l7policies_dict,
|
lib_consts.L7POLICIES: self.provider_l7policies_dict,
|
||||||
lib_consts.LISTENER_ID: self.listener1_id,
|
lib_consts.LISTENER_ID: self.listener1_id,
|
||||||
@ -571,6 +580,9 @@ class SampleDriverDataModels(object):
|
|||||||
self.provider_listener2_dict[
|
self.provider_listener2_dict[
|
||||||
lib_consts.CLIENT_CRL_CONTAINER_REF] = None
|
lib_consts.CLIENT_CRL_CONTAINER_REF] = None
|
||||||
del self.provider_listener2_dict[lib_consts.CLIENT_CRL_CONTAINER_DATA]
|
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 = driver_dm.Listener(
|
||||||
**self.provider_listener1_dict)
|
**self.provider_listener1_dict)
|
||||||
|
@ -45,34 +45,9 @@ class TestRootController(base_db_test.OctaviaDBTestBase):
|
|||||||
def test_api_versions(self):
|
def test_api_versions(self):
|
||||||
versions = self._get_versions_with_config()
|
versions = self._get_versions_with_config()
|
||||||
version_ids = tuple(v.get('id') for v in versions)
|
version_ids = tuple(v.get('id') for v in versions)
|
||||||
self.assertEqual(27, len(version_ids))
|
expected_versions = (f"v2.{i}" for i in range(28))
|
||||||
self.assertIn('v2.0', version_ids)
|
for version in expected_versions:
|
||||||
self.assertIn('v2.1', version_ids)
|
self.assertIn(version, 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)
|
|
||||||
|
|
||||||
# Each version should have a 'self' 'href' to the API version URL
|
# Each version should have a 'self' 'href' to the API version URL
|
||||||
# [{u'rel': u'self', u'href': u'http://localhost/v2'}]
|
# [{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,
|
client_ca_tls_container_ref=ca_tls_uuid,
|
||||||
tls_versions=[lib_consts.TLS_VERSION_1_3],
|
tls_versions=[lib_consts.TLS_VERSION_1_3],
|
||||||
tls_ciphers='TLS_AES_256_GCM_SHA384',
|
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)
|
self.set_lb_status(self.lb_id)
|
||||||
unset_params = {
|
unset_params = {
|
||||||
'name': None, 'description': None, 'connection_limit': None,
|
'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,
|
'timeout_tcp_inspect': None, 'client_ca_tls_container_ref': None,
|
||||||
'client_authentication': None, 'default_pool_id': None,
|
'client_authentication': None, 'default_pool_id': None,
|
||||||
'client_crl_container_ref': None, 'tls_versions': 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)
|
body = self._build_body(unset_params)
|
||||||
listener_path = self.LISTENER_PATH.format(
|
listener_path = self.LISTENER_PATH.format(
|
||||||
listener_id=listener['id'])
|
listener_id=listener['id'])
|
||||||
@ -1931,6 +1937,9 @@ class TestListener(base.BaseAPITest):
|
|||||||
self.assertEqual(constants.CIPHERS_OWASP_SUITE_B,
|
self.assertEqual(constants.CIPHERS_OWASP_SUITE_B,
|
||||||
api_listener['tls_ciphers'])
|
api_listener['tls_ciphers'])
|
||||||
self.assertEqual(['http/1.1'], api_listener['alpn_protocols'])
|
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')
|
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
|
||||||
def test_update_with_bad_ca_cert(self, mock_cert_data):
|
def test_update_with_bad_ca_cert(self, mock_cert_data):
|
||||||
|
@ -2879,7 +2879,10 @@ class TestLoadBalancerGraph(base.BaseAPITest):
|
|||||||
'allowed_cidrs': None,
|
'allowed_cidrs': None,
|
||||||
'tls_ciphers': None,
|
'tls_ciphers': None,
|
||||||
'tls_versions': 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:
|
if create_sni_containers:
|
||||||
create_listener['sni_container_refs'] = 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"
|
fe = ("frontend sample_listener_id_1\n"
|
||||||
" maxconn {maxconn}\n"
|
" maxconn {maxconn}\n"
|
||||||
" redirect scheme https if !{{ ssl_fc }}\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 "
|
" bind 10.0.0.2:443 "
|
||||||
"ssl crt-list {crt_list} "
|
"ssl crt-list {crt_list} "
|
||||||
"ca-file /var/lib/octavia/certs/sample_loadbalancer_id_1/"
|
"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"
|
fe = ("frontend sample_listener_id_1\n"
|
||||||
" maxconn {maxconn}\n"
|
" maxconn {maxconn}\n"
|
||||||
" redirect scheme https if !{{ ssl_fc }}\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}"
|
" bind 10.0.0.2:443 ssl crt-list {crt_list}"
|
||||||
" ciphers {ciphers} no-sslv3 no-tlsv10 no-tlsv11 alpn {alpn}\n"
|
" ciphers {ciphers} no-sslv3 no-tlsv10 no-tlsv11 alpn {alpn}\n"
|
||||||
" mode http\n"
|
" mode http\n"
|
||||||
@ -158,6 +162,8 @@ class TestHaproxyCfg(base.TestCase):
|
|||||||
fe = ("frontend sample_listener_id_1\n"
|
fe = ("frontend sample_listener_id_1\n"
|
||||||
" maxconn {maxconn}\n"
|
" maxconn {maxconn}\n"
|
||||||
" redirect scheme https if !{{ ssl_fc }}\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} "
|
" bind 10.0.0.2:443 ssl crt-list {crt_list} "
|
||||||
"no-sslv3 no-tlsv10 no-tlsv11 alpn {alpn}\n"
|
"no-sslv3 no-tlsv10 no-tlsv11 alpn {alpn}\n"
|
||||||
" mode http\n"
|
" mode http\n"
|
||||||
@ -208,6 +214,8 @@ class TestHaproxyCfg(base.TestCase):
|
|||||||
fe = ("frontend sample_listener_id_1\n"
|
fe = ("frontend sample_listener_id_1\n"
|
||||||
" maxconn {maxconn}\n"
|
" maxconn {maxconn}\n"
|
||||||
" redirect scheme https if !{{ ssl_fc }}\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 "
|
" bind 10.0.0.2:443 "
|
||||||
"ssl crt-list {crt_list} "
|
"ssl crt-list {crt_list} "
|
||||||
"ca-file /var/lib/octavia/certs/sample_loadbalancer_id_1/"
|
"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"
|
fe = ("frontend sample_listener_id_1\n"
|
||||||
" maxconn {maxconn}\n"
|
" maxconn {maxconn}\n"
|
||||||
" redirect scheme https if !{{ ssl_fc }}\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} "
|
" bind 10.0.0.2:443 ssl crt-list {crt_list} "
|
||||||
"alpn {alpn}\n"
|
"alpn {alpn}\n"
|
||||||
" mode http\n"
|
" mode http\n"
|
||||||
@ -318,6 +328,8 @@ class TestHaproxyCfg(base.TestCase):
|
|||||||
fe = ("frontend sample_listener_id_1\n"
|
fe = ("frontend sample_listener_id_1\n"
|
||||||
" maxconn {maxconn}\n"
|
" maxconn {maxconn}\n"
|
||||||
" redirect scheme https if !{{ ssl_fc }}\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} "
|
" bind 10.0.0.2:443 ssl crt-list {crt_list} "
|
||||||
"ciphers {ciphers} no-sslv3 no-tlsv10 no-tlsv11 alpn {alpn}\n"
|
"ciphers {ciphers} no-sslv3 no-tlsv10 no-tlsv11 alpn {alpn}\n"
|
||||||
" mode http\n"
|
" mode http\n"
|
||||||
@ -370,6 +382,8 @@ class TestHaproxyCfg(base.TestCase):
|
|||||||
fe = ("frontend sample_listener_id_1\n"
|
fe = ("frontend sample_listener_id_1\n"
|
||||||
" maxconn {maxconn}\n"
|
" maxconn {maxconn}\n"
|
||||||
" redirect scheme https if !{{ ssl_fc }}\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} "
|
" bind 10.0.0.2:443 ssl crt-list {crt_list} "
|
||||||
"ciphers {ciphers} no-sslv3 no-tlsv10 no-tlsv11\n"
|
"ciphers {ciphers} no-sslv3 no-tlsv10 no-tlsv11\n"
|
||||||
" mode http\n"
|
" mode http\n"
|
||||||
@ -412,6 +426,119 @@ class TestHaproxyCfg(base.TestCase):
|
|||||||
frontend=fe, backend=be),
|
frontend=fe, backend=be),
|
||||||
rendered_obj)
|
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):
|
def test_render_template_http(self):
|
||||||
be = ("backend sample_pool_id_1:sample_listener_id_1\n"
|
be = ("backend sample_pool_id_1:sample_listener_id_1\n"
|
||||||
" mode http\n"
|
" mode http\n"
|
||||||
@ -1758,6 +1885,8 @@ class TestHaproxyCfg(base.TestCase):
|
|||||||
fe = ("frontend sample_listener_id_1\n"
|
fe = ("frontend sample_listener_id_1\n"
|
||||||
" maxconn {maxconn}\n"
|
" maxconn {maxconn}\n"
|
||||||
" redirect scheme https if !{{ ssl_fc }}\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} "
|
" bind 10.0.0.2:443 ciphers {ciphers} "
|
||||||
"no-sslv3 no-tlsv10 no-tlsv11 alpn {alpn}\n"
|
"no-sslv3 no-tlsv10 no-tlsv11 alpn {alpn}\n"
|
||||||
" mode http\n"
|
" mode http\n"
|
||||||
|
@ -707,7 +707,9 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True,
|
|||||||
backend_alpn_protocols=constants.
|
backend_alpn_protocols=constants.
|
||||||
AMPHORA_SUPPORTED_ALPN_PROTOCOLS,
|
AMPHORA_SUPPORTED_ALPN_PROTOCOLS,
|
||||||
include_pools=True,
|
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
|
proto = 'HTTP' if proto is None else proto
|
||||||
if be_proto is None:
|
if be_proto is None:
|
||||||
be_proto = 'HTTP' if proto == 'TERMINATED_HTTPS' else proto
|
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, '
|
'timeout_tcp_inspect, client_ca_tls_certificate_id, '
|
||||||
'client_ca_tls_certificate, client_authentication, '
|
'client_ca_tls_certificate, client_authentication, '
|
||||||
'client_crl_container_id, provisioning_status, '
|
'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:
|
if l7:
|
||||||
pools = [
|
pools = [
|
||||||
sample_pool_tuple(
|
sample_pool_tuple(
|
||||||
@ -859,7 +863,10 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True,
|
|||||||
provisioning_status=provisioning_status,
|
provisioning_status=provisioning_status,
|
||||||
tls_ciphers=tls_ciphers,
|
tls_ciphers=tls_ciphers,
|
||||||
tls_versions=tls_versions,
|
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:
|
if recursive_nest:
|
||||||
listener.load_balancer.listeners.append(listener)
|
listener.load_balancer.listeners.append(listener)
|
||||||
|
@ -16,6 +16,7 @@ from unittest import mock
|
|||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_config import fixture as oslo_fixture
|
from oslo_config import fixture as oslo_fixture
|
||||||
from oslo_utils import uuidutils
|
from oslo_utils import uuidutils
|
||||||
|
from wsme import types as wtypes
|
||||||
|
|
||||||
import octavia.common.constants as constants
|
import octavia.common.constants as constants
|
||||||
import octavia.common.exceptions as exceptions
|
import octavia.common.exceptions as exceptions
|
||||||
@ -536,3 +537,69 @@ class TestValidations(base.TestCase):
|
|||||||
'2001:db8::/32'))
|
'2001:db8::/32'))
|
||||||
self.assertFalse(validate.is_ip_member_of_cidr('::ffff:0:203.0.113.5',
|
self.assertFalse(validate.is_ip_member_of_cidr('::ffff:0:203.0.113.5',
|
||||||
'2001:db8::/32'))
|
'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
|
tenacity>=5.0.4 # Apache-2.0
|
||||||
distro>=1.2.0 # Apache-2.0
|
distro>=1.2.0 # Apache-2.0
|
||||||
jsonschema>=3.2.0 # MIT
|
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
|
simplejson>=3.13.2 # MIT
|
||||||
setproctitle>=1.1.10 # BSD
|
setproctitle>=1.1.10 # BSD
|
||||||
python-dateutil>=2.7.0 # BSD
|
python-dateutil>=2.7.0 # BSD
|
||||||
|
Loading…
Reference in New Issue
Block a user