From: Manuel Federanko <m.federanko@proxmox.com>
To: pbs-devel@lists.proxmox.com, pdm-devel@lists.proxmox.com
Subject: [PATCH proxmox-backup 6/7] acme: fix #6372 implement ARI renewal information fetching.
Date: Thu, 25 Jun 2026 16:13:36 +0200 [thread overview]
Message-ID: <20260625141337.181684-7-m.federanko@proxmox.com> (raw)
In-Reply-To: <20260625141337.181684-1-m.federanko@proxmox.com>
Try to fetch ARI renewal information and renew based on that, if it is
not available fall back to normal lifetime based renewal logic.
The ARI check needs to talk to the server, move all checks into the
worker process and unconditionally call the worker process from all
entry points.
Add a method that returns the ARI ID from the CertInfo struct, which is
easier than trying to pass along other information or switching to
proxmox-acme-api's CertificateInfo struct.
Fixes: https://bugzilla.proxmox.com/show_bug.cgi?id=6372
Signed-off-by: Manuel Federanko <m.federanko@proxmox.com>
---
src/api2/node/certificates.rs | 106 +++++++++++++++++++------
src/bin/proxmox-daily-update.rs | 6 --
src/bin/proxmox_backup_manager/acme.rs | 8 --
3 files changed, 81 insertions(+), 39 deletions(-)
diff --git a/src/api2/node/certificates.rs b/src/api2/node/certificates.rs
index 3df05b020..a6a4b67cc 100644
--- a/src/api2/node/certificates.rs
+++ b/src/api2/node/certificates.rs
@@ -1,11 +1,11 @@
-use anyhow::{Error, bail, format_err};
+use anyhow::{Error, format_err};
use openssl::pkey::PKey;
use openssl::x509::X509;
use serde::{Deserialize, Serialize};
use tracing::info;
use pbs_api_types::{NODE_SCHEMA, PRIV_SYS_MODIFY};
-use proxmox_acme_api::AcmeDomain;
+use proxmox_acme_api::{AcmeConfig, AcmeDomain};
use proxmox_rest_server::WorkerTask;
use proxmox_router::SubdirMap;
use proxmox_router::list_subdirs_api_method;
@@ -307,13 +307,6 @@ pub fn new_acme_cert(force: bool, rpcenv: &mut dyn RpcEnvironment) -> Result<Str
/// Renew the current ACME certificate if it is within its renewal lead time (or always if the
/// `force` parameter is set).
pub fn renew_acme_cert(force: bool, rpcenv: &mut dyn RpcEnvironment) -> Result<String, Error> {
- let (expires_soon, lead_days) = check_renewal_needed()?;
- if !expires_soon && !force {
- bail!(
- "Certificate does not expire within the next {lead_days} days and 'force' is not set."
- )
- }
-
spawn_certificate_worker("acme-renew-cert", force, rpcenv)
}
@@ -341,17 +334,67 @@ fn cert_renew_lead_time(cert: &cert::CertInfo) -> i64 {
}
}
+/// ARI renewal time if available
+///
+/// Query the ARI endpoint for a suggested renewal window, draw a uniform random time in this window
+/// Return None if ARI does not apply.
+async fn cert_renew_lead_time_ari(
+ acme_config: &AcmeConfig,
+ cert_info: &cert::CertInfo,
+) -> Result<Option<i64>, Error> {
+ let now = proxmox_time::epoch_i64();
+ if cert_info.is_expired_after_epoch(now)? {
+ return Ok(Some(0));
+ }
+ let ari_id = &match cert_info.ari_id() {
+ Some(x) => x,
+ None => return Ok(None),
+ };
+ let window = match proxmox_acme_api::get_renewal_info(acme_config, &ari_id).await? {
+ Some(x) => x,
+ None => return Ok(None),
+ };
+ if let Some(reason) = window.data.explanation_url {
+ info!(
+ "Obtained renewal window, for information on this chosen window please visit {reason}"
+ );
+ }
+ let window_start = proxmox_time::parse_rfc3339(&window.data.suggested_window.start)?;
+ let window_end = proxmox_time::parse_rfc3339(&window.data.suggested_window.end)?;
+ let rand = proxmox_sys::linux::random_data(8)?
+ .into_iter()
+ .enumerate()
+ .fold(0, |acc, (index, x)| acc + ((x as u64) << (index * 8))) as f64
+ / (u64::MAX as f64);
+ let renew = window_start + (((window_end - window_start) as f64) * rand) as i64;
+ // need max since the randomness could result in negative values
+ Ok(Some(std::cmp::max(0, renew - now)))
+}
+
/// Check whether the current certificate expires within its renewal lead time.
///
/// Returns `(expires_soon, lead_time_in_days)`; the lead time is returned so callers can produce
/// consistent user-facing messages without re-reading and re-parsing the certificate.
-pub fn check_renewal_needed() -> Result<(bool, i64), Error> {
- let cert = pem_to_cert_info(get_certificate_pem()?.as_bytes())?;
- let lead = cert_renew_lead_time(&cert);
- let expires_soon = cert
- .is_expired_after_epoch(proxmox_time::epoch_i64() + lead)
- .map_err(|err| format_err!("Failed to check certificate expiration date: {}", err))?;
- Ok((expires_soon, lead / SECONDS_PER_DAY))
+async fn check_renewal_needed(
+ acme_config: &AcmeConfig,
+ cert_info: &cert::CertInfo,
+) -> Result<bool, Error> {
+ let lead_ari = cert_renew_lead_time_ari(acme_config, cert_info).await?;
+ if let Some(lead_ari) = lead_ari {
+ // rfc9773 section 4.2 tells us to renew if the chosen renewal time would be before the next check
+ let expires_soon = lead_ari < SECONDS_PER_DAY;
+ let ts = proxmox_time::TimeSpan::from(std::time::Duration::new(lead_ari as u64, 0));
+ info!("Certificate is scheduled for renewal in {ts:.0} by ARI");
+ Ok(expires_soon)
+ } else {
+ let lead = cert_renew_lead_time(&cert_info);
+ let expires_soon = cert_info
+ .is_expired_after_epoch(proxmox_time::epoch_i64() + lead)
+ .map_err(|err| format_err!("Failed to check certificate expiration date: {}", err))?;
+ let ts = proxmox_time::TimeSpan::from(std::time::Duration::new(lead as u64, 0));
+ info!("Certificate renewal lead time is {ts:.0}");
+ Ok(expires_soon)
+ }
}
fn spawn_certificate_worker(
@@ -359,10 +402,6 @@ fn spawn_certificate_worker(
force: bool,
rpcenv: &mut dyn RpcEnvironment,
) -> Result<String, Error> {
- // We only have 1 certificate path in PBS which makes figuring out whether or not it is a
- // custom one too hard... We keep the parameter because the widget-toolkit may be using it...
- let _ = force;
-
let (node_config, _digest) = pbs_config::node::config()?;
let auth_id = rpcenv.get_auth_id().unwrap();
@@ -384,11 +423,28 @@ fn spawn_certificate_worker(
WorkerTask::spawn(name, None, auth_id, true, move |worker| async move {
let work = || async {
- if let Some(cert) =
- proxmox_acme_api::order_certificate(worker, &acme_config, &domains).await?
- {
- crate::config::set_proxy_certificate(&cert.certificate, &cert.private_key_pem)?;
- crate::server::reload_proxy_certificate().await?;
+ let cert_info = pem_to_cert_info(get_certificate_pem()?.as_bytes())?;
+ let expires_soon = if !force {
+ let expires_soon = check_renewal_needed(&acme_config, &cert_info).await?;
+ if !expires_soon {
+ info!("Certificate does not expire soon and 'force' was not set, not renewing");
+ }
+ expires_soon
+ } else {
+ false
+ };
+ if force || expires_soon {
+ if let Some(cert) = proxmox_acme_api::order_certificate(
+ worker,
+ &acme_config,
+ &domains,
+ cert_info.ari_id().as_deref(),
+ )
+ .await?
+ {
+ crate::config::set_proxy_certificate(&cert.certificate, &cert.private_key_pem)?;
+ crate::server::reload_proxy_certificate().await?;
+ }
}
Ok(())
diff --git a/src/bin/proxmox-daily-update.rs b/src/bin/proxmox-daily-update.rs
index 42ce62d16..eeadf9d13 100644
--- a/src/bin/proxmox-daily-update.rs
+++ b/src/bin/proxmox-daily-update.rs
@@ -74,12 +74,6 @@ async fn check_acme_certificates(rpcenv: &mut dyn RpcEnvironment) -> Result<(),
return Ok(());
}
- let (expires_soon, lead_days) = api2::node::certificates::check_renewal_needed()?;
- if !expires_soon {
- log::info!("Certificate does not expire within the next {lead_days} days, not renewing.");
- return Ok(());
- }
-
let info = &api2::node::certificates::API_METHOD_RENEW_ACME_CERT;
let result = match info.handler {
ApiHandler::Sync(handler) => (handler)(json!({}), info, rpcenv)?,
diff --git a/src/bin/proxmox_backup_manager/acme.rs b/src/bin/proxmox_backup_manager/acme.rs
index ed9e5868c..ea14cfe2b 100644
--- a/src/bin/proxmox_backup_manager/acme.rs
+++ b/src/bin/proxmox_backup_manager/acme.rs
@@ -413,14 +413,6 @@ pub fn plugin_cli() -> CommandLineInterface {
)]
/// Order a new ACME certificate.
async fn order_acme_cert(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> {
- if !param["force"].as_bool().unwrap_or(false) {
- let (expires_soon, lead_days) = api2::node::certificates::check_renewal_needed()?;
- if !expires_soon {
- println!("Certificate does not expire within the next {lead_days} days, not renewing.");
- return Ok(());
- }
- }
-
let info = &api2::node::certificates::API_METHOD_RENEW_ACME_CERT;
let result = match info.handler {
ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
--
2.47.3
next prev parent reply other threads:[~2026-06-25 14:13 UTC|newest]
Thread overview: 8+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-25 14:13 [PATCH proxmox{,-backup,-datacenter-manager} 0/7] acme: fix #6372 implement basic ARI support Manuel Federanko
2026-06-25 14:13 ` [PATCH proxmox 1/7] acme: client: add methods to fetch renewal information Manuel Federanko
2026-06-25 14:13 ` [PATCH proxmox 2/7] acme: add retry-after header to " Manuel Federanko
2026-06-25 14:13 ` [PATCH proxmox 3/7] acme: allow specifying the certificate that is replaced by an order Manuel Federanko
2026-06-25 14:13 ` [PATCH proxmox 4/7] acme: cert: add dedicated ari_id field to the certificate info Manuel Federanko
2026-06-25 14:13 ` [PATCH proxmox-backup 5/7] acme: add ari_id to cert info Manuel Federanko
2026-06-25 14:13 ` Manuel Federanko [this message]
2026-06-25 14:13 ` [PATCH proxmox-datacenter-manager 7/7] acme: fix #6372 use ARI for renewal if available Manuel Federanko
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260625141337.181684-7-m.federanko@proxmox.com \
--to=m.federanko@proxmox.com \
--cc=pbs-devel@lists.proxmox.com \
--cc=pdm-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.