diff --git a/octaviaclient/osc/v2/constants.py b/octaviaclient/osc/v2/constants.py index 46b99ca..53a0c35 100644 --- a/octaviaclient/osc/v2/constants.py +++ b/octaviaclient/osc/v2/constants.py @@ -82,7 +82,11 @@ LISTENER_ROWS = ( 'tls_ciphers', 'tls_versions', 'alpn_protocols', - 'tags') + 'tags', + 'hsts_max_age', + 'hsts_include_subdomains', + 'hsts_preload', +) LISTENER_COLUMNS = ( 'id', diff --git a/octaviaclient/osc/v2/listener.py b/octaviaclient/osc/v2/listener.py index 4f81f0a..410c68f 100644 --- a/octaviaclient/osc/v2/listener.py +++ b/octaviaclient/osc/v2/listener.py @@ -201,6 +201,32 @@ class CreateListener(command.ShowOne): help="Set the ALPN protocol to be used " "by the listener (can be set multiple times)." ) + parser.add_argument( + '--hsts-max-age', + dest='hsts_max_age', + metavar='', + type=int, + help="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." + ) + parser.add_argument( + '--hsts-include-subdomains', + action='store_true', + dest='hsts_include_subdomains', + help="Defines whether the includeSubDomains directive should be " + "added to the Strict-Transport-Security HTTP response " + "header." + ) + parser.add_argument( + '--hsts-preload', + action='store_true', + dest='hsts_preload', + help="Defines whether the preload directive should be " + "added to the Strict-Transport-Security HTTP response " + "header." + ) _tag.add_tag_option_to_parser_for_create( parser, 'listener') @@ -528,6 +554,32 @@ class SetListener(command.Command): help="Set the ALPN protocol to be used " "by the listener (can be set multiple times)." ) + parser.add_argument( + '--hsts-max-age', + dest='hsts_max_age', + metavar='', + type=int, + help="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." + ) + parser.add_argument( + '--hsts-include-subdomains', + action='store_true', + dest='hsts_include_subdomains', + help="Defines whether the includeSubDomains directive should be " + "added to the Strict-Transport-Security HTTP response " + "header." + ) + parser.add_argument( + '--hsts-preload', + action='store_true', + dest='hsts_preload', + help="Defines whether the preload directive should be " + "added to the Strict-Transport-Security HTTP response " + "header." + ) _tag.add_tag_option_to_parser_for_set(parser, 'listener') diff --git a/octaviaclient/osc/v2/utils.py b/octaviaclient/osc/v2/utils.py index 637839c..e05b943 100644 --- a/octaviaclient/osc/v2/utils.py +++ b/octaviaclient/osc/v2/utils.py @@ -322,6 +322,9 @@ def get_listener_attrs(client_manager, parsed_args): 'tls_ciphers': ('tls_ciphers', str), 'tls_versions': ('tls_versions', list), 'alpn_protocols': ('alpn_protocols', list), + 'hsts_max_age': ('hsts_max_age', int), + 'hsts_include_subdomains': ('hsts_include_subdomains', bool), + 'hsts_preload': ('hsts_preload', bool), } add_tags_attr_map(attr_map) diff --git a/octaviaclient/tests/unit/osc/v2/constants.py b/octaviaclient/tests/unit/osc/v2/constants.py index 39fcfaf..cf313ea 100644 --- a/octaviaclient/tests/unit/osc/v2/constants.py +++ b/octaviaclient/tests/unit/osc/v2/constants.py @@ -82,7 +82,8 @@ LISTENER_ATTRS = { 'tls_ciphers': "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256", 'tls_versions': ['TLSv1.1', 'TLSv1.2'], 'alpn_protocols': ['h2', 'http/1.1'], - "tags": ["foo", "bar"] + "tags": ["foo", "bar"], + 'hsts_header_opts': "max-age=16000000; includeSubDomains; preload;", } LOADBALANCER_ATTRS = { diff --git a/octaviaclient/tests/unit/osc/v2/test_listener.py b/octaviaclient/tests/unit/osc/v2/test_listener.py index 2da8c65..eb2de93 100644 --- a/octaviaclient/tests/unit/osc/v2/test_listener.py +++ b/octaviaclient/tests/unit/osc/v2/test_listener.py @@ -255,7 +255,11 @@ class TestListenerCreate(TestListener): '--alpn-protocol', self._listener.alpn_protocols[0], '--alpn-protocol', - self._listener.alpn_protocols[1]] + self._listener.alpn_protocols[1], + '--enable-hsts', + '--hsts-header-opts', + self._listener.hsts_header_opts, + ] verifylist = [ ('loadbalancer', 'mock_lb_id'), @@ -276,6 +280,9 @@ class TestListenerCreate(TestListener): self._listener.tls_versions), ('alpn_protocols', self._listener.alpn_protocols), + ('hsts_max_age', 15000000), + ('hsts_include_subdomains', False), + ('hsts_preload', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -386,7 +393,11 @@ class TestListenerSet(TestListener): '--alpn-protocol', self._listener.alpn_protocols[0], '--alpn-protocol', - self._listener.alpn_protocols[1]] + self._listener.alpn_protocols[1], + '--enable-hsts', + '--hsts-header-opts', + self._listener.hsts_header_opts, + ] verifylist = [ ('listener', self._listener.id), ('name', 'new_name'), @@ -402,7 +413,10 @@ class TestListenerSet(TestListener): ('allowed_cidrs', self._listener.allowed_cidrs), ('tls_ciphers', self._listener.tls_ciphers), ('tls_versions', self._listener.tls_versions), - ('alpn_protocols', self._listener.alpn_protocols) + ('alpn_protocols', self._listener.alpn_protocols), + ('hsts_max_age', 15000000), + ('hsts_include_subdomains', False), + ('hsts_preload', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -424,7 +438,12 @@ class TestListenerSet(TestListener): 'tls_ciphers': self._listener.tls_ciphers, 'tls_versions': self._listener.tls_versions, 'alpn_protocols': self._listener.alpn_protocols, - }}) + 'hsts_max_age': 15000000, + 'hsts_include_subdomains': False, + 'hsts_preload': False, + } + } + ) @mock.patch('osc_lib.utils.wait_for_status') def test_listener_set_wait(self, mock_wait): @@ -438,7 +457,12 @@ class TestListenerSet(TestListener): parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.api_mock.listener_set.assert_called_with( - self._listener.id, json={'listener': {'name': 'new_name'}}) + self._listener.id, json={ + 'listener': {'name': 'new_name', + 'hsts_include_subdomains': False, + 'hsts_preload': False} + } + ) mock_wait.assert_called_once_with( status_f=mock.ANY, res_id=self._listener.id, @@ -480,7 +504,11 @@ class TestListenerSet(TestListener): self.api_mock.listener_set.assert_called_once_with( self._listener.id, - json={"listener": {"tags": ['bar']}} + json={"listener": { + 'hsts_include_subdomains': False, + 'hsts_preload': False, + "tags": ['bar']} + } ) diff --git a/releasenotes/notes/add-hsts-support-f612ec171e28a3b3.yaml b/releasenotes/notes/add-hsts-support-f612ec171e28a3b3.yaml new file mode 100644 index 0000000..4fe971c --- /dev/null +++ b/releasenotes/notes/add-hsts-support-f612ec171e28a3b3.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Added support for HTTP Strict Transport Security (HSTS). This new feature + can be enabled and configured during listener creation using the new + ``--enable-hsts`` and ``--hsts-header-opts`` settings.