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:
Jacob Hoffman-Andrews
2025-03-12 10:35:59 -07:00
committed by GitHub
co-authored by Will Greenberg
parent b3bd4304f4
commit d91e552491
4 changed files with 103 additions and 25 deletions
+4 -1
View File
@@ -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
+23 -8
View File
@@ -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
+9 -9
View File
@@ -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