Gradually increasing HSTS max-age (#5912)

This PR adds the functionality to enhance Apache configuration to include HTTP Strict Transport Security header with a low initial max-age value.

The max-age value will get increased on every (scheduled) run of certbot renew regardless of the certificate actually getting renewed, if the last increase took place longer than ten hours ago. The increase steps are visible in constants.AUTOHSTS_STEPS.

Upon the first actual renewal after reaching the maximum increase step, the max-age value will be made "permanent" and will get value of one year.

To achieve accurate VirtualHost discovery on subsequent runs, a comment with unique id string will be added to each enhanced VirtualHost.

* AutoHSTS code rebased on master

* Fixes to match the changes in master

* Make linter happy with metaclass registration

* Address small review comments

* Use new enhancement interfaces

* New style enhancement changes

* Do not allow --hsts and --auto-hsts simultaneuously

* MyPy annotation fixes and added test

* Change oldest requrements to point to local certbot core version

* Enable new style enhancements for run and install verbs

* Test refactor

* New test class for main.install tests

* Move a test to a correct test class
This commit is contained in:
Joona Hoikkala
2018-06-21 07:27:19 -07:00
committed by Brad Warren
parent a875246a4b
commit 3877af6619
17 changed files with 1027 additions and 35 deletions
+37 -7
View File
@@ -36,7 +36,7 @@ from certbot import util
from certbot.display import util as display_util, ops as display_ops
from certbot.plugins import disco as plugins_disco
from certbot.plugins import selection as plug_sel
from certbot.plugins import enhancements
USER_CANCELLED = ("User chose to cancel the operation and may "
"reinvoke the client.")
@@ -750,8 +750,8 @@ def _install_cert(config, le_client, domains, lineage=None):
:param le_client: Client object
:type le_client: client.Client
:param plugins: List of domains
:type plugins: `list` of `str`
:param domains: List of domains
:type domains: `list` of `str`
:param lineage: Certificate lineage object. Defaults to `None`
:type lineage: storage.RenewableCert
@@ -789,11 +789,19 @@ def install(config, plugins):
except errors.PluginSelectionError as e:
return str(e)
if not enhancements.are_supported(config, installer):
raise errors.NotSupportedError("One ore more of the requested enhancements "
"are not supported by the selected installer")
# If cert-path is defined, populate missing (ie. not overridden) values.
# Unfortunately this can't be done in argument parser, as certificate
# manager needs the access to renewal directory paths
if config.certname:
config = _populate_from_certname(config)
elif enhancements.are_requested(config):
# Preflight config check
raise errors.ConfigurationError("One or more of the requested enhancements "
"require --cert-name to be provided")
if config.key_path and config.cert_path:
_check_certificate_and_key(config)
domains, _ = _find_domains_or_certname(config, installer)
@@ -804,6 +812,11 @@ def install(config, plugins):
"If your certificate is managed by Certbot, please use --cert-name "
"to define which certificate you would like to install.")
if enhancements.are_requested(config):
# In the case where we don't have certname, we have errored out already
lineage = cert_manager.lineage_for_certname(config, config.certname)
enhancements.enable(lineage, domains, installer, config)
def _populate_from_certname(config):
"""Helper function for install to populate missing config values from lineage
defined by --cert-name."""
@@ -881,7 +894,8 @@ def enhance(config, plugins):
"""
supported_enhancements = ["hsts", "redirect", "uir", "staple"]
# Check that at least one enhancement was requested on command line
if not any([getattr(config, enh) for enh in supported_enhancements]):
oldstyle_enh = any([getattr(config, enh) for enh in supported_enhancements])
if not enhancements.are_requested(config) and not oldstyle_enh:
msg = ("Please specify one or more enhancement types to configure. To list "
"the available enhancement types, run:\n\n%s --help enhance\n")
logger.warning(msg, sys.argv[0])
@@ -892,6 +906,10 @@ def enhance(config, plugins):
except errors.PluginSelectionError as e:
return str(e)
if not enhancements.are_supported(config, installer):
raise errors.NotSupportedError("One ore more of the requested enhancements "
"are not supported by the selected installer")
certname_question = ("Which certificate would you like to use to enhance "
"your configuration?")
config.certname = cert_manager.get_certnames(
@@ -907,11 +925,15 @@ def enhance(config, plugins):
if not domains:
raise errors.Error("User cancelled the domain selection. No domains "
"defined, exiting.")
lineage = cert_manager.lineage_for_certname(config, config.certname)
if not config.chain_path:
lineage = cert_manager.lineage_for_certname(config, config.certname)
config.chain_path = lineage.chain_path
le_client = _init_le_client(config, authenticator=None, installer=installer)
le_client.enhance_config(domains, config.chain_path, ask_redirect=False)
if oldstyle_enh:
le_client = _init_le_client(config, authenticator=None, installer=installer)
le_client.enhance_config(domains, config.chain_path, ask_redirect=False)
if enhancements.are_requested(config):
enhancements.enable(lineage, domains, installer, config)
def rollback(config, plugins):
@@ -1073,6 +1095,11 @@ def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals
except errors.PluginSelectionError as e:
return str(e)
# Preflight check for enhancement support by the selected installer
if not enhancements.are_supported(config, installer):
raise errors.NotSupportedError("One ore more of the requested enhancements "
"are not supported by the selected installer")
# TODO: Handle errors from _init_le_client?
le_client = _init_le_client(config, authenticator, installer)
@@ -1091,6 +1118,9 @@ def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals
_install_cert(config, le_client, domains, new_lineage)
if enhancements.are_requested(config) and new_lineage:
enhancements.enable(new_lineage, domains, installer, config)
if lineage is None or not should_get_cert:
display_ops.success_installation(domains)
else: