public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [PATCH common/manager v3 0/4] fix #8006: acme: lifetime-based certificate renewal threshold
@ 2026-10-21  9:52 David Riley
  2026-10-21  9:52 ` [PATCH pve-common v3 1/4] certificate: add helper to extract validity period David Riley
                   ` (3 more replies)
  0 siblings, 4 replies; 5+ messages in thread
From: David Riley @ 2026-10-21  9:52 UTC (permalink / raw)
  To: pve-devel; +Cc: David Riley

Thanks Michael K. for taking a look at v2.

Differences from v2: 
* Split both pve-common and pve-manager patches into two commits each
  (4 commits total) as suggested by Michael K.

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
* Add: dynamic lead time calculation to bin/pveupdate

Note on short-lived certificates:
Because pveupdate runs daily and there is a 3-day minimum lead time,
short-lived certificates (e.g., 24h, 48h) will get renewed every day
and therefore might be limited by upstream rate limiting.
Supporting such short-lived certificates safely will require both
implementing ARI [3] and adjusting the renewal check frequency.

Dependencies:
pve-manager requires a versioned dependency bump on libpve-common-perl
for the new helper.

---
Original Cover Letter:

This patch series addresses an issue where short-lived ACME
certificates (e.g., 31-day validity) trigger daily renewal attempts
due to PVE's hardcoded 30-day threshold.

Because PVE currently relies on non-ARI renewal logic, these daily
attempts quickly exhaust upstream rate limits. With Let's Encrypt,
for example, this would hit their duplicate certificate limit
(5 renewals per exact set of identifiers per 7 days) [0], resulting
in blocked issuances.

To prevent hitting these upstream limits, the static threshold is 
replaced with a dynamic one based on the certificate's actual
lifetime (notAfter - notBefore) [1]. The scaling mirrors the logic
already present in Proxmox Backup Server [2].

This is intended as a stop-gap measure to gracefully handle the
shift toward shorter-lived certificates until the full ACME Renewal
Information (ARI) [3] flow is implemented.

Link: https://bugzilla.proxmox.com/show_bug.cgi?id=8006

[0] https://letsencrypt.org/docs/rate-limits/#new-certificates-per-exact-set-of-identifiers
[1] https://www.cs.auckland.ac.nz/~pgut001/pubs/x509guide.txt
[2] https://lore.proxmox.com/pbs-devel/20260423134607.105229-2-m.federanko@proxmox.com/
[3] https://www.rfc-editor.org/rfc/rfc9773.html


pve-common:

David Riley (2):
  certificate: add helper to extract validity period
  certificate: add helper for lifetime-based ACME renewal lead time

 src/PVE/Certificate.pm | 39 +++++++++++++++++++++++++++++++++++++--
 1 file changed, 37 insertions(+), 2 deletions(-)


pve-manager:

David Riley (2):
  fix #8006: api: acme: use lifetime-based certificate renewal lead time
  fix #8006: pveupdate: use lifetime-based renewal lead time

 PVE/API2/ACME.pm | 10 ++++++----
 bin/pveupdate    |  4 +++-
 2 files changed, 9 insertions(+), 5 deletions(-)


Summary over all repositories:
  3 files changed, 46 insertions(+), 7 deletions(-)

-- 
Generated by murpp 0.11.0




^ permalink raw reply	[flat|nested] 5+ messages in thread

* [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

end of thread, other threads:[~2026-10-21  9:53 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 ` [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

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal