From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 8ACC11FF142 for ; Tue, 21 Apr 2026 16:47:03 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 6485127CE2; Tue, 21 Apr 2026 16:47:03 +0200 (CEST) From: Manuel Federanko To: pbs-devel@lists.proxmox.com Subject: [PATCH proxmox-backup] acme: partially fix #6372: scale certificate renewal checks by lifetime Date: Tue, 21 Apr 2026 16:46:45 +0200 Message-ID: <20260421144645.275884-1-m.federanko@proxmox.com> X-Mailer: git-send-email 2.47.3 MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.177 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy HEADER_FROM_DIFFERENT_DOMAINS 0.25 From and EnvelopeFrom 2nd level mail domains are different KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment KAM_LAZY_DOMAIN_SECURITY 1 Sending domain does not have any anti-forgery methods RDNS_NONE 0.793 Delivered to internal network by a host with no rDNS SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_NONE 0.001 SPF: sender does not publish an SPF Record Message-ID-Hash: PLLO3XIXVB3U4AMIOEUGUZWOWNISSXNW X-Message-ID-Hash: PLLO3XIXVB3U4AMIOEUGUZWOWNISSXNW X-MailFrom: mfederanko@dev.localdomain X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox Backup Server development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Start renewing a certificate once 2/3 of its total lifetime have passed, instead of the hardcoded 30 days. This stays consistent with many certificates, which are valid for 90 days. The update service runs daily, impose a 3 day minimum remaining lifetime to still be able to handle transient failures for certificate renewals. Signed-off-by: Manuel Federanko Fixes: https://bugzilla.proxmox.com/show_bug.cgi?id=6372 --- src/api2/node/certificates.rs | 21 +++++++++++++++++++-- src/bin/proxmox-daily-update.rs | 3 ++- src/bin/proxmox_backup_manager/acme.rs | 3 ++- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/api2/node/certificates.rs b/src/api2/node/certificates.rs index a69f6511..6e7b3326 100644 --- a/src/api2/node/certificates.rs +++ b/src/api2/node/certificates.rs @@ -306,16 +306,33 @@ pub fn new_acme_cert(force: bool, rpcenv: &mut dyn RpcEnvironment) -> Result Result { if !cert_expires_soon()? && !force { - bail!("Certificate does not expire within the next 30 days and 'force' is not set.") + let lead = cert_renew_lead_time()? / (24 * 60 * 60); + bail!("Certificate does not expire within the next {lead} days and 'force' is not set.") } spawn_certificate_worker("acme-renew-cert", force, rpcenv) } +/// When to start checking for new certs. +pub fn cert_renew_lead_time() -> Result { + let cert = pem_to_cert_info(get_certificate_pem()?.as_bytes())?; + if let (Some(notafter), Some(notbefore)) = + (cert.not_after_unix().ok(), cert.not_before_unix().ok()) + { + // gets usually checked every day by the daily-update service, + // start checking at least 3 days before expiry + let lifetime = notafter - notbefore; + let lead = std::cmp::max(lifetime / 3, 3 * 24 * 60 * 60); + Ok(lead) + } else { + Ok(30 * 24 * 60 * 60) + } +} + /// Check whether the current certificate expires within the next 30 days. pub fn cert_expires_soon() -> Result { let cert = pem_to_cert_info(get_certificate_pem()?.as_bytes())?; - cert.is_expired_after_epoch(proxmox_time::epoch_i64() + 30 * 24 * 60 * 60) + cert.is_expired_after_epoch(proxmox_time::epoch_i64() + cert_renew_lead_time()?) .map_err(|err| format_err!("Failed to check certificate expiration date: {}", err)) } diff --git a/src/bin/proxmox-daily-update.rs b/src/bin/proxmox-daily-update.rs index c4d68e30..e5e96eb9 100644 --- a/src/bin/proxmox-daily-update.rs +++ b/src/bin/proxmox-daily-update.rs @@ -75,7 +75,8 @@ async fn check_acme_certificates(rpcenv: &mut dyn RpcEnvironment) -> Result<(), } if !api2::node::certificates::cert_expires_soon()? { - log::info!("Certificate does not expire within the next 30 days, not renewing."); + let lead = api2::node::certificates::cert_renew_lead_time()? / (24 * 60 * 60); + log::info!("Certificate does not expire within the next {lead} days, not renewing."); return Ok(()); } diff --git a/src/bin/proxmox_backup_manager/acme.rs b/src/bin/proxmox_backup_manager/acme.rs index 57431225..d1a2323f 100644 --- a/src/bin/proxmox_backup_manager/acme.rs +++ b/src/bin/proxmox_backup_manager/acme.rs @@ -415,7 +415,8 @@ pub fn plugin_cli() -> CommandLineInterface { async fn order_acme_cert(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> { if !param["force"].as_bool().unwrap_or(false) && !api2::node::certificates::cert_expires_soon()? { - println!("Certificate does not expire within the next 30 days, not renewing."); + let lead = api2::node::certificates::cert_renew_lead_time()? / (24 * 60 * 60); + println!("Certificate does not expire within the next {lead} days, not renewing."); return Ok(()); } -- 2.47.3