all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: "Shannon Sterz" <s.sterz@proxmox.com>
To: "Manuel Federanko" <m.federanko@proxmox.com>,
	<pbs-devel@lists.proxmox.com>
Subject: Re: [PATCH proxmox-backup] acme: partially fix #6372: scale certificate renewal checks by lifetime
Date: Wed, 22 Apr 2026 11:10:03 +0200	[thread overview]
Message-ID: <DHZKEJBTPLZY.TNSL8CWILFKT@proxmox.com> (raw)
In-Reply-To: <20260421144645.275884-1-m.federanko@proxmox.com>

On Tue Apr 21, 2026 at 4:46 PM CEST, Manuel Federanko wrote:
> 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 <m.federanko@proxmox.com>
> 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<Str
>  /// parameter is set).
>  pub fn renew_acme_cert(force: bool, rpcenv: &mut dyn RpcEnvironment) -> Result<String, Error> {
>      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<i64, Error> {
> +    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())

no need to convert between `Option` and `Result` here. simply do:

    if let (Ok(notafter), Ok(notbefore)) = (cert.not_after_unix(), cert.not_before_unix()) {

here :)

> +    {
> +        // 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);

i talked to Fabian a bit and we came to the following consensus
regarding the 3 day cut-off here. this function should probably just
encode the lead time itself, not the 3 day cut off as that is an
artifact of how we refresh acme certificates in the daily-update task.
essentially the plan is to have this function return the lead time as
follows:

1. for short-lived certificates (< 10 days) the lead time should be half
   of the lifetime of the certificate [1].
2. for other certificates the lead time should be 1/3 of the
   certificates lifetime [2].

this is based on recommendations by let's encrypt [1,2]. for
ultra-short-lived certificates, however, the daily update service will
only have very limited chances to successfully renew the certificate,
since it will only run once a day. hence, the 3 day cut-off should be
moved to the daily update service. the service should check the validity
of the certificate with the 3 day cut-off in mind. it should then call
the acme renewal endpoint with the `force` parameter set to `true` to
by-pass the validity check based on the lead time outlined above. does
that make sense?

[1]: https://letsencrypt.org/docs/integration-guide/
[2]: https://letsencrypt.org/2025/12/02/from-90-to-45#action-required

> +        Ok(lead)
> +    } else {
> +        Ok(30 * 24 * 60 * 60)
> +    }

tiny nit: imo it might be a bit cleaner to do `return Ok(lead)` in the
if branch above, drop the else and return `Ok(30 * 24 * 60 * 60)` by
default.

> +}
> +
>  /// Check whether the current certificate expires within the next 30 days.
>  pub fn cert_expires_soon() -> Result<bool, Error> {
>      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(());
>      }
>






  parent reply	other threads:[~2026-04-22  9:10 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-04-21 14:46 [PATCH proxmox-backup] acme: partially fix #6372: scale certificate renewal checks by lifetime Manuel Federanko
2026-04-22  6:49 ` Christian Ebner
2026-04-22  9:10   ` Shannon Sterz
2026-04-22  9:18     ` Manuel Federanko
2026-04-22  9:10 ` Shannon Sterz [this message]
2026-04-22  9:15   ` Manuel Federanko
2026-04-23 13:48 ` superseded: " Manuel Federanko
2026-04-23 18:57 ` applied: " Thomas Lamprecht

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=DHZKEJBTPLZY.TNSL8CWILFKT@proxmox.com \
    --to=s.sterz@proxmox.com \
    --cc=m.federanko@proxmox.com \
    --cc=pbs-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.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal