-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Gradually increasing HSTS max-age #5912
Conversation
The Py3 tests will fail until #5913 is merged and this branch is brought up to date. |
@@ -48,3 +48,16 @@ | |||
|
|||
HEADER_ARGS = {"Strict-Transport-Security": HSTS_ARGS, | |||
"Upgrade-Insecure-Requests": UIR_ARGS} | |||
|
|||
AUTOHSTS_STEPS = [60, 300, 900, 3600, 21600, 43200, 86400] | |||
"""AutoHSTS increase steps: 5min, 15min, 1h, 6h, 12h, 24h""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: We're missing the first step of 1 min here.
@@ -1470,6 +1471,63 @@ def _add_name_vhost_if_necessary(self, vhost): | |||
if need_to_save: | |||
self.save() | |||
|
|||
def find_vhost_by_id(self, id_str): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Everywhere this function is called outside of tests, no checks are done that the return type isn't None
. This to me suggestions we should raise an exception here since we're failing to find the vhost we're supposed to be updating.
:type ssl_vhost: :class:`~certbot_apache.obj.VirtualHost` or None | ||
|
||
""" | ||
try: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can remove the try
and except
here. certbot.client.Client.apply_enhancement
already catches PluginEnhancementAlreadyPresent
errors, logs a message, and continues execution.
# AutoHSTS not enabled | ||
return | ||
curtime = time.time() | ||
for id_str in managed: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I'd use something like:
for id_str, config in managed.items():
for shorter lines in the body of the loop and to avoid repeatedly looking up the matching config values.
return | ||
vhosts = [] | ||
# Copy, as we are removing from the dict inside the loop | ||
for id_str in list(managed.keys()): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I'd use something like:
for id_str, config in list(managed.items()):
Matches the private key path. | ||
""" | ||
|
||
return any( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think you need this any
here. any
iterates over the list calling bool
on each item, returning True
if any item in the list is True
. All we're checking here is that the list is non-empty right?
If so, I think the most Pythonic thing to do is just return the list or use bool
instead of any
if you want True
or False
to be returned.
managed = self.storage.fetch("autohsts") | ||
except KeyError: | ||
# AutoHSTS not enabled | ||
return |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the long run, I'd like to get auto_hsts
saved in the renewal conf file and if this method is called and autohsts
isn't in plugin storage, we blow up. This helps people get notified if .pluginstorage.json
is deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That sounds like a good way to address this. Let's look into it as a part of the larger discussion about the enhancement interfaces.
"of {0} to VirtualHost in {1}\n".format( | ||
initial_maxage, ssl_vhost.filep)) | ||
self.save_notes += note_msg | ||
self.save(note_msg) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you're calling this method from the old enhancement system, you shouldn't call save here. This is done for you in certbot.client.Client.apply_enhancement
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will get addressed as a part of the enhancement interfaces refactor.
certbot/cli.py
Outdated
"--auto-hsts", action="store_true", dest="auto_hsts", | ||
default=flag_default("auto_hsts"), | ||
help="Automatically increase Strict-Transport-Security header maxAge " | ||
"value over time. Implies --hsts.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens when both --auto-hsts
and --hsts
are set?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Valid question. This isn't currently handled too well. I believe we should ignore --hsts
and go with the --auto-hsts
, as that's what the user most likely wants.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That'd be fine, but I personally would prefer just failing fast and telling the user to pick only one. This avoids us having to make a call about what we think the user wants and directly tells them that their command doesn't make sense. This could be done in certbot.cli.HelpfulArgumentParser.parse_args
.
when running certbot with "renew" verb, regardless of the lineage | ||
being renewed. | ||
""" | ||
self._autohsts_update() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought the plan was to make another enhancement type for autohsts updates. Did you change your mind or do you want to do this in another PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking of doing this later, as a part of a redesign of the complex enhancements but looks like it would make most sense to implement it as a part of this PR as well. I'm answering this in more detail in the comment about the interfaces, and old enhancement system.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In addition to my inline comments, I think we need to be calling restart()
somewhere after adding/modifying the HSTS value.
certbot/cli.py
Outdated
@@ -1124,6 +1129,16 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis | |||
default=flag_default("redirect"), | |||
help="Do not automatically redirect all HTTP traffic to HTTPS for the newly " | |||
"authenticated vhost. (default: Ask)") | |||
for enh in enhancements._INDEX: # pylint: disable=protected-access |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Maybe we should make a simple function in the enhancements
module that takes the add
method of helpful
and adds the flag for all enhancements. This is similar to what we do for plugins, breaks up this large function, and allows us to remove the pylint: disable
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this makes it cleaner, and is now implemented.
certbot/cli.py
Outdated
helpful.add(enh["cli_groups"], enh["cli_flag"], action=enh["cli_action"], | ||
dest=enh["cli_dest"], default=enh["cli_flag_default"], | ||
help=enh["cli_help"]) | ||
helpful.add( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we still need this here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, this is some leftover cruft from the earlier iteration without the new enhancement system. Removed.
certbot/client.py
Outdated
@@ -516,6 +516,7 @@ def enhance_config(self, domains, chain_path, ask_redirect=True): | |||
|
|||
enhanced = False | |||
enhancement_info = ( | |||
("auto_hsts", "auto_hsts", None), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is cruft that can be removed. When will auto_hsts
ever be returned from the call to supported_enhancements()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, this is some leftover cruft from the earlier iteration without the new enhancement system. Removed.
certbot/plugins/enhancements.py
Outdated
for enh in _INDEX: | ||
enh_requested = hasattr(config, enh["cli_dest"]) | ||
enh_enabled = bool(getattr(config, enh["cli_dest"])) | ||
if enh_requested and enh_enabled: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We duplicate these four lines elsewhere in this module and I think they can be simplified and this duplication removed.
I don't think we need this cast to bool
here. Also, in what scenario are we worried about the config not having the enh["cli_dest"]
attribute? I couldn't come up with one so I think we can remove it but if there is one, we can make use of the default value parameter to getattr and do this with one value rather than two.
Additionally, I wonder if we shouldn't make a simple generator function that yields the requested enhancements. This method could be public or private, but would look something like:
def enabled_enhancements(config):
for enh in _INDEX:
if getattr(config, enh["cli_dest"]):
yield enh
Then this method could be as simple as:
def is_supported(config):
return any(enabled_enhancements(config))
Up to you if you want to add the generator method, but I think the casting and using two variables when one will do should be fixed at least.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this approach, the now implemented generator sure yields (pun intended) cleaner and more readable code.
certbot/plugins/enhancements.py
Outdated
from acme.magic_typing import Dict, List, Any # pylint: disable=unused-import, no-name-in-module | ||
|
||
|
||
def is_supported(config): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I wonder if a name like is_requested
or is_enabled
is better here. Initially, I thought this function was checking if the requested enhancements were supported by the plugin. I also personally like are
rather than is
because there will be multiple enhancements in the future, but that's a super nitpick. I think you may have been trying to differentiate between enhancements defined here and defined elsewhere, but I personally think it's fine to just explain this in a comment.
If you disagree feel free to leave it as is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed, and it's better this way, thanks!
vhost = self.find_vhost_by_id(id_str) | ||
except errors.PluginError: | ||
# Remove the orphaned AutoHSTS entry from pluginstorage | ||
self._autohsts.pop(id_str) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should log about this here.
continue | ||
if self._autohsts_vhost_in_lineage(vhost, lineage): | ||
vhosts.append(vhost) | ||
self._autohsts.pop(id_str) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is definitely a nit, but if Certbot gets interrupted between this line and the Apache config update down below, they'll never get the large max-age bump. Maybe we should keep the relevant id strings around and pop them afterwards?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, handling the modification later on in the code now.
self.assertTrue( | ||
"VirtualHost with id orphan_id was not" in mock_log.call_args[0][0]) | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
super nit: Too many blank lines.
AUTOHSTS_PERMANENT = 31536000 | ||
"""Value for the last max-age of HSTS""" | ||
|
||
AUTOHSTS_FREQ = 36000 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still think this value and/or AUTOHSTS_STEPS
should be increased. Do you disagree?
certbot/main.py
Outdated
le_client = _init_le_client(config, authenticator=None, installer=installer) | ||
le_client.enhance_config(domains, config.chain_path, ask_redirect=False) | ||
if enhancements.is_supported(config): | ||
enhancements.enable(lineage, domains, installer, config) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This enhancement can currently only be enabled with the enhance
subcommand. I think in the long term we also want it to work for run
and install
.
If this was on purpose and you want to do it later, that's fine, otherwise, I think this should be moved to le_client.enhance_config
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will add this functionality after #6097 lands.
certbot/plugins/enhancement_test.py
Outdated
@@ -0,0 +1,179 @@ | |||
"""Tests for new style enhancements""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We still need to do #5912 (comment).
@@ -908,11 +909,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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh Python. Nice catch moving this outside of the if not config.chain_path
branch to avoid an ugly crash down below when calling enhancements.enable
.
certbot-apache/setup.py
Outdated
@@ -8,7 +8,7 @@ | |||
# acme/certbot version. | |||
install_requires = [ | |||
'acme>0.24.0', | |||
'certbot>=0.21.1', | |||
'certbot>0.24.0', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should now be set to certbot>0.25.1
"of {0} to VirtualHost in {1}\n".format( | ||
initial_maxage, ssl_vhost.filep)) | ||
self.save_notes += note_msg | ||
self.save(note_msg) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to call restart after this.
Also, while I missed this in my previous review, the way we're using save
here is different from the other AutoHSTS methods and the old enhancement system. Here we're calling save
after enabling HSTS for each domain, with each save creating a checkpoint, rather than creating one save at the end. I think the single save is nicer rather than creating up to 100 checkpoints (and doing 100 restarts) in a single run.
These are now fixed, and the new style enhancements have been enabled for
Functionality wise it should be ready for the next round though. |
if _enhanced_vhosts: | ||
note_msg = "Enabling AutoHSTS" | ||
self.save(note_msg) | ||
logger.info(note_msg) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we need to call restart()
here.
certbot/tests/util.py
Outdated
@@ -379,3 +380,26 @@ def hold_lock(cv, lock_path): # pragma: no cover | |||
cv.notify() | |||
cv.wait() | |||
my_lock.release() | |||
|
|||
|
|||
class MockInstallerAutoHSTS(enhancements.AutoHSTSEnhancement): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if we need this class. I think we can get by with setting the spec attribute of the Mock class to enhancements.AutoHSTSEnhancement
so it'll pass isinstance
checks. Is there something else we get here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We get the call counters for the interface methods, that are easy to check in tests. For this I'd like to keep it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But what's wrong with installer.update_autohsts.call_count
, installer.update_autohsts.called
, installer.update_autohsts.call_args
, etc?
This seems more conventional and takes less code. The only change with the current approach I see is you access these values on installer.update_counter
rather than installer.update_autohsts
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You have a point, and the tests are now refactored. The tests for interfaces.GenericUpdater
and interfaces.RenewDeployer
were similar and handled in the same files, so I went ahead and refactored those too for consistency.
certbot/plugins/enhancement_test.py
Outdated
@test_util.patch_get_utility() | ||
def test_enhancement_updates(self, _, mock_select): | ||
mock_select.return_value = (self.mockinstaller, None) | ||
updater.run_generic_updaters(self.config, mock.MagicMock(), None) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry to keep nitpicking this, but I think tests that are calling into other modules like updater
should be deleted or moved to that module. I think we should just be testing the functions in enhancements
here. The tests should be very simple.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes a lot of sense, and the tests have now been refactored as such.
certbot/plugins/enhancement_test.py
Outdated
@@ -0,0 +1,103 @@ | |||
"""Tests for new style enhancements""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should rename this file to the plural "enhancements" to match the name of the module.
certbot/tests/main_test.py
Outdated
|
||
@mock.patch('certbot.main.plug_sel.record_chosen_plugins') | ||
@mock.patch('certbot.main.plug_sel.pick_installer') | ||
def test_install_enhancement_not_supported(self, mock_inst, _rec): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One more test location nit. This docstring for this test class is "Tests for certbot.main.enhance.", but this test and the two below it aren't testing the enhance method. I think the one calling run
should be moved to RunTest
. We don't have an InstallTest
class, but I'd prefer we created one and put these methods there.
certbot/tests/renewupdater_test.py
Outdated
@@ -75,6 +62,68 @@ def test_deployer_skip_dry_run(self, mock_log): | |||
self.assertEquals(mock_log.call_args[0][0], | |||
"Skipping renewal deployer in dry-run mode.") | |||
|
|||
@mock.patch('certbot.plugins.selection.choose_configurator_plugins') | |||
@test_util.patch_get_utility() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need to call patch_get_utility
here or on any of the tests below. I just ran tests with them removed and they still passed.
certbot/tests/main_test.py
Outdated
self._call, ['enhance', '--auto-hsts', '--hsts']) | ||
|
||
@mock.patch('certbot.main.plug_sel.choose_configurator_plugins') | ||
def test_run_enhancement_not_supported(self, mock_choose): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like part of my comment at #5912 (comment) was missed. Can we move this to the RunTest
class in this file?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm very sorry, this is getting embarrassing. I'm apparently too eager to get the PR out of my hands. Moving the tests now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No worries!
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 ofcertbot renew
regardless of the certificate actually getting renewed, if the last increase took place longer than ten hours ago. The increase steps are visible inconstants.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 enhancedVirtualHost
.