* [PATCH pve-common v3 1/4] certificate: add helper to extract validity period
2026-10-21 9:52 [PATCH common/manager v3 0/4] fix #8006: acme: lifetime-based certificate renewal threshold David Riley
@ 2026-10-21 9:52 ` David Riley
2026-10-21 9:52 ` [PATCH pve-common v3 2/4] certificate: add helper for lifetime-based ACME renewal lead time David Riley
` (2 subsequent siblings)
3 siblings, 0 replies; 5+ messages in thread
From: David Riley @ 2026-10-21 9:52 UTC (permalink / raw)
To: pve-devel; +Cc: David Riley
Previously, only the expiration timestamp was needed, so only notAfter
was parsed from the certificate.
To prepare for lifetime calculations, introduce a helper that extracts
both the notBefore and notAfter timestamps [0] in a single certificate
read. Adapt the existing expiration helper to consume it without any
change in behavior for existing callers.
[0] https://www.rfc-editor.org/info/rfc5280/#section-4.1
Signed-off-by: David Riley <d.riley@proxmox.com>
Reviewed-by: Michael Köppl <m.koeppl@proxmox.com>
Tested-by: Michael Köppl <m.koeppl@proxmox.com>
---
src/PVE/Certificate.pm | 16 ++++++++++++++--
1 file changed, 14 insertions(+), 2 deletions(-)
diff --git a/src/PVE/Certificate.pm b/src/PVE/Certificate.pm
index b8415e2..8406949 100644
--- a/src/PVE/Certificate.pm
+++ b/src/PVE/Certificate.pm
@@ -331,16 +331,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)
--
2.47.3
^ permalink raw reply related [flat|nested] 5+ messages in thread* [PATCH pve-common v3 2/4] certificate: add helper for lifetime-based ACME renewal lead time
2026-10-21 9:52 [PATCH common/manager v3 0/4] fix #8006: acme: lifetime-based certificate renewal threshold David Riley
2026-10-21 9:52 ` [PATCH pve-common v3 1/4] certificate: add helper to extract validity period David Riley
@ 2026-10-21 9:52 ` David Riley
2026-10-21 9:52 ` [PATCH pve-manager v3 3/4] fix #8006: api: acme: use lifetime-based certificate " David Riley
2026-10-21 9:52 ` [PATCH pve-manager v3 4/4] fix #8006: pveupdate: use lifetime-based " David Riley
3 siblings, 0 replies; 5+ messages in thread
From: David Riley @ 2026-10-21 9:52 UTC (permalink / raw)
To: pve-devel; +Cc: David Riley
Add helper to calculate renewal lead time dynamically based on the
certificate's actual validity period.
The scaling mirrors the logic already used in Proxmox Backup Server
[0]: 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 [1]. A 3-day minimum floor is present to
ensure the daily update service has multiple chances to retry
transient failures.
[0] https://lore.proxmox.com/pbs-devel/20260423134607.105229-2-m.federanko@proxmox.com/
[1] https://letsencrypt.org/docs/integration-guide/#when-to-renew
Signed-off-by: David Riley <d.riley@proxmox.com>
Reviewed-by: Michael Köppl <m.koeppl@proxmox.com>
Tested-by: Michael Köppl <m.koeppl@proxmox.com>
---
src/PVE/Certificate.pm | 23 +++++++++++++++++++++++
1 file changed, 23 insertions(+)
diff --git a/src/PVE/Certificate.pm b/src/PVE/Certificate.pm
index 8406949..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;
@@ -366,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] 5+ messages in thread* [PATCH pve-manager v3 3/4] fix #8006: api: acme: use lifetime-based certificate renewal lead time
2026-10-21 9:52 [PATCH common/manager v3 0/4] fix #8006: acme: lifetime-based certificate renewal threshold David Riley
2026-10-21 9:52 ` [PATCH pve-common v3 1/4] certificate: add helper to extract validity period David Riley
2026-10-21 9:52 ` [PATCH pve-common v3 2/4] certificate: add helper for lifetime-based ACME renewal lead time David Riley
@ 2026-10-21 9:52 ` David Riley
2026-10-21 9:52 ` [PATCH pve-manager v3 4/4] fix #8006: pveupdate: use lifetime-based " David Riley
3 siblings, 0 replies; 5+ messages in thread
From: David Riley @ 2026-10-21 9:52 UTC (permalink / raw)
To: pve-devel; +Cc: David Riley
Replace the hardcoded 30-day renewal check with a dynamic threshold
based on the certificate's actual lifetime.
Signed-off-by: David Riley <d.riley@proxmox.com>
Reviewed-by: Michael Köppl <m.koeppl@proxmox.com>
Tested-by: Michael Köppl <m.koeppl@proxmox.com>
---
PVE/API2/ACME.pm | 10 ++++++----
1 file changed, 6 insertions(+), 4 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};
--
2.47.3
^ permalink raw reply related [flat|nested] 5+ messages in thread* [PATCH pve-manager v3 4/4] fix #8006: pveupdate: use lifetime-based renewal lead time
2026-10-21 9:52 [PATCH common/manager v3 0/4] fix #8006: acme: lifetime-based certificate renewal threshold David Riley
` (2 preceding siblings ...)
2026-10-21 9:52 ` [PATCH pve-manager v3 3/4] fix #8006: api: acme: use lifetime-based certificate " David Riley
@ 2026-10-21 9:52 ` David Riley
3 siblings, 0 replies; 5+ messages in thread
From: David Riley @ 2026-10-21 9:52 UTC (permalink / raw)
To: pve-devel; +Cc: David Riley
Use a dynamically calculated renewal lead time based on the
certificate's lifetime instead of the previously hardcoded 30-day
lead time.
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.
Signed-off-by: David Riley <d.riley@proxmox.com>
Reviewed-by: Michael Köppl <m.koeppl@proxmox.com>
Tested-by: Michael Köppl <m.koeppl@proxmox.com>
---
bin/pveupdate | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
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] 5+ messages in thread