public inbox for pbs-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: "Fabian Grünbichler" <f.gruenbichler@proxmox.com>
To: Proxmox Backup Server development discussion
	<pbs-devel@lists.proxmox.com>
Subject: Re: [pbs-devel] [PATCH proxmox-backup v5 5/5] acme: certificate ordering through proxmox-acme-api
Date: Tue, 13 Jan 2026 14:45:06 +0100	[thread overview]
Message-ID: <1768310071.o1dolezxdy.astroid@yuna.none> (raw)
In-Reply-To: <20260108112629.189670-10-s.rufinatscha@proxmox.com>

On January 8, 2026 12:26 pm, Samuel Rufinatscha wrote:
> PBS currently uses its own ACME client and API logic, while PDM uses the
> factored out proxmox-acme and proxmox-acme-api crates. This duplication
> risks differences in behaviour and requires ACME maintenance in two
> places. This patch is part of a series to move PBS over to the shared
> ACME stack.
> 
> Changes:
> - Replace the custom ACME order/authorization loop in node certificates
> with a call to proxmox_acme_api::order_certificate.
> - Build domain + config data as proxmox-acme-api types
> - Remove obsolete local ACME ordering and plugin glue code.
> 
> Signed-off-by: Samuel Rufinatscha <s.rufinatscha@proxmox.com>
> ---
>  src/acme/mod.rs               |   2 -
>  src/acme/plugin.rs            | 335 ----------------------------------
>  src/api2/node/certificates.rs | 229 ++++-------------------
>  src/api2/types/acme.rs        |  73 --------
>  src/api2/types/mod.rs         |   3 -
>  src/config/acme/mod.rs        |   8 +-
>  src/config/acme/plugin.rs     |  92 +---------
>  src/config/node.rs            |  20 +-
>  src/lib.rs                    |   2 -
>  9 files changed, 38 insertions(+), 726 deletions(-)
>  delete mode 100644 src/acme/mod.rs
>  delete mode 100644 src/acme/plugin.rs
>  delete mode 100644 src/api2/types/acme.rs
> 

[..]

> diff --git a/src/api2/node/certificates.rs b/src/api2/node/certificates.rs
> index 47ff8de5..73401c41 100644
> --- a/src/api2/node/certificates.rs
> +++ b/src/api2/node/certificates.rs
> @@ -1,14 +1,11 @@
> -use std::sync::Arc;
> -use std::time::Duration;
> -
>  use anyhow::{bail, format_err, Error};
>  use openssl::pkey::PKey;
>  use openssl::x509::X509;
>  use serde::{Deserialize, Serialize};
> -use tracing::{info, warn};
> +use tracing::info;
>  
>  use pbs_api_types::{NODE_SCHEMA, PRIV_SYS_MODIFY};
> -use proxmox_acme::async_client::AcmeClient;
> +use proxmox_acme_api::AcmeDomain;
>  use proxmox_rest_server::WorkerTask;
>  use proxmox_router::list_subdirs_api_method;
>  use proxmox_router::SubdirMap;
> @@ -18,8 +15,6 @@ use proxmox_schema::api;
>  use pbs_buildcfg::configdir;
>  use pbs_tools::cert;
>  
> -use crate::api2::types::AcmeDomain;
> -use crate::config::node::NodeConfig;
>  use crate::server::send_certificate_renewal_mail;
>  
>  pub const ROUTER: Router = Router::new()
> @@ -268,193 +263,6 @@ pub async fn delete_custom_certificate() -> Result<(), Error> {
>      Ok(())
>  }
>  
> -struct OrderedCertificate {
> -    certificate: hyper::body::Bytes,
> -    private_key_pem: Vec<u8>,
> -}
> -
> -async fn order_certificate(
> -    worker: Arc<WorkerTask>,
> -    node_config: &NodeConfig,
> -) -> Result<Option<OrderedCertificate>, Error> {
> -    use proxmox_acme::authorization::Status;
> -    use proxmox_acme::order::Identifier;
> -
> -    let domains = node_config.acme_domains().try_fold(
> -        Vec::<AcmeDomain>::new(),
> -        |mut acc, domain| -> Result<_, Error> {
> -            let mut domain = domain?;
> -            domain.domain.make_ascii_lowercase();
> -            if let Some(alias) = &mut domain.alias {
> -                alias.make_ascii_lowercase();
> -            }
> -            acc.push(domain);
> -            Ok(acc)
> -        },
> -    )?;
> -
> -    let get_domain_config = |domain: &str| {
> -        domains
> -            .iter()
> -            .find(|d| d.domain == domain)
> -            .ok_or_else(|| format_err!("no config for domain '{}'", domain))
> -    };
> -
> -    if domains.is_empty() {
> -        info!("No domains configured to be ordered from an ACME server.");
> -        return Ok(None);
> -    }
> -
> -    let (plugins, _) = crate::config::acme::plugin::config()?;
> -
> -    let mut acme = node_config.acme_client().await?;
> -
> -    info!("Placing ACME order");
> -    let order = acme
> -        .new_order(domains.iter().map(|d| d.domain.to_ascii_lowercase()))
> -        .await?;
> -    info!("Order URL: {}", order.location);
> -
> -    let identifiers: Vec<String> = order
> -        .data
> -        .identifiers
> -        .iter()
> -        .map(|identifier| match identifier {
> -            Identifier::Dns(domain) => domain.clone(),
> -        })
> -        .collect();
> -
> -    for auth_url in &order.data.authorizations {
> -        info!("Getting authorization details from '{auth_url}'");
> -        let mut auth = acme.get_authorization(auth_url).await?;
> -
> -        let domain = match &mut auth.identifier {
> -            Identifier::Dns(domain) => domain.to_ascii_lowercase(),
> -        };
> -
> -        if auth.status == Status::Valid {
> -            info!("{domain} is already validated!");
> -            continue;
> -        }
> -
> -        info!("The validation for {domain} is pending");
> -        let domain_config: &AcmeDomain = get_domain_config(&domain)?;
> -        let plugin_id = domain_config.plugin.as_deref().unwrap_or("standalone");
> -        let mut plugin_cfg = crate::acme::get_acme_plugin(&plugins, plugin_id)?
> -            .ok_or_else(|| format_err!("plugin '{plugin_id}' for domain '{domain}' not found!"))?;
> -
> -        info!("Setting up validation plugin");
> -        let validation_url = plugin_cfg
> -            .setup(&mut acme, &auth, domain_config, Arc::clone(&worker))
> -            .await?;
> -
> -        let result = request_validation(&mut acme, auth_url, validation_url).await;
> -
> -        if let Err(err) = plugin_cfg
> -            .teardown(&mut acme, &auth, domain_config, Arc::clone(&worker))
> -            .await
> -        {
> -            warn!("Failed to teardown plugin '{plugin_id}' for domain '{domain}' - {err}");
> -        }
> -
> -        result?;
> -    }
> -
> -    info!("All domains validated");
> -    info!("Creating CSR");
> -
> -    let csr = proxmox_acme::util::Csr::generate(&identifiers, &Default::default())?;
> -    let mut finalize_error_cnt = 0u8;
> -    let order_url = &order.location;
> -    let mut order;
> -    loop {
> -        use proxmox_acme::order::Status;
> -
> -        order = acme.get_order(order_url).await?;
> -
> -        match order.status {
> -            Status::Pending => {
> -                info!("still pending, trying to finalize anyway");
> -                let finalize = order
> -                    .finalize
> -                    .as_deref()
> -                    .ok_or_else(|| format_err!("missing 'finalize' URL in order"))?;
> -                if let Err(err) = acme.finalize(finalize, &csr.data).await {
> -                    if finalize_error_cnt >= 5 {
> -                        return Err(err);
> -                    }
> -
> -                    finalize_error_cnt += 1;
> -                }
> -                tokio::time::sleep(Duration::from_secs(5)).await;
> -            }
> -            Status::Ready => {
> -                info!("order is ready, finalizing");
> -                let finalize = order
> -                    .finalize
> -                    .as_deref()
> -                    .ok_or_else(|| format_err!("missing 'finalize' URL in order"))?;
> -                acme.finalize(finalize, &csr.data).await?;
> -                tokio::time::sleep(Duration::from_secs(5)).await;
> -            }
> -            Status::Processing => {
> -                info!("still processing, trying again in 30 seconds");
> -                tokio::time::sleep(Duration::from_secs(30)).await;
> -            }
> -            Status::Valid => {
> -                info!("valid");
> -                break;
> -            }
> -            other => bail!("order status: {:?}", other),
> -        }
> -    }
> -
> -    info!("Downloading certificate");
> -    let certificate = acme
> -        .get_certificate(
> -            order
> -                .certificate
> -                .as_deref()
> -                .ok_or_else(|| format_err!("missing certificate url in finalized order"))?,
> -        )
> -        .await?;
> -
> -    Ok(Some(OrderedCertificate {
> -        certificate,
> -        private_key_pem: csr.private_key_pem,
> -    }))
> -}
> -
> -async fn request_validation(
> -    acme: &mut AcmeClient,
> -    auth_url: &str,
> -    validation_url: &str,
> -) -> Result<(), Error> {
> -    info!("Triggering validation");
> -    acme.request_challenge_validation(validation_url).await?;
> -
> -    info!("Sleeping for 5 seconds");
> -    tokio::time::sleep(Duration::from_secs(5)).await;
> -
> -    loop {
> -        use proxmox_acme::authorization::Status;
> -
> -        let auth = acme.get_authorization(auth_url).await?;
> -        match auth.status {
> -            Status::Pending => {
> -                info!("Status is still 'pending', trying again in 10 seconds");
> -                tokio::time::sleep(Duration::from_secs(10)).await;
> -            }
> -            Status::Valid => return Ok(()),
> -            other => bail!(
> -                "validating challenge '{}' failed - status: {:?}",
> -                validation_url,
> -                other
> -            ),
> -        }
> -    }
> -}
> -
>  #[api(
>      input: {
>          properties: {
> @@ -524,9 +332,30 @@ fn spawn_certificate_worker(
>  
>      let auth_id = rpcenv.get_auth_id().unwrap();
>  
> +    let acme_config = if let Some(cfg) = node_config.acme_config().transpose()? {
> +        cfg
> +    } else {
> +        proxmox_acme_api::parse_acme_config_string("account=default")?
> +    };

wouldn't it make sense to inline this into acme_config() ? the same
fallback is already there for acme_client()

> +
> +    let domains = node_config.acme_domains().try_fold(
> +        Vec::<AcmeDomain>::new(),
> +        |mut acc, domain| -> Result<_, Error> {
> +            let mut domain = domain?;
> +            domain.domain.make_ascii_lowercase();
> +            if let Some(alias) = &mut domain.alias {
> +                alias.make_ascii_lowercase();
> +            }
> +            acc.push(domain);
> +            Ok(acc)
> +        },
> +    )?;
> +
>      WorkerTask::spawn(name, None, auth_id, true, move |worker| async move {
>          let work = || async {
> -            if let Some(cert) = order_certificate(worker, &node_config).await? {
> +            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?;
>              }
> @@ -562,16 +391,20 @@ pub fn revoke_acme_cert(rpcenv: &mut dyn RpcEnvironment) -> Result<String, Error
>  
>      let auth_id = rpcenv.get_auth_id().unwrap();
>  
> +    let acme_config = if let Some(cfg) = node_config.acme_config().transpose()? {
> +        cfg
> +    } else {
> +        proxmox_acme_api::parse_acme_config_string("account=default")?
> +    };

here as well

> +
>      WorkerTask::spawn(
>          "acme-revoke-cert",
>          None,
>          auth_id,
>          true,
>          move |_worker| async move {
> -            info!("Loading ACME account");
> -            let mut acme = node_config.acme_client().await?;
>              info!("Revoking old certificate");
> -            acme.revoke_certificate(cert_pem.as_bytes(), None).await?;
> +            proxmox_acme_api::revoke_certificate(&acme_config, &cert_pem.as_bytes()).await?;
>              info!("Deleting certificate and regenerating a self-signed one");
>              delete_custom_certificate().await?;
>              Ok(())

[..]

> diff --git a/src/config/acme/plugin.rs b/src/config/acme/plugin.rs
> index 8ce852ec..4b4a216e 100644
> --- a/src/config/acme/plugin.rs
> +++ b/src/config/acme/plugin.rs
> @@ -1,104 +1,16 @@
>  use std::sync::LazyLock;
>  
>  use anyhow::Error;
> -use serde::{Deserialize, Serialize};
>  use serde_json::Value;
>  
> -use pbs_api_types::PROXMOX_SAFE_ID_FORMAT;
> -use proxmox_schema::{api, ApiType, Schema, StringSchema, Updater};
> +use proxmox_acme_api::{DnsPlugin, StandalonePlugin, PLUGIN_ID_SCHEMA};
> +use proxmox_schema::{ApiType, Schema};
>  use proxmox_section_config::{SectionConfig, SectionConfigData, SectionConfigPlugin};
>  
>  use pbs_config::{open_backup_lockfile, BackupLockGuard};
>  
> -pub const PLUGIN_ID_SCHEMA: Schema = StringSchema::new("ACME Challenge Plugin ID.")
> -    .format(&PROXMOX_SAFE_ID_FORMAT)
> -    .min_length(1)
> -    .max_length(32)
> -    .schema();
> -
>  pub static CONFIG: LazyLock<SectionConfig> = LazyLock::new(init);
>  
> -#[api(
> -    properties: {
> -        id: { schema: PLUGIN_ID_SCHEMA },
> -    },
> -)]
> -#[derive(Deserialize, Serialize)]
> -/// Standalone ACME Plugin for the http-1 challenge.
> -pub struct StandalonePlugin {
> -    /// Plugin ID.
> -    id: String,
> -}
> -
> -impl Default for StandalonePlugin {
> -    fn default() -> Self {
> -        Self {
> -            id: "standalone".to_string(),
> -        }
> -    }
> -}
> -
> -#[api(
> -    properties: {
> -        id: { schema: PLUGIN_ID_SCHEMA },
> -        disable: {
> -            optional: true,
> -            default: false,
> -        },
> -        "validation-delay": {
> -            default: 30,
> -            optional: true,
> -            minimum: 0,
> -            maximum: 2 * 24 * 60 * 60,
> -        },
> -    },
> -)]
> -/// DNS ACME Challenge Plugin core data.
> -#[derive(Deserialize, Serialize, Updater)]
> -#[serde(rename_all = "kebab-case")]
> -pub struct DnsPluginCore {
> -    /// Plugin ID.
> -    #[updater(skip)]
> -    pub id: String,
> -
> -    /// DNS API Plugin Id.
> -    pub api: String,
> -
> -    /// Extra delay in seconds to wait before requesting validation.
> -    ///
> -    /// Allows to cope with long TTL of DNS records.
> -    #[serde(skip_serializing_if = "Option::is_none", default)]
> -    pub validation_delay: Option<u32>,
> -
> -    /// Flag to disable the config.
> -    #[serde(skip_serializing_if = "Option::is_none", default)]
> -    pub disable: Option<bool>,
> -}
> -
> -#[api(
> -    properties: {
> -        core: { type: DnsPluginCore },
> -    },
> -)]
> -/// DNS ACME Challenge Plugin.
> -#[derive(Deserialize, Serialize)]
> -#[serde(rename_all = "kebab-case")]
> -pub struct DnsPlugin {
> -    #[serde(flatten)]
> -    pub core: DnsPluginCore,
> -
> -    // We handle this property separately in the API calls.
> -    /// DNS plugin data (base64url encoded without padding).
> -    #[serde(with = "proxmox_serde::string_as_base64url_nopad")]
> -    pub data: String,
> -}
> -
> -impl DnsPlugin {
> -    pub fn decode_data(&self, output: &mut Vec<u8>) -> Result<(), Error> {
> -        Ok(proxmox_base64::url::decode_to_vec(&self.data, output)?)
> -    }
> -}
> -
>  fn init() -> SectionConfig {
>      let mut config = SectionConfig::new(&PLUGIN_ID_SCHEMA);
>  
> diff --git a/src/config/node.rs b/src/config/node.rs
> index e4b66a20..6865b815 100644
> --- a/src/config/node.rs
> +++ b/src/config/node.rs
> @@ -9,14 +9,14 @@ use pbs_api_types::{
>      OPENSSL_CIPHERS_TLS_1_3_SCHEMA,
>  };
>  use proxmox_acme::async_client::AcmeClient;
> -use proxmox_acme_api::AcmeAccountName;
> +use proxmox_acme_api::{AcmeAccountName, AcmeConfig, AcmeDomain, ACME_DOMAIN_PROPERTY_SCHEMA};
>  use proxmox_http::ProxyConfig;
>  use proxmox_schema::{api, ApiStringFormat, ApiType, Updater};
>  
>  use pbs_buildcfg::configdir;
>  use pbs_config::{open_backup_lockfile, BackupLockGuard};
>  
> -use crate::api2::types::{AcmeDomain, ACME_DOMAIN_PROPERTY_SCHEMA, HTTP_PROXY_SCHEMA};
> +use crate::api2::types::HTTP_PROXY_SCHEMA;
>  
>  const CONF_FILE: &str = configdir!("/node.cfg");
>  const LOCK_FILE: &str = configdir!("/.node.lck");
> @@ -43,20 +43,6 @@ pub fn save_config(config: &NodeConfig) -> Result<(), Error> {
>      pbs_config::replace_backup_config(CONF_FILE, &raw)
>  }
>  
> -#[api(
> -    properties: {
> -        account: { type: AcmeAccountName },
> -    }
> -)]
> -#[derive(Deserialize, Serialize)]
> -/// The ACME configuration.
> -///
> -/// Currently only contains the name of the account use.
> -pub struct AcmeConfig {
> -    /// Account to use to acquire ACME certificates.
> -    account: AcmeAccountName,
> -}
> -
>  /// All available languages in Proxmox. Taken from proxmox-i18n repository.
>  /// pt_BR, zh_CN, and zh_TW use the same case in the translation files.
>  // TODO: auto-generate from available translations
> @@ -242,7 +228,7 @@ impl NodeConfig {
>  
>      pub async fn acme_client(&self) -> Result<AcmeClient, Error> {
>          let account = if let Some(cfg) = self.acme_config().transpose()? {
> -            cfg.account
> +            AcmeAccountName::from_string(cfg.account)?
>          } else {
>              AcmeAccountName::from_string("default".to_string())? // should really not happen
>          };
> diff --git a/src/lib.rs b/src/lib.rs
> index 8633378c..828f5842 100644
> --- a/src/lib.rs
> +++ b/src/lib.rs
> @@ -27,8 +27,6 @@ pub(crate) mod auth;
>  
>  pub mod tape;
>  
> -pub mod acme;
> -
>  pub mod client_helpers;
>  
>  pub mod traffic_control_cache;
> -- 
> 2.47.3
> 
> 
> 
> _______________________________________________
> pbs-devel mailing list
> pbs-devel@lists.proxmox.com
> https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
> 
> 
> 


_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel


  reply	other threads:[~2026-01-13 13:45 UTC|newest]

Thread overview: 29+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-01-08 11:26 [pbs-devel] [PATCH proxmox{, -backup} v5 0/9] fix #6939: acme: support servers returning 204 for nonce requests Samuel Rufinatscha
2026-01-08 11:26 ` [pbs-devel] [PATCH proxmox v5 1/4] acme: reduce visibility of Request type Samuel Rufinatscha
2026-01-13 13:46   ` Fabian Grünbichler
2026-01-14 15:07     ` Samuel Rufinatscha
2026-01-08 11:26 ` [pbs-devel] [PATCH proxmox v5 2/4] acme: introduce http_status module Samuel Rufinatscha
2026-01-13 13:45   ` Fabian Grünbichler
2026-01-14 10:29     ` Samuel Rufinatscha
2026-01-08 11:26 ` [pbs-devel] [PATCH proxmox v5 3/4] fix #6939: acme: support servers returning 204 for nonce requests Samuel Rufinatscha
2026-01-08 11:26 ` [pbs-devel] [PATCH proxmox v5 4/4] acme-api: add helper to load client for an account Samuel Rufinatscha
2026-01-13 13:45   ` Fabian Grünbichler
2026-01-13 16:57     ` Samuel Rufinatscha
2026-01-08 11:26 ` [pbs-devel] [PATCH proxmox-backup v5 1/5] acme: clean up ACME-related imports Samuel Rufinatscha
2026-01-13 13:45   ` [pbs-devel] applied: " Fabian Grünbichler
2026-01-08 11:26 ` [pbs-devel] [PATCH proxmox-backup v5 2/5] acme: include proxmox-acme-api dependency Samuel Rufinatscha
2026-01-13 13:45   ` Fabian Grünbichler
2026-01-13 16:41     ` Samuel Rufinatscha
2026-01-08 11:26 ` [pbs-devel] [PATCH proxmox-backup v5 3/5] acme: drop local AcmeClient Samuel Rufinatscha
2026-01-13 13:45   ` Fabian Grünbichler
2026-01-14  8:56     ` Samuel Rufinatscha
2026-01-14  9:58       ` Fabian Grünbichler
2026-01-14 10:52         ` Samuel Rufinatscha
2026-01-14 16:41           ` Samuel Rufinatscha
2026-01-08 11:26 ` [pbs-devel] [PATCH proxmox-backup v5 4/5] acme: change API impls to use proxmox-acme-api handlers Samuel Rufinatscha
2026-01-13 13:45   ` Fabian Grünbichler
2026-01-13 16:53     ` Samuel Rufinatscha
2026-01-08 11:26 ` [pbs-devel] [PATCH proxmox-backup v5 5/5] acme: certificate ordering through proxmox-acme-api Samuel Rufinatscha
2026-01-13 13:45   ` Fabian Grünbichler [this message]
2026-01-13 16:51     ` Samuel Rufinatscha
2026-01-13 13:48 ` [pbs-devel] [PATCH proxmox{, -backup} v5 0/9] fix #6939: acme: support servers returning 204 for nonce requests Fabian Grünbichler

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=1768310071.o1dolezxdy.astroid@yuna.none \
    --to=f.gruenbichler@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 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