diff --git a/letsencrypt-apache/letsencrypt_apache/configurator.py b/letsencrypt-apache/letsencrypt_apache/configurator.py index d8157c33a..9319d8022 100644 --- a/letsencrypt-apache/letsencrypt_apache/configurator.py +++ b/letsencrypt-apache/letsencrypt_apache/configurator.py @@ -123,7 +123,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): self.version = version self.vhosts = None self._enhance_func = {"redirect": self._enable_redirect, - "hsts": self._enable_hsts} + "http-header": self._set_http_header} @property def mod_ssl_conf(self): @@ -682,7 +682,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): ############################################################################ def supported_enhancements(self): # pylint: disable=no-self-use """Returns currently supported enhancements.""" - return ["redirect", "hsts"] + return ["redirect", "http-header"] def enhance(self, domain, enhancement, options=None): """Enhance configuration. @@ -709,7 +709,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): logger.warn("Failed %s for %s", enhancement, domain) raise - def _enable_hsts(self, ssl_vhost, unused_options): + def _set_http_header(self, ssl_vhost, header_name): + # TODO REWRITE COMMENT """Enables the HSTS header on all HTTP responses. .. note:: HSTS defends against SSL stripping attacks. @@ -736,18 +737,22 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if "headers_module" not in self.parser.modules: self.enable_mod("headers") - # Check if HSTS header is already set - self._verify_no_hsts_header(ssl_vhost) + # Check if selected header is already set + self._verify_no_http_header(ssl_vhost, header_name) # Add directives to server - self.parser.add_dir(ssl_vhost.path, "Header", constants.HSTS_ARGS) - self.save_notes += ("Adding HSTS header to every response from ssl " - "vhost in %s\n" % (ssl_vhost.filep)) + self.parser.add_dir(ssl_vhost.path, "Header", + constants.HEADER_ARGS[header_name]) + + self.save_notes += ("Adding %s header to ssl vhost in %s\n" % + (header_name, ssl_vhost.filep)) + self.save() - logger.info("Adding HSTS header to every response from ssl vhost in %s", + logger.info("Adding %s header to ssl vhost in %s", header_name, ssl_vhost.filep) - def _verify_no_hsts_header(self, ssl_vhost): + def _verify_no_http_header(self, ssl_vhost, header_name): + # TODO revise comment """Checks to see if existing HSTS settings is in place. Checks to see if virtualhost already contains a HSTS header @@ -765,15 +770,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if header_path: # "Existing Header directive for virtualhost" for match in header_path: - if match == "Strict-Transport-Security": - raise errors.PluginError("Existing HSTS header") - - for match, arg in itertools.izip(header_path, constants.HSTS_ARGS): - if self.aug.get(match) != arg: - raise errors.PluginError("Unknown Existing HSTS header") - raise errors.PluginError("Let's Encrypt has already enabled HSTS") - - + if self.aug.get(match) == header_name.lower(): + raise errors.PluginError("Existing %s header" % + (header_name)) + def _enable_redirect(self, ssl_vhost, unused_options): """Redirect all equivalent HTTP traffic to ssl_vhost. diff --git a/letsencrypt-apache/letsencrypt_apache/constants.py b/letsencrypt-apache/letsencrypt_apache/constants.py index dac796c52..63f67fc91 100644 --- a/letsencrypt-apache/letsencrypt_apache/constants.py +++ b/letsencrypt-apache/letsencrypt_apache/constants.py @@ -28,8 +28,14 @@ REWRITE_HTTPS_ARGS = [ "^", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,QSA,R=permanent]"] """Apache rewrite rule arguments used for redirections to https vhost""" -HSTS_ARGS = [ - "always", "set", "Strict-Transport-Security", + +HSTS_ARGS = ["always", "set", "Strict-Transport-Security", "\"max-age=31536000; includeSubDomains\""] """Apache header arguments for HSTS""" +UIR_ARGS = ["always", "set", "Content-Security-Policy", + "upgrade-insecure-requests"] + +HEADER_ARGS = {"Strict-Transport-Security" : HSTS_ARGS, + "Upgrade-Insecure-Requests" : UIR_ARGS} + diff --git a/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py b/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py index adb96c2cd..3832cf13e 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py @@ -15,6 +15,7 @@ from letsencrypt import errors from letsencrypt.tests import acme_util from letsencrypt_apache import configurator +from letsencrypt_apache import constants from letsencrypt_apache import obj from letsencrypt_apache.tests import util @@ -509,13 +510,14 @@ class TwoVhost80Test(util.ApacheTest): @mock.patch("letsencrypt.le_util.run_script") @mock.patch("letsencrypt.le_util.exe_exists") - def test_hsts(self, mock_exe, _): + def test_http_header_hsts(self, mock_exe, _): self.config.parser.update_runtime_variables = mock.Mock() self.config.parser.modules.add("mod_ssl.c") mock_exe.return_value = True # This will create an ssl vhost for letsencrypt.demo - self.config.enhance("letsencrypt.demo", "hsts") + self.config.enhance("letsencrypt.demo", "http-header", + "Strict-Transport-Security") self.assertTrue("headers_module" in self.config.parser.modules) @@ -526,6 +528,31 @@ class TwoVhost80Test(util.ApacheTest): # load(). They must be found in sites-available hsts_header = self.config.parser.find_dir( "Header", None, ssl_vhost.path) + + # four args to HSTS header + self.assertEqual(len(hsts_header), 4) + + @mock.patch("letsencrypt.le_util.run_script") + @mock.patch("letsencrypt.le_util.exe_exists") + def test_http_header_hsts_with_conflict(self, mock_exe, _): + mock_exe.return_value = True + self.config.parser.update_runtime_variables = mock.Mock() + self.config.parser.modules.add("mod_ssl.c") + + ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[3]) + self.config.parser.add_dir( + ssl_vhost.path, "Header", constants.HEADER_ARGS[ + "Strict-Transport-Security"]) + + # This will create an ssl vhost for letsencrypt.demo + self.config.enhance(self.vh_truth[3].name, "http-header", + "Strict-Transport-Security") + + # These are not immediately available in find_dir even with save() and + # load(). They must be found in sites-available + hsts_header = self.config.parser.find_dir( + "Header", None, ssl_vhost.path) + # four args to HSTS header self.assertEqual(len(hsts_header), 4)