* [PATCH common/manager 0/2] fix #8006: acme: lifetime-based certificate renewal threshold
@ 2026-10-06 12:31 David Riley
2026-10-06 12:31 ` [PATCH pve-common 1/2] certificate: add helpers for lifetime-based ACME renewal David Riley
2026-10-06 12:31 ` [PATCH pve-manager 2/2] fix #8006: api: acme: lifetime-based certificate renewal threshold David Riley
0 siblings, 2 replies; 3+ messages in thread
From: David Riley @ 2026-10-06 12:31 UTC (permalink / raw)
To: pve-devel
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 (1):
certificate: add helpers for lifetime-based ACME renewal
src/PVE/Certificate.pm | 31 +++++++++++++++++++++++++++++++
1 file changed, 31 insertions(+)
pve-manager:
David Riley (1):
fix #8006: api: acme: lifetime-based certificate renewal threshold
PVE/API2/ACME.pm | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
Summary over all repositories:
2 files changed, 36 insertions(+), 2 deletions(-)
--
Generated by murpp 0.11.0
^ permalink raw reply [flat|nested] 3+ messages in thread
* [PATCH pve-common 1/2] certificate: add helpers for lifetime-based ACME renewal
2026-10-06 12:31 [PATCH common/manager 0/2] fix #8006: acme: lifetime-based certificate renewal threshold David Riley
@ 2026-10-06 12:31 ` David Riley
2026-10-06 12:31 ` [PATCH pve-manager 2/2] fix #8006: api: acme: lifetime-based certificate renewal threshold David Riley
1 sibling, 0 replies; 3+ messages in thread
From: David Riley @ 2026-10-06 12:31 UTC (permalink / raw)
To: pve-devel
Add helpers to calculate a renewal lead time based on the actual
lifetime of an X.509 certificate (the difference between its notBefore
and notAfter dates [0]).
This commit is in preparation for fixing #8006. The scaling mirrors
the logic already used in Proxmox Backup Server [1]: certificates
living for less than 10 days are renewed at half their lifetime,
while longer-lived certificates use a third of their lifetime.
Link: https://bugzilla.proxmox.com/show_bug.cgi?id=8006
[0] https://www.cs.auckland.ac.nz/~pgut001/pubs/x509guide.txt
[1] https://lore.proxmox.com/pbs-devel/20260423134607.105229-2-m.federanko@proxmox.com/
Signed-off-by: David Riley <d.riley@proxmox.com>
---
src/PVE/Certificate.pm | 31 +++++++++++++++++++++++++++++++
1 file changed, 31 insertions(+)
diff --git a/src/PVE/Certificate.pm b/src/PVE/Certificate.pm
index b8415e2..45c9bc7 100644
--- a/src/PVE/Certificate.pm
+++ b/src/PVE/Certificate.pm
@@ -328,6 +328,21 @@ sub get_certificate_info {
return $info;
}
+# Obtain the notBefore timestamp of a X.509 certificate as a UNIX epoch.
+sub get_not_before_as_epoch {
+ my ($cert_path) = @_;
+
+ my $cert = $read_certificate->($cert_path);
+ my $not_before = eval { convert_asn1_to_epoch(Net::SSLeay::X509_get_notBefore($cert)) };
+ my $err = $@;
+
+ Net::SSLeay::X509_free($cert);
+
+ die $err if $err;
+
+ return $not_before;
+}
+
# Obtain the expiration timestamp of a X.509 certificate as a UNIX epoch.
sub get_expiration_as_epoch {
my ($cert_path) = @_;
@@ -354,6 +369,22 @@ sub check_expiry {
return ($not_after < $timestamp) ? 1 : 0;
}
+sub get_cert_renew_lead_time {
+ my ($cert_path) = @_;
+
+ my $default_lead = 30 * 24 * 60 * 60;
+
+ my $not_before = get_not_before_as_epoch($cert_path);
+ my $not_after = get_expiration_as_epoch($cert_path);
+
+ my $lifetime = $not_after - $not_before;
+ return $default_lead if $lifetime <= 0;
+
+ my $scale = $lifetime < (10 * 24 * 60 * 60) ? 2 : 3;
+
+ return int($lifetime / $scale);
+}
+
# 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] 3+ messages in thread* [PATCH pve-manager 2/2] fix #8006: api: acme: lifetime-based certificate renewal threshold
2026-10-06 12:31 [PATCH common/manager 0/2] fix #8006: acme: lifetime-based certificate renewal threshold David Riley
2026-10-06 12:31 ` [PATCH pve-common 1/2] certificate: add helpers for lifetime-based ACME renewal David Riley
@ 2026-10-06 12:31 ` David Riley
1 sibling, 0 replies; 3+ messages in thread
From: David Riley @ 2026-10-06 12:31 UTC (permalink / raw)
To: pve-devel
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 | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/PVE/API2/ACME.pm b/PVE/API2/ACME.pm
index a948a72a..4f2590f0 100644
--- a/PVE/API2/ACME.pm
+++ b/PVE/API2/ACME.pm
@@ -261,12 +261,15 @@ __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 $lead_days = int($lead_time / (24 * 60 * 60));
+
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.",
+ "Certificate does not expire within the next $lead_days days, and 'force' is not set.",
},
) if !$expires_soon && !$param->{force};
--
2.47.3
^ permalink raw reply related [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-10-06 12:31 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-06 12:31 [PATCH common/manager 0/2] fix #8006: acme: lifetime-based certificate renewal threshold David Riley
2026-10-06 12:31 ` [PATCH pve-common 1/2] certificate: add helpers for lifetime-based ACME renewal David Riley
2026-10-06 12:31 ` [PATCH pve-manager 2/2] fix #8006: api: acme: lifetime-based certificate renewal threshold David Riley
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox