* [PATCH pve-common v2 1/2] certificate: add helpers for lifetime-based ACME renewal
2026-10-20 8:24 [PATCH common/manager v2 0/2] fix #8006: acme: lifetime-based certificate renewal threshold David Riley
@ 2026-10-20 8:24 ` David Riley
2026-10-20 13:34 ` Michael Köppl
2026-10-20 8:24 ` [PATCH pve-manager v2 2/2] fix #8006: api: acme: lifetime-based certificate renewal threshold David Riley
` (2 subsequent siblings)
3 siblings, 1 reply; 7+ messages in thread
From: David Riley @ 2026-10-20 8:24 UTC (permalink / raw)
To: pve-devel; +Cc: David Riley
Add helpers to calculate a renewal lead time based on the actual
lifetime of an X.509 certificate.
Previously, certificate renewals relied on a hardcoded threshold.
To prepare for dynamic thresholds (#8006), introduce calculations
that scale the renewal lead time proportionally to the certificate's
validity period.
The scaling mirrors the logic already used in Proxmox Backup Server
[1]: certificates living for less than 10 days are renewed once 1/2 of
their lifetime has elapsed, while longer-lived certificates are
renewed once 2/3 has elapsed [2]. A 3-day minimum floor is present to
ensure the daily update service has multiple chances to retry
transient failures.
Link: https://bugzilla.proxmox.com/show_bug.cgi?id=8006
[1] https://lore.proxmox.com/pbs-devel/20260423134607.105229-2-m.federanko@proxmox.com/
[2] https://letsencrypt.org/docs/integration-guide/#when-to-renew
Signed-off-by: David Riley <d.riley@proxmox.com>
---
src/PVE/Certificate.pm | 39 +++++++++++++++++++++++++++++++++++++--
1 file changed, 37 insertions(+), 2 deletions(-)
diff --git a/src/PVE/Certificate.pm b/src/PVE/Certificate.pm
index b8415e2..d2d0847 100644
--- a/src/PVE/Certificate.pm
+++ b/src/PVE/Certificate.pm
@@ -5,6 +5,7 @@ use warnings;
use Date::Parse;
use Encode qw(decode encode);
+use List::Util qw(max);
use MIME::Base64 qw(decode_base64 encode_base64);
use Net::SSLeay;
@@ -331,16 +332,28 @@ sub get_certificate_info {
# Obtain the expiration timestamp of a X.509 certificate as a UNIX epoch.
sub get_expiration_as_epoch {
my ($cert_path) = @_;
+ my (undef, $not_after) = get_validity_as_epoch($cert_path);
+ return $not_after;
+}
+
+# Obtain the notBefore and notAfter timestamps of a X.509 certificate as UNIX epochs.
+sub get_validity_as_epoch {
+ my ($cert_path) = @_;
my $cert = $read_certificate->($cert_path);
- my $not_after = eval { convert_asn1_to_epoch(Net::SSLeay::X509_get_notAfter($cert)) };
+ my ($not_before, $not_after) = eval {
+ (
+ convert_asn1_to_epoch(Net::SSLeay::X509_get_notBefore($cert)),
+ convert_asn1_to_epoch(Net::SSLeay::X509_get_notAfter($cert)),
+ );
+ };
my $err = $@;
Net::SSLeay::X509_free($cert);
die $err if $err;
- return $not_after;
+ return ($not_before, $not_after);
}
# Checks whether certificate expires before $timestamp (UNIX epoch)
@@ -354,6 +367,28 @@ sub check_expiry {
return ($not_after < $timestamp) ? 1 : 0;
}
+# Long-lived certs are renewed once 2/3 of their lifetime has elapsed; short-lived ones (under
+# ten days) already at 1/2, following Let's Encrypt's integration guide. A 3-day floor still
+# applies so the daily-update service has a couple of chances to retry transient failures.
+sub get_cert_renew_lead_time {
+ my ($cert_path) = @_;
+
+ my ($not_before, $not_after) = get_validity_as_epoch($cert_path);
+ my $lifetime = $not_after - $not_before;
+
+ if ($lifetime <= 0) {
+ warn "Unexpected validity period ($lifetime s) for '$cert_path', falling back to a 30 day"
+ . " renewal lead time\n";
+
+ return 30 * 24 * 60 * 60;
+ }
+
+ my $scale = $lifetime < (10 * 24 * 60 * 60) ? 2 : 3;
+ my $lead = int($lifetime / $scale);
+
+ return max($lead, 3 * 24 * 60 * 60); # 3 day minimum
+}
+
# Create a CSR and certificate key for a given order
# returns path to CSR file or path to CSR and key files
sub generate_csr {
--
2.47.3
^ permalink raw reply related [flat|nested] 7+ messages in thread* Re: [PATCH pve-common v2 1/2] certificate: add helpers for lifetime-based ACME renewal
2026-10-20 8:24 ` [PATCH pve-common v2 1/2] certificate: add helpers for lifetime-based ACME renewal David Riley
@ 2026-10-20 13:34 ` Michael Köppl
0 siblings, 0 replies; 7+ messages in thread
From: Michael Köppl @ 2026-10-20 13:34 UTC (permalink / raw)
To: David Riley, pve-devel
nit: I think this would also benefit from being split into 2 commits,
with the first adding get_validity_as_epoch and adapting
get_expiration_as_epoch to use it, and the second commit adding the
actual renewal helper. It's definitely not a blocker IMO, it just seems
a bit misleading to me right now as the commit messages says that only
a new helper is added, suggesting that existing behavior doesn't change,
but it does.
On Tue Oct 20, 2026 at 10:24 AM CEST, David Riley wrote:
> Add helpers to calculate a renewal lead time based on the actual
> lifetime of an X.509 certificate.
>
> Previously, certificate renewals relied on a hardcoded threshold.
> To prepare for dynamic thresholds (#8006), introduce calculations
> that scale the renewal lead time proportionally to the certificate's
> validity period.
>
> The scaling mirrors the logic already used in Proxmox Backup Server
> [1]: certificates living for less than 10 days are renewed once 1/2 of
> their lifetime has elapsed, while longer-lived certificates are
> renewed once 2/3 has elapsed [2]. A 3-day minimum floor is present to
> ensure the daily update service has multiple chances to retry
> transient failures.
>
[snip]
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH pve-manager v2 2/2] fix #8006: api: acme: lifetime-based certificate renewal threshold
2026-10-20 8:24 [PATCH common/manager v2 0/2] fix #8006: acme: lifetime-based certificate renewal threshold David Riley
2026-10-20 8:24 ` [PATCH pve-common v2 1/2] certificate: add helpers for lifetime-based ACME renewal David Riley
@ 2026-10-20 8:24 ` David Riley
2026-10-20 13:32 ` Michael Köppl
2026-10-20 13:34 ` [PATCH common/manager v2 0/2] fix #8006: " Michael Köppl
2026-10-21 9:58 ` superseded: " David Riley
3 siblings, 1 reply; 7+ messages in thread
From: David Riley @ 2026-10-20 8:24 UTC (permalink / raw)
To: pve-devel; +Cc: David Riley
Replace the hardcoded 30-day renewal limit with a threshold based on
the certificate's actual lifetime.
The previous hardcoded limit caused excessive daily renewals for
short-lived certificates (e.g., 31 days). This change ensures they
renew at sensible intervals, avoiding upstream rate limits.
Link: https://bugzilla.proxmox.com/show_bug.cgi?id=8006
Signed-off-by: David Riley <d.riley@proxmox.com>
---
PVE/API2/ACME.pm | 10 ++++++----
bin/pveupdate | 4 +++-
2 files changed, 9 insertions(+), 5 deletions(-)
diff --git a/PVE/API2/ACME.pm b/PVE/API2/ACME.pm
index a948a72a..21ed3878 100644
--- a/PVE/API2/ACME.pm
+++ b/PVE/API2/ACME.pm
@@ -243,7 +243,8 @@ __PACKAGE__->register_method({
node => get_standard_option('pve-node'),
force => {
type => 'boolean',
- description => 'Force renewal even if expiry is more than 30 days away.',
+ description =>
+ 'Force renewal even if the certificate is not yet due for renewal.',
optional => 1,
default => 0,
},
@@ -261,12 +262,13 @@ __PACKAGE__->register_method({
raise("No current (custom) certificate found, please order a new certificate!\n")
if !-e "${cert_prefix}.pem";
+ my $lead_time = PVE::Certificate::get_cert_renew_lead_time("${cert_prefix}.pem");
+
my $expires_soon =
- PVE::Certificate::check_expiry("${cert_prefix}.pem", time() + 30 * 24 * 60 * 60);
+ PVE::Certificate::check_expiry("${cert_prefix}.pem", time() + $lead_time);
raise_param_exc(
{
- 'force' =>
- "Certificate does not expire within the next 30 days, and 'force' is not set.",
+ 'force' => "Certificate is not yet due for renewal, and 'force' is not set.",
},
) if !$expires_soon && !$param->{force};
diff --git a/bin/pveupdate b/bin/pveupdate
index b1960c35..a014d1d3 100755
--- a/bin/pveupdate
+++ b/bin/pveupdate
@@ -83,7 +83,9 @@ eval {
if ($acme_node_config && $acme_node_config->{domains}) {
my $cert = PVE::CertHelpers::cert_path_prefix($nodename) . ".pem";
if (-e $cert) {
- if (PVE::Certificate::check_expiry($cert, time() + 30 * 24 * 60 * 60)) {
+ my $lead_time = PVE::Certificate::get_cert_renew_lead_time($cert);
+
+ if (PVE::Certificate::check_expiry($cert, time() + $lead_time)) {
PVE::API2::ACME->renew_certificate({ node => $nodename });
} else {
syslog(
--
2.47.3
^ permalink raw reply related [flat|nested] 7+ messages in thread* Re: [PATCH pve-manager v2 2/2] fix #8006: api: acme: lifetime-based certificate renewal threshold
2026-10-20 8:24 ` [PATCH pve-manager v2 2/2] fix #8006: api: acme: lifetime-based certificate renewal threshold David Riley
@ 2026-10-20 13:32 ` Michael Köppl
0 siblings, 0 replies; 7+ messages in thread
From: Michael Köppl @ 2026-10-20 13:32 UTC (permalink / raw)
To: David Riley, pve-devel
nit: IMO the pveupdate change would make more sense as a separate
commit. Or at least the commit subject should be adapted, as it
currently only mentions "api: acme:", which is a bit misleading.
On Tue Oct 20, 2026 at 10:24 AM CEST, David Riley wrote:
> Replace the hardcoded 30-day renewal limit with a threshold based on
> the certificate's actual lifetime.
>
> The previous hardcoded limit caused excessive daily renewals for
> short-lived certificates (e.g., 31 days). This change ensures they
> renew at sensible intervals, avoiding upstream rate limits.
>
> Link: https://bugzilla.proxmox.com/show_bug.cgi?id=8006
>
> Signed-off-by: David Riley <d.riley@proxmox.com>
> ---
> PVE/API2/ACME.pm | 10 ++++++----
> bin/pveupdate | 4 +++-
> 2 files changed, 9 insertions(+), 5 deletions(-)
[snip]
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH common/manager v2 0/2] fix #8006: acme: lifetime-based certificate renewal threshold
2026-10-20 8:24 [PATCH common/manager v2 0/2] fix #8006: acme: lifetime-based certificate renewal threshold David Riley
2026-10-20 8:24 ` [PATCH pve-common v2 1/2] certificate: add helpers for lifetime-based ACME renewal David Riley
2026-10-20 8:24 ` [PATCH pve-manager v2 2/2] fix #8006: api: acme: lifetime-based certificate renewal threshold David Riley
@ 2026-10-20 13:34 ` Michael Köppl
2026-10-21 9:58 ` superseded: " David Riley
3 siblings, 0 replies; 7+ messages in thread
From: Michael Köppl @ 2026-10-20 13:34 UTC (permalink / raw)
To: David Riley, pve-devel
Tested this version as well, running through the following scenarios:
- Checked that with only 72h remaining, certs are always considered due
for renewal and are renewed by pveupdate. Generated a couple of
certs for this at 24h, 72h, 96h. For 96h the scaling should use 2, so
48h, but the floor raises this to 72h. So I forwarded time by 23h,
such that the certificate was not considered due for renewal yet and
thus refused. At 24h(+1s) after issuance and every point after,
renewal worked. Works as expected!
- Also checked that the scaling works by using a 216h and a 240h
certificate, with the switch to 1/3 scaling being applied to 240h
and longer-lived certs.
- Just to be sure (and because I wanted to try), I also used pebble [0]
and configured it to use 96h certs and ran renewals again at the 3-day
floor. Also had it issue longer-lived certificates and tried to make
it miss/fail renewal, but couldn't.
I also had a look at the code and left some minor comments regarding
the structure of the series. The implementation itself looks good to me
now.
Consider this:
Reviewed-by: Michael Köppl <m.koeppl@proxmox.com>
Tested-by: Michael Köppl <m.koeppl@proxmox.com>
[0] https://github.com/letsencrypt/pebble
On Tue Oct 20, 2026 at 10:24 AM CEST, David Riley wrote:
> Thanks Michael K. for taking a look.
>
> Differences from v1:
> * Refactor: Reduce certificate reads by combining the read of
> notBefore and notAfter.
> * Add: Warning users of certificates with lifetime <= 0
> * Add: minimum lead time of 3 days
> * Update 'force' parameter description in API schema
[snip]
^ permalink raw reply [flat|nested] 7+ messages in thread
* superseded: [PATCH common/manager v2 0/2] fix #8006: acme: lifetime-based certificate renewal threshold
2026-10-20 8:24 [PATCH common/manager v2 0/2] fix #8006: acme: lifetime-based certificate renewal threshold David Riley
` (2 preceding siblings ...)
2026-10-20 13:34 ` [PATCH common/manager v2 0/2] fix #8006: " Michael Köppl
@ 2026-10-21 9:58 ` David Riley
3 siblings, 0 replies; 7+ messages in thread
From: David Riley @ 2026-10-21 9:58 UTC (permalink / raw)
To: pve-devel
superseded by:
https://lore.proxmox.com/pve-devel/20261021095229.50842-1-d.riley@proxmox.com/
^ permalink raw reply [flat|nested] 7+ messages in thread