mirror of
https://github.com/certbot/certbot.git
synced 2026-07-24 14:55:44 +02:00
renewal: by default, use a fraction of lifetime (#10207)
Previously we defaulted to renewing at 30 days before expiry, and allowed users to customize the config file to set a different value. Instead, we should renew when 1/3 of the lifetime is left, or for shorter certificates (<10 days), when 1/2 of the lifetime is left. This still allows explicitly configured values to take precedence. --------- Co-authored-by: Will Greenberg <ifnspifn@gmail.com>
This commit is contained in:
co-authored by
Will Greenberg
parent
b3bd4304f4
commit
d91e552491
@@ -10,7 +10,10 @@ Certbot adheres to [Semantic Versioning](https://semver.org/).
|
||||
|
||||
### Changed
|
||||
|
||||
*
|
||||
* Certificates now renew with 1/3rd of lifetime left (or 1/2 of lifetime left,
|
||||
if the lifetime is shorter than 10 days). This is a change from a hardcoded
|
||||
renewal at 30 days before expiration. The config field renew_before_expiry
|
||||
still overrides this default.
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@@ -1001,17 +1001,32 @@ class RenewableCert(interfaces.RenewableCert):
|
||||
logger.debug("Should renew, certificate is revoked.")
|
||||
return True
|
||||
|
||||
# Renews some period before expiry time
|
||||
default_interval = constants.RENEWER_DEFAULTS["renew_before_expiry"]
|
||||
interval = self.configuration.get("renew_before_expiry", default_interval)
|
||||
expiry = crypto_util.notAfter(self.version(
|
||||
"cert", self.latest_common_version()))
|
||||
cert = self.version("cert", self.latest_common_version())
|
||||
notBefore = crypto_util.notBefore(cert)
|
||||
notAfter = crypto_util.notAfter(cert)
|
||||
lifetime = notAfter - notBefore
|
||||
|
||||
config_interval = self.configuration.get("renew_before_expiry")
|
||||
now = datetime.datetime.now(pytz.UTC)
|
||||
if expiry < add_time_interval(now, interval):
|
||||
if config_interval is not None and notAfter < add_time_interval(now, config_interval):
|
||||
logger.debug("Should renew, less than %s before certificate "
|
||||
"expiry %s.", interval,
|
||||
expiry.strftime("%Y-%m-%d %H:%M:%S %Z"))
|
||||
"expiry %s.", config_interval,
|
||||
notAfter.strftime("%Y-%m-%d %H:%M:%S %Z"))
|
||||
return True
|
||||
|
||||
# No config for "renew_before_expiry", provide default behavior.
|
||||
# For most certs, renew with 1/3 of certificate lifetime remaining.
|
||||
# For short lived certificates, renew at 1/2 of certificate lifetime.
|
||||
default_interval = lifetime / 3
|
||||
if lifetime.total_seconds() < 10 * 86400:
|
||||
default_interval = lifetime / 2
|
||||
remaining_time = notAfter - now
|
||||
if remaining_time < default_interval:
|
||||
logger.debug("Should renew, less than %ss before certificate "
|
||||
"expiry %s.", default_interval,
|
||||
notAfter.strftime("%Y-%m-%d %H:%M:%S %Z"))
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -19,8 +19,33 @@ from certbot.compat import filesystem
|
||||
from certbot.compat import os
|
||||
import certbot.tests.util as test_util
|
||||
|
||||
CERT = test_util.load_cert('cert_512.pem')
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from cryptography.hazmat.primitives import serialization, hashes
|
||||
from cryptography import x509
|
||||
from cryptography.x509 import Certificate
|
||||
|
||||
import datetime
|
||||
from typing import Optional, Any
|
||||
|
||||
def make_cert_with_lifetime(not_before: datetime.datetime, lifetime_days: int) -> bytes:
|
||||
"""Return PEM of a self-signed certificate with the given notBefore and lifetime."""
|
||||
key = ec.generate_private_key(ec.SECP256R1())
|
||||
not_after=not_before + datetime.timedelta(days=lifetime_days)
|
||||
cert = x509.CertificateBuilder(
|
||||
issuer_name=x509.Name([]),
|
||||
subject_name=x509.Name([]),
|
||||
public_key=key.public_key(),
|
||||
serial_number=x509.random_serial_number(),
|
||||
not_valid_before=not_before,
|
||||
not_valid_after=not_after,
|
||||
).add_extension(
|
||||
x509.SubjectAlternativeName([x509.DNSName("example.com")]),
|
||||
critical=False,
|
||||
).sign(
|
||||
private_key=key,
|
||||
algorithm=hashes.SHA256(),
|
||||
)
|
||||
return cert.public_bytes(serialization.Encoding.PEM)
|
||||
|
||||
def unlink_all(rc_object):
|
||||
"""Unlink all four items associated with this RenewableCert."""
|
||||
@@ -445,32 +470,45 @@ class RenewableCertTests(BaseRenewableCertTest):
|
||||
@mock.patch("certbot._internal.storage.datetime")
|
||||
def test_time_interval_judgments(self, mock_datetime, mock_set_by_user):
|
||||
"""Test should_autorenew() on the basis of expiry time windows."""
|
||||
test_cert = test_util.load_vector("cert_512.pem")
|
||||
# Note: this certificate happens to have a lifetime of 7 days,
|
||||
# and the tests below that use a "None" interval (i.e. choose a
|
||||
# default) rely on that fact.
|
||||
#
|
||||
# Not Before: Dec 11 22:34:45 2014 GMT
|
||||
# Not After : Dec 18 22:34:45 2014 GMT
|
||||
not_before = datetime.datetime(2014, 12, 11, 22, 34, 45)
|
||||
short_cert = make_cert_with_lifetime(not_before, 7)
|
||||
|
||||
self._write_out_ex_kinds()
|
||||
|
||||
self.test_rc.update_all_links_to(12)
|
||||
with open(self.test_rc.cert, "wb") as f:
|
||||
f.write(test_cert)
|
||||
f.write(short_cert)
|
||||
self.test_rc.update_all_links_to(11)
|
||||
with open(self.test_rc.cert, "wb") as f:
|
||||
f.write(test_cert)
|
||||
f.write(short_cert)
|
||||
|
||||
mock_datetime.timedelta = datetime.timedelta
|
||||
mock_set_by_user.return_value = False
|
||||
self.test_rc.configuration["renewalparams"] = {}
|
||||
|
||||
for (current_time, interval, result) in [
|
||||
# 2014-12-13 12:00:00+00:00 (about 5 days prior to expiry)
|
||||
# 2014-12-13 12:00 (about 5 days prior to expiry)
|
||||
# Times that should result in autorenewal/autodeployment
|
||||
(1418472000, "2 months", True), (1418472000, "1 week", True),
|
||||
# Times that should not
|
||||
# With the "default" logic, this 7-day certificate should autorenew
|
||||
# at 3.5 days prior to expiry. We haven't reached that yet,
|
||||
# so don't renew.
|
||||
(1418472000, None, False),
|
||||
# 2014-12-16 03:20, a little less than 3.5 days to expiry.
|
||||
(1418700000, None, True),
|
||||
# Times that should not renew
|
||||
(1418472000, "4 days", False), (1418472000, "2 days", False),
|
||||
# 2009-05-01 12:00:00+00:00 (about 5 years prior to expiry)
|
||||
# Times that should result in autorenewal/autodeployment
|
||||
(1241179200, "7 years", True),
|
||||
(1241179200, "11 years 2 months", True),
|
||||
# Times that should not
|
||||
# Times that should not renew
|
||||
(1241179200, "8 hours", False), (1241179200, "2 days", False),
|
||||
(1241179200, "40 days", False), (1241179200, "9 months", False),
|
||||
# 2015-01-01 (after expiry has already happened, so all
|
||||
@@ -480,6 +518,28 @@ class RenewableCertTests(BaseRenewableCertTest):
|
||||
(1420070400, "10 minutes", True),
|
||||
(1420070400, "10 weeks", True), (1420070400, "10 months", True),
|
||||
(1420070400, "10 years", True), (1420070400, "99 months", True),
|
||||
(1420070400, None, True)
|
||||
]:
|
||||
sometime = datetime.datetime.fromtimestamp(current_time, pytz.UTC)
|
||||
mock_datetime.datetime.now.return_value = sometime
|
||||
self.test_rc.configuration["renew_before_expiry"] = interval
|
||||
assert self.test_rc.should_autorenew() == result
|
||||
|
||||
# Lifetime: 31 years
|
||||
# Default renewal: about 10 years from expiry
|
||||
# Not Before: May 29 07:42:01 2017 GMT
|
||||
# Not After : Mar 30 07:42:01 2048 GMT
|
||||
not_before=datetime.datetime(2017, 5, 29, 7, 42, 1)
|
||||
long_cert = make_cert_with_lifetime(not_before, 31 * 365)
|
||||
self.test_rc.update_all_links_to(12)
|
||||
with open(self.test_rc.cert, "wb") as f:
|
||||
f.write(long_cert)
|
||||
self.test_rc.update_all_links_to(11)
|
||||
with open(self.test_rc.cert, "wb") as f:
|
||||
f.write(long_cert)
|
||||
for (current_time, result) in [
|
||||
(2114380800, False), # 2037-01-01
|
||||
(2148000000, True), # 2038-01-25
|
||||
]:
|
||||
sometime = datetime.datetime.fromtimestamp(current_time, pytz.UTC)
|
||||
mock_datetime.datetime.now.return_value = sometime
|
||||
|
||||
@@ -628,10 +628,6 @@ Follow these steps to safely delete a certificate:
|
||||
Renewing certificates
|
||||
---------------------
|
||||
|
||||
.. note:: Let's Encrypt CA issues short-lived certificates (90
|
||||
days). Make sure you renew the certificates at least once in 3
|
||||
months.
|
||||
|
||||
.. seealso:: Most Certbot installations come with automatic
|
||||
renewal out of the box. See `Automated Renewals`_ for more details.
|
||||
|
||||
@@ -639,14 +635,18 @@ Renewing certificates
|
||||
will not renew automatically, unless combined with authentication hook scripts.
|
||||
See `Renewal with the manual plugin <#manual-renewal>`_.
|
||||
|
||||
As of version 0.10.0, Certbot supports a ``renew`` action to check
|
||||
all installed certificates for impending expiry and attempt to renew
|
||||
them. The simplest form is simply
|
||||
Certbot supports a ``renew`` action to check all installed certificates for
|
||||
impending expiry and attempt to renew them. The simplest form is simply
|
||||
|
||||
``certbot renew``
|
||||
|
||||
This command attempts to renew any previously-obtained certificates that
|
||||
expire in less than 30 days. The same plugin and options that were used
|
||||
This command attempts to renew any previously-obtained certificates which are ready
|
||||
for renewal. As of Certbot 4.0.0, a certificate is considered ready for renewal
|
||||
when less than 1/3rd of its lifetime remains. For certificates with a lifetime
|
||||
of 10 days or less, that threshold is 1/2 of the lifetime. Prior to Certbot 4.0.0
|
||||
the threshold was a fixed 30 days.
|
||||
|
||||
The same plugin and options that were used
|
||||
at the time the certificate was originally issued will be used for the
|
||||
renewal attempt, unless you specify other plugins or options. Unlike ``certonly``, ``renew`` acts on
|
||||
multiple certificates and always takes into account whether each one is near
|
||||
|
||||
Reference in New Issue
Block a user