From: Samuel Rufinatscha <s.rufinatscha@proxmox.com>
To: "Proxmox Backup Server development discussion"
<pbs-devel@lists.proxmox.com>,
"Fabian Grünbichler" <f.gruenbichler@proxmox.com>
Subject: Re: [pbs-devel] [PATCH proxmox-backup v5 5/5] acme: certificate ordering through proxmox-acme-api
Date: Tue, 13 Jan 2026 17:51:36 +0100 [thread overview]
Message-ID: <8d0f5eb6-11ef-4186-b23c-9bcbb845a000@proxmox.com> (raw)
In-Reply-To: <1768310071.o1dolezxdy.astroid@yuna.none>
comments inline
On 1/13/26 2:45 PM, Fabian Grünbichler wrote:
> 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()
>
Good catch, will refactor!
>> +
>> + 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
>
Will adjust!
>> +
>> 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
>
>
_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
next prev parent reply other threads:[~2026-01-13 16:52 UTC|newest]
Thread overview: 23+ 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-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-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-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
2026-01-13 16:51 ` Samuel Rufinatscha [this message]
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=8d0f5eb6-11ef-4186-b23c-9bcbb845a000@proxmox.com \
--to=s.rufinatscha@proxmox.com \
--cc=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