From 1d45c57fd27b1049e5663b6814ded08a52cbf3a4 Mon Sep 17 00:00:00 2001 From: Nobuto Murata Date: Fri, 13 Oct 2017 10:53:09 -0400 Subject: [PATCH] Allow to configure max-age for HSTS(HTTP Strict Transport Security) HSTS is helpful to bring more protection to users, but on the other hand, it locks down users to use HTTPS only until max-age expires. To enable HSTS, admins must enable enforce-ssl option and set non-zero value to hsts-max-age-seconds explicitly. Content Security Policy (CSP) is not enabled this time. Horizon upstream may need some work: https://bugs.launchpad.net/horizon/+bug/1618024 Change-Id: I7fd774ba9a1c292d51625d6d36a086b2a531ae75 Partial-Bug: #1713202 --- config.yaml | 16 ++++++++++++++++ hooks/horizon_contexts.py | 3 ++- templates/default-ssl | 3 +++ unit_tests/test_horizon_contexts.py | 18 +++++++++++++++--- 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/config.yaml b/config.yaml index 690a44a8..7e2cf1d5 100644 --- a/config.yaml +++ b/config.yaml @@ -301,6 +301,22 @@ options: description: | If True, redirects plain http requests to https port 443. For this option to have an effect, SSL must be configured. + hsts-max-age-seconds: + type: int + default: 0 + description: | + "max-age" parameter for HSTS(HTTP Strict Transport Security) + header. Use with caution since once you set this option, browsers + will remember it so they can only use HTTPS (HTTP connection won't + be allowed) until max-age expires. + . + An example value is one year (31536000). However, a shorter + max-age such as 24 hours (86400) is recommended during initial + rollout in case of any mistakes. For more details on HSTS, refer to: + https://developer.mozilla.org/docs/Web/Security/HTTP_strict_transport_security + . + For this option to have an effect, SSL must be configured and + enforce-ssl option must be true. database-user: type: string default: horizon diff --git a/hooks/horizon_contexts.py b/hooks/horizon_contexts.py index a4c75c7d..dc509293 100644 --- a/hooks/horizon_contexts.py +++ b/hooks/horizon_contexts.py @@ -209,7 +209,8 @@ class ApacheContext(OSContextGenerator): ctxt = { 'http_port': 70, 'https_port': 433, - 'enforce_ssl': False + 'enforce_ssl': False, + 'hsts_max_age_seconds': config('hsts-max-age-seconds') } if config('enforce-ssl'): diff --git a/templates/default-ssl b/templates/default-ssl index 08ed57aa..2a588086 100644 --- a/templates/default-ssl +++ b/templates/default-ssl @@ -33,6 +33,9 @@ {% if ssl_configured %} SSLCertificateFile {{ ssl_cert }} SSLCertificateKeyFile {{ ssl_key }} +{% if enforce_ssl %} + Header set Strict-Transport-Security "max-age={{ hsts_max_age_seconds }}" +{% endif %} {% else %} SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key diff --git a/unit_tests/test_horizon_contexts.py b/unit_tests/test_horizon_contexts.py index cbcb0d67..77ea4a8d 100644 --- a/unit_tests/test_horizon_contexts.py +++ b/unit_tests/test_horizon_contexts.py @@ -63,21 +63,33 @@ class TestHorizonContexts(CharmTestCase): def test_Apachecontext(self): self.assertEqual(horizon_contexts.ApacheContext()(), {'http_port': 70, 'https_port': 433, - 'enforce_ssl': False}) + 'enforce_ssl': False, + 'hsts_max_age_seconds': 0}) def test_Apachecontext_enforce_ssl(self): self.test_config.set('enforce-ssl', True) self.get_cert.return_value = ('cert', 'key') self.assertEquals(horizon_contexts.ApacheContext()(), {'http_port': 70, 'https_port': 433, - 'enforce_ssl': True}) + 'enforce_ssl': True, + 'hsts_max_age_seconds': 0}) def test_Apachecontext_enforce_ssl_no_cert(self): self.test_config.set('enforce-ssl', True) self.get_cert.return_value = (None, 'key') self.assertEquals(horizon_contexts.ApacheContext()(), {'http_port': 70, 'https_port': 433, - 'enforce_ssl': False}) + 'enforce_ssl': False, + 'hsts_max_age_seconds': 0}) + + def test_Apachecontext_hsts_max_age_seconds(self): + self.test_config.set('enforce-ssl', True) + self.get_cert.return_value = ('cert', 'key') + self.test_config.set('hsts-max-age-seconds', 15768000) + self.assertEquals(horizon_contexts.ApacheContext()(), + {'http_port': 70, 'https_port': 433, + 'enforce_ssl': True, + 'hsts_max_age_seconds': 15768000}) @patch.object(horizon_contexts, 'get_ca_cert', lambda: None) @patch('os.chmod')