* [RFC datacenter-manager/proxmox{,-backup} 00/10] TLS Certificate Rotation
@ 2026-04-07 13:57 Shannon Sterz
2026-04-07 13:57 ` [PATCH proxmox 01/10] acme-api: make self-signed certificate expiry configurable Shannon Sterz
` (9 more replies)
0 siblings, 10 replies; 12+ messages in thread
From: Shannon Sterz @ 2026-04-07 13:57 UTC (permalink / raw)
To: pbs-devel
this series adds certificate rotation to Proxmox Backup Server and Proxmox
Datacenter Manager. currently, both products issue a certificate that is valid
for almost 1000 years (365000 days). no cryptographic key can reasonably be
considered secure for this amount of time. this series:
- allows specifying the lifetime of the certificate when creating one via
proxmox-acme-api and reduces the default to 3650 days (almost ten years).
- sends and logs reminders 30 days before a certificate expires (pdm currently
does not support the notification framework yet, so adding notifications is
left as future work here).
- refreshes a certificate at the earliest 15 days before it expires, logs
and notifies when that happens.
- warns on certificates with excessive lifetimes (>3650 days) and documents
how to manually update them.
- for pdm: exposes cert handling cli methods in proxmox-datacenter-manager-admin.
sending this as an rfc mainly because there are some open questions for me
about the chosen time frames for the lifetime and renewal periods.
## Testing
the easiest way to test this is to manipulate the date of the host with `date
--set` and then manually trigger the daily update binary for each product:
* PBS: `/usr/lib/x86_64-linux-gnu/proxmox-backup/proxmox-daily-update`
* PDM: `/usr/libexec/proxmox/proxmox-datacenter-manager-daily-update`
you can then check the logs and the certificate itself to see what happened.
specifying the `PBS_LOG` with the parameter `trace` or `debug` will also enable
debug logging here.
## Open Questions
+ 10 years is still a long time and i'd rather reduce that further down if
possible. see the first patch for proxmox-acme-api for more info.
+ should we remove pre-existing long lasting certificates by ourselves? imo
that is too risky at the moment given that an unplanned certificate rotation
could cause backups to fail.
+ notifying every day for 15 days before the renewal might be excessive, see
the second commit for pbs.
## Future Work
- pve and pdm should be extended to allow automatically updating allowed
fingerprints before a new self-signed certificate goes into action. this will
be handled in a follow-up series. if this series is applied, we have ten years
to implement such a mechanism before any setups are realistically expected to
break.
- pdm should send notifications similar to pbs once support for notifications
is added.
proxmox:
Shannon Sterz (1):
acme-api: make self-signed certificate expiry configurable
proxmox-acme-api/src/certificate_helpers.rs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
backup:
Shannon Sterz (5):
config: use proxmox_acme_api for generating self-signed certificates
config: adapt to api change in proxmox_acme_api, add expiry paramter
config/server/api: add certificate renewal logic including
notifications
daily-update/docs: warn on excessive self-signed certificate lifetime
backup-manager cli: `cert update` can create auth and csrf key
debian/proxmox-backup-server.install | 4 +
docs/certificate-management.rst | 31 ++++++
src/api2/node/certificates.rs | 44 +++++++++
src/bin/proxmox-daily-update.rs | 32 +++++++
src/bin/proxmox_backup_manager/cert.rs | 2 +
src/config/mod.rs | 96 ++-----------------
src/server/notifications/mod.rs | 50 ++++++++++
templates/Makefile | 62 ++++++------
templates/default/cert-refresh-body.txt.hbs | 8 ++
.../default/cert-refresh-subject.txt.hbs | 1 +
.../cert-upcoming-refresh-body.txt.hbs | 9 ++
.../cert-upcoming-refresh-subject.txt.hbs | 1 +
12 files changed, 225 insertions(+), 115 deletions(-)
create mode 100644 templates/default/cert-refresh-body.txt.hbs
create mode 100644 templates/default/cert-refresh-subject.txt.hbs
create mode 100644 templates/default/cert-upcoming-refresh-body.txt.hbs
create mode 100644 templates/default/cert-upcoming-refresh-subject.txt.hbs
datacenter-manager:
Shannon Sterz (4):
certs: adapt to api change in proxmox_acme_api, add expiry paramter
api/auth/bin: add certificate renewal logic
cli: expose certificate management endpoints via the cli
daily-update/docs: warn on excessive tls certificate validity periods
cli/admin/Cargo.toml | 2 +
cli/admin/src/cert.rs | 86 +++++++++++++++++++
cli/admin/src/main.rs | 2 +
docs/certificate-management.rst | 31 +++++++
server/Cargo.toml | 1 +
server/src/api/nodes/certificates.rs | 48 +++++++++++
server/src/auth/certs.rs | 4 +-
...proxmox-datacenter-manager-daily-update.rs | 30 +++++++
8 files changed, 203 insertions(+), 1 deletion(-)
create mode 100644 cli/admin/src/cert.rs
Summary over all repositories:
21 files changed, 430 insertions(+), 117 deletions(-)
--
Generated by murpp 0.10.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH proxmox 01/10] acme-api: make self-signed certificate expiry configurable
2026-04-07 13:57 [RFC datacenter-manager/proxmox{,-backup} 00/10] TLS Certificate Rotation Shannon Sterz
@ 2026-04-07 13:57 ` Shannon Sterz
2026-04-07 13:57 ` [PATCH proxmox-backup 02/10] config: use proxmox_acme_api for generating self-signed certificates Shannon Sterz
` (8 subsequent siblings)
9 siblings, 0 replies; 12+ messages in thread
From: Shannon Sterz @ 2026-04-07 13:57 UTC (permalink / raw)
To: pbs-devel
and change the default from 365000 days (almost 1000 years) to 3650
days (almost 10 years). almost 1000 years is excessive, as no
practical cryptographic key can reasonably be considered safe for that
amount of time. almost 10 years should still give plenty of time to
prepare for certificate changes.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
Notes:
imo, we could go down even more. as far as i am aware there is no real
limit that is being enforced here for self-signed certificates from a
browser perspective. they are already trusted on an exemption-basis
anyway. however, certificates signed by public CAs will only be valid
for a maximum of 47 days by 2029 [1].
hence, i would personally either adopt the same limit or go down to a
year, as a sensible middle-ground. certificate rotation should really
be automated even in self-signed scenarios. we also had cases in the
past, where customers already ran into issue because they wanted to
limit the lifetime of their certificates below 30 days [2]. meaning
that there is a need out there for shorter lived certificates (though,
in that case a custom CA & ACME setup was used).
[1]: https://github.com/cabforum/servercert/pull/553
[2]: https://bugzilla.proxmox.com/show_bug.cgi?id=6372
proxmox-acme-api/src/certificate_helpers.rs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/proxmox-acme-api/src/certificate_helpers.rs b/proxmox-acme-api/src/certificate_helpers.rs
index a4fe153a..c09bd65c 100644
--- a/proxmox-acme-api/src/certificate_helpers.rs
+++ b/proxmox-acme-api/src/certificate_helpers.rs
@@ -214,6 +214,7 @@ pub fn create_self_signed_cert(
product_name: &str,
nodename: &str,
domain: Option<&str>,
+ expire: Option<u32>,
) -> Result<(PKey<Private>, X509), Error> {
let rsa = Rsa::generate(4096).unwrap();
@@ -223,7 +224,7 @@ pub fn create_self_signed_cert(
let today = openssl::asn1::Asn1Time::days_from_now(0)?;
x509.set_not_before(&today)?;
- let expire = openssl::asn1::Asn1Time::days_from_now(365 * 1000)?;
+ let expire = openssl::asn1::Asn1Time::days_from_now(expire.unwrap_or(365 * 10))?;
x509.set_not_after(&expire)?;
let mut fqdn = nodename.to_owned();
--
2.47.3
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH proxmox-backup 02/10] config: use proxmox_acme_api for generating self-signed certificates
2026-04-07 13:57 [RFC datacenter-manager/proxmox{,-backup} 00/10] TLS Certificate Rotation Shannon Sterz
2026-04-07 13:57 ` [PATCH proxmox 01/10] acme-api: make self-signed certificate expiry configurable Shannon Sterz
@ 2026-04-07 13:57 ` Shannon Sterz
2026-04-07 13:57 ` [PATCH proxmox-backup 03/10] config: adapt to api change in proxmox_acme_api, add expiry paramter Shannon Sterz
` (7 subsequent siblings)
9 siblings, 0 replies; 12+ messages in thread
From: Shannon Sterz @ 2026-04-07 13:57 UTC (permalink / raw)
To: pbs-devel
to avoid duplicating almost identical code here, re-use the version
from `proxmox_acme_api::create_self_signed_cert`. proxmox backup
server already depends on `proxmox_acme_api` and the code is identical
apart from handling arguments. no functional change intended.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
src/config/mod.rs | 93 ++++-------------------------------------------
1 file changed, 7 insertions(+), 86 deletions(-)
diff --git a/src/config/mod.rs b/src/config/mod.rs
index 2de76bb1..3d48a25e 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -5,9 +5,6 @@
use anyhow::{bail, format_err, Error};
use nix::sys::stat::Mode;
-use openssl::pkey::PKey;
-use openssl::rsa::Rsa;
-use openssl::x509::X509Builder;
use std::path::Path;
use proxmox_lang::try_block;
@@ -89,92 +86,16 @@ pub fn update_self_signed_cert(force: bool) -> Result<(), Error> {
if key_path.exists() && cert_path.exists() && !force {
return Ok(());
}
-
- let rsa = Rsa::generate(4096).unwrap();
-
- let priv_pem = rsa.private_key_to_pem()?;
-
- let mut x509 = X509Builder::new()?;
-
- x509.set_version(2)?;
-
- let today = openssl::asn1::Asn1Time::days_from_now(0)?;
- x509.set_not_before(&today)?;
- let expire = openssl::asn1::Asn1Time::days_from_now(365 * 1000)?;
- x509.set_not_after(&expire)?;
-
- let nodename = proxmox_sys::nodename();
- let mut fqdn = nodename.to_owned();
-
let resolv_conf = crate::api2::node::dns::read_etc_resolv_conf()?;
- if let Some(search) = resolv_conf["search"].as_str() {
- fqdn.push('.');
- fqdn.push_str(search);
- }
- // we try to generate an unique 'subject' to avoid browser problems
- //(reused serial numbers, ..)
- let uuid = proxmox_uuid::Uuid::generate();
+ let (priv_key, cert) = proxmox_acme_api::create_self_signed_cert(
+ "Proxmox Backup Server",
+ proxmox_sys::nodename(),
+ resolv_conf["search"].as_str(),
+ )?;
- let mut subject_name = openssl::x509::X509NameBuilder::new()?;
- subject_name.append_entry_by_text("O", "Proxmox Backup Server")?;
- subject_name.append_entry_by_text("OU", &format!("{uuid:X}"))?;
- subject_name.append_entry_by_text("CN", &fqdn)?;
- let subject_name = subject_name.build();
-
- x509.set_subject_name(&subject_name)?;
- x509.set_issuer_name(&subject_name)?;
-
- let bc = openssl::x509::extension::BasicConstraints::new(); // CA = false
- let bc = bc.build()?;
- x509.append_extension(bc)?;
-
- let usage = openssl::x509::extension::ExtendedKeyUsage::new()
- .server_auth()
- .build()?;
- x509.append_extension(usage)?;
-
- let context = x509.x509v3_context(None, None);
-
- let mut alt_names = openssl::x509::extension::SubjectAlternativeName::new();
-
- alt_names.ip("127.0.0.1");
- alt_names.ip("::1");
-
- alt_names.dns("localhost");
-
- if nodename != "localhost" {
- alt_names.dns(nodename);
- }
- if nodename != fqdn {
- alt_names.dns(&fqdn);
- }
-
- let alt_names = alt_names.build(&context)?;
-
- x509.append_extension(alt_names)?;
-
- let pub_pem = rsa.public_key_to_pem()?;
- let pubkey = PKey::public_key_from_pem(&pub_pem)?;
-
- x509.set_pubkey(&pubkey)?;
-
- let context = x509.x509v3_context(None, None);
- let ext = openssl::x509::extension::SubjectKeyIdentifier::new().build(&context)?;
- x509.append_extension(ext)?;
-
- let context = x509.x509v3_context(None, None);
- let ext = openssl::x509::extension::AuthorityKeyIdentifier::new()
- .keyid(true)
- .build(&context)?;
- x509.append_extension(ext)?;
-
- let privkey = PKey::from_rsa(rsa)?;
-
- x509.sign(&privkey, openssl::hash::MessageDigest::sha256())?;
-
- let x509 = x509.build();
- let cert_pem = x509.to_pem()?;
+ let cert_pem = cert.to_pem()?;
+ let priv_pem = priv_key.private_key_to_pem_pkcs8()?;
set_proxy_certificate(&cert_pem, &priv_pem)?;
--
2.47.3
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH proxmox-backup 03/10] config: adapt to api change in proxmox_acme_api, add expiry paramter
2026-04-07 13:57 [RFC datacenter-manager/proxmox{,-backup} 00/10] TLS Certificate Rotation Shannon Sterz
2026-04-07 13:57 ` [PATCH proxmox 01/10] acme-api: make self-signed certificate expiry configurable Shannon Sterz
2026-04-07 13:57 ` [PATCH proxmox-backup 02/10] config: use proxmox_acme_api for generating self-signed certificates Shannon Sterz
@ 2026-04-07 13:57 ` Shannon Sterz
2026-04-07 13:57 ` [PATCH proxmox-backup 04/10] config/server/api: add certificate renewal logic including notifications Shannon Sterz
` (6 subsequent siblings)
9 siblings, 0 replies; 12+ messages in thread
From: Shannon Sterz @ 2026-04-07 13:57 UTC (permalink / raw)
To: pbs-devel
using `None` means that we use the default expiry of 3650 days, almost
ten years.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
src/config/mod.rs | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/config/mod.rs b/src/config/mod.rs
index 3d48a25e..56907ae9 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -92,6 +92,7 @@ pub fn update_self_signed_cert(force: bool) -> Result<(), Error> {
"Proxmox Backup Server",
proxmox_sys::nodename(),
resolv_conf["search"].as_str(),
+ None,
)?;
let cert_pem = cert.to_pem()?;
--
2.47.3
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH proxmox-backup 04/10] config/server/api: add certificate renewal logic including notifications
2026-04-07 13:57 [RFC datacenter-manager/proxmox{,-backup} 00/10] TLS Certificate Rotation Shannon Sterz
` (2 preceding siblings ...)
2026-04-07 13:57 ` [PATCH proxmox-backup 03/10] config: adapt to api change in proxmox_acme_api, add expiry paramter Shannon Sterz
@ 2026-04-07 13:57 ` Shannon Sterz
2026-04-07 13:57 ` [PATCH proxmox-backup 05/10] daily-update/docs: warn on excessive self-signed certificate lifetime Shannon Sterz
` (5 subsequent siblings)
9 siblings, 0 replies; 12+ messages in thread
From: Shannon Sterz @ 2026-04-07 13:57 UTC (permalink / raw)
To: pbs-devel
the daily-update service is used to check whether a self-signed
certificate is in use and renews it if it would expire within the next
15 days. it will also send out reminder notifications starting 15 days
before a certificate could be renewed.
a self-signed certificate is detected by parsing the issuer field of
the certificate. if it aligns with how self-signed certificates are
created by this instance of proxmox backup server, it will be deemed
self-signed.
15 days was chosen as that should give a reasonable trade-off between
not failing if the daily-update service can't run on the specific day
that the certificate would expire and not refreshing the certificate
too often. note that 15 days before expiry corresponds to let's
encrypt's recommendation for when to update new certificates that are
valid for 45 days:
> Acceptable behavior includes renewing certificates at approximately
> two thirds of the way through the current certificate’s lifetime.
>
> - https://letsencrypt.org/2025/12/02/from-90-to-45#action-required
15 days before expiry is about two thirds of the lifetime of a
certificate that lasts for 45 days.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
Notes:
currently this will send out a notification every time the service is
run in the 15 days between a renewal being imminent and not being
renewed yet. this is probably excessive and we should limit the
notifications here to only go out once per week (or similar).
i chose 15 days instead of two thirds of ten years, because for a long
lasting certificate like that, trying to refresh it starting from 3.3
years before it expires seems excessive to me.
debian/proxmox-backup-server.install | 4 ++
src/api2/node/certificates.rs | 44 +++++++++++++
src/bin/proxmox-daily-update.rs | 28 +++++++++
src/config/mod.rs | 4 +-
src/server/notifications/mod.rs | 50 +++++++++++++++
templates/Makefile | 62 ++++++++++---------
templates/default/cert-refresh-body.txt.hbs | 8 +++
.../default/cert-refresh-subject.txt.hbs | 1 +
.../cert-upcoming-refresh-body.txt.hbs | 9 +++
.../cert-upcoming-refresh-subject.txt.hbs | 1 +
10 files changed, 181 insertions(+), 30 deletions(-)
create mode 100644 templates/default/cert-refresh-body.txt.hbs
create mode 100644 templates/default/cert-refresh-subject.txt.hbs
create mode 100644 templates/default/cert-upcoming-refresh-body.txt.hbs
create mode 100644 templates/default/cert-upcoming-refresh-subject.txt.hbs
diff --git a/debian/proxmox-backup-server.install b/debian/proxmox-backup-server.install
index 254a4b4b..674c9bf0 100644
--- a/debian/proxmox-backup-server.install
+++ b/debian/proxmox-backup-server.install
@@ -45,6 +45,10 @@ usr/share/man/man5/user.cfg.5
usr/share/man/man5/verification.cfg.5
usr/share/proxmox-backup/templates/default/acme-err-body.txt.hbs
usr/share/proxmox-backup/templates/default/acme-err-subject.txt.hbs
+usr/share/proxmox-backup/templates/default/cert-refresh-body.txt.hbs
+usr/share/proxmox-backup/templates/default/cert-refresh-subject.txt.hbs
+usr/share/proxmox-backup/templates/default/cert-upcoming-refresh-body.txt.hbs
+usr/share/proxmox-backup/templates/default/cert-upcoming-refresh-subject.txt.hbs
usr/share/proxmox-backup/templates/default/gc-err-body.txt.hbs
usr/share/proxmox-backup/templates/default/gc-err-subject.txt.hbs
usr/share/proxmox-backup/templates/default/gc-ok-body.txt.hbs
diff --git a/src/api2/node/certificates.rs b/src/api2/node/certificates.rs
index a69f6511..5812c4fa 100644
--- a/src/api2/node/certificates.rs
+++ b/src/api2/node/certificates.rs
@@ -1,4 +1,6 @@
use anyhow::{bail, format_err, Error};
+use const_format::concatcp;
+use openssl::asn1::Asn1Time;
use openssl::pkey::PKey;
use openssl::x509::X509;
use serde::{Deserialize, Serialize};
@@ -17,6 +19,14 @@ use pbs_tools::cert;
use crate::server::send_certificate_renewal_mail;
+proxmox_schema::const_regex! {
+ pub SELF_SIGNED_REGEX = concatcp!(
+ r#"^O\s?=\s?"#,
+ crate::config::PRODUCT_NAME,
+ r#", OU\s?=\s?[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}, CN\s?=\s?(?<fqdn>.*)$"#
+ );
+}
+
pub const ROUTER: Router = Router::new()
.get(&list_subdirs_api_method!(SUBDIRS))
.subdirs(SUBDIRS);
@@ -319,6 +329,40 @@ pub fn cert_expires_soon() -> Result<bool, Error> {
.map_err(|err| format_err!("Failed to check certificate expiration date: {}", err))
}
+/// Check whether the current certificate is self-signed and returns the remaining days the
+/// certificate is valid for.
+pub fn self_signed_cert_expires_in() -> Result<Option<i32>, Error> {
+ let cert = pem_to_cert_info(get_certificate_pem()?.as_bytes())?;
+ let cert_issuer = cert.issuer_name()?;
+
+ let Some(captures) = (SELF_SIGNED_REGEX.regex_obj)().captures(&cert_issuer) else {
+ return Ok(None);
+ };
+
+ let mut fqdn = proxmox_sys::nodename().to_owned();
+ let resolv_conf = crate::api2::node::dns::read_etc_resolv_conf()?;
+
+ if let Some(domain) = resolv_conf["search"].as_str() {
+ fqdn.push('.');
+ fqdn.push_str(domain);
+ }
+
+ if captures["fqdn"] != fqdn {
+ return Ok(None);
+ }
+
+ let now = Asn1Time::from_unix(proxmox_time::epoch_i64())?;
+ let diff = now.diff(cert.not_after())?;
+ Ok(Some(diff.days))
+}
+
+/// Renews the self signed certificate. The caller needs to make sure the current certificate is
+/// really a self-signed certificate and not an ACME or custom certificate.
+pub async fn renew_self_signed_cert() -> Result<(), Error> {
+ crate::config::update_self_signed_cert(true)?;
+ crate::server::reload_proxy_certificate().await
+}
+
fn spawn_certificate_worker(
name: &'static str,
force: bool,
diff --git a/src/bin/proxmox-daily-update.rs b/src/bin/proxmox-daily-update.rs
index c4d68e30..49159d24 100644
--- a/src/bin/proxmox-daily-update.rs
+++ b/src/bin/proxmox-daily-update.rs
@@ -1,6 +1,9 @@
use anyhow::Error;
use serde_json::json;
+use proxmox_backup::server::notifications::{
+ send_self_signed_renewal_notification, send_upcoming_self_signed_renewal_notification,
+};
use proxmox_notify::context::pbs::PBS_CONTEXT;
use proxmox_router::{cli::*, ApiHandler, RpcEnvironment};
use proxmox_subscription::SubscriptionStatus;
@@ -61,6 +64,10 @@ async fn do_update(rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> {
log::error!("error checking certificates: {err}");
}
+ if let Err(err) = renew_self_signed_certificate().await {
+ log::error!("error checking self-signed certificate renewal: {err:#}");
+ }
+
// TODO: cleanup tasks like in PVE?
Ok(())
@@ -89,6 +96,27 @@ async fn check_acme_certificates(rpcenv: &mut dyn RpcEnvironment) -> Result<(),
Ok(())
}
+async fn renew_self_signed_certificate() -> Result<(), Error> {
+ let days = match api2::node::certificates::self_signed_cert_expires_in()? {
+ None => {
+ log::debug!("Certificate is not self-signed, nothing to do.");
+ return Ok(());
+ }
+ Some(days) => days,
+ };
+
+ if days <= 15 {
+ log::info!("Certificate expires within 15 days, renewing certificate...");
+ let err = &api2::node::certificates::renew_self_signed_cert().await;
+ send_self_signed_renewal_notification(&err)?;
+ } else if days <= 30 {
+ log::info!("Certificate expires within 30 days, notify about renewal.");
+ send_upcoming_self_signed_renewal_notification()?;
+ }
+
+ Ok(())
+}
+
async fn run(rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> {
let backup_user = pbs_config::backup_user()?;
let file_opts = CreateOptions::new()
diff --git a/src/config/mod.rs b/src/config/mod.rs
index 56907ae9..4712fb1d 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -14,6 +14,8 @@ use pbs_buildcfg::{self, configdir};
pub mod tfa;
+pub(crate) const PRODUCT_NAME: &str = "Proxmox Backup Server";
+
/// Check configuration directory permissions
///
/// For security reasons, we want to make sure they are set correctly:
@@ -89,7 +91,7 @@ pub fn update_self_signed_cert(force: bool) -> Result<(), Error> {
let resolv_conf = crate::api2::node::dns::read_etc_resolv_conf()?;
let (priv_key, cert) = proxmox_acme_api::create_self_signed_cert(
- "Proxmox Backup Server",
+ PRODUCT_NAME,
proxmox_sys::nodename(),
resolv_conf["search"].as_str(),
None,
diff --git a/src/server/notifications/mod.rs b/src/server/notifications/mod.rs
index 95ff9ef1..2cbe3aee 100644
--- a/src/server/notifications/mod.rs
+++ b/src/server/notifications/mod.rs
@@ -575,6 +575,56 @@ pub fn send_certificate_renewal_mail(result: &Result<(), Error>) -> Result<(), E
Ok(())
}
+/// send email for upcoming self signed renewal.
+pub fn send_upcoming_self_signed_renewal_notification() -> Result<(), Error> {
+ let metadata = HashMap::from([
+ ("hostname".into(), proxmox_sys::nodename().into()),
+ ("type".into(), "cert".into()),
+ ]);
+
+ let notification = Notification::from_template(
+ Severity::Info,
+ "cert-upcoming-refresh",
+ serde_json::to_value(CommonData::new())?,
+ metadata,
+ );
+
+ send_notification(notification)?;
+ Ok(())
+}
+
+/// send email renewed self-signed certificate
+pub fn send_self_signed_renewal_notification(result: &Result<(), Error>) -> Result<(), Error> {
+ let metadata = HashMap::from([
+ ("hostname".into(), proxmox_sys::nodename().into()),
+ ("type".into(), "acme".into()),
+ ]);
+
+ let notification = match result {
+ Err(e) => {
+ let template_data = AcmeErrTemplateData {
+ common: CommonData::new(),
+ error: format!("{e:#}"),
+ };
+
+ Notification::from_template(
+ Severity::Info,
+ "acme-err",
+ serde_json::to_value(template_data)?,
+ metadata,
+ )
+ }
+ _ => Notification::from_template(
+ Severity::Info,
+ "cert-refresh",
+ serde_json::to_value(CommonData::new())?,
+ metadata,
+ ),
+ };
+
+ send_notification(notification)
+}
+
/// Lookup users email address
pub fn lookup_user_email(userid: &Userid) -> Option<String> {
if let Ok(user_config) = pbs_config::user::cached_config() {
diff --git a/templates/Makefile b/templates/Makefile
index 0539902e..45929f72 100644
--- a/templates/Makefile
+++ b/templates/Makefile
@@ -1,34 +1,38 @@
include ../defines.mk
-NOTIFICATION_TEMPLATES= \
- default/acme-err-body.txt.hbs \
- default/acme-err-subject.txt.hbs \
- default/gc-err-body.txt.hbs \
- default/gc-ok-body.txt.hbs \
- default/gc-err-subject.txt.hbs \
- default/gc-ok-subject.txt.hbs \
- default/package-updates-body.txt.hbs \
- default/package-updates-subject.txt.hbs \
- default/prune-err-body.txt.hbs \
- default/prune-ok-body.txt.hbs \
- default/prune-err-subject.txt.hbs \
- default/prune-ok-subject.txt.hbs \
- default/sync-err-body.txt.hbs \
- default/sync-ok-body.txt.hbs \
- default/sync-err-subject.txt.hbs \
- default/sync-ok-subject.txt.hbs \
- default/tape-backup-err-body.txt.hbs \
- default/tape-backup-err-subject.txt.hbs \
- default/tape-backup-ok-body.txt.hbs \
- default/tape-backup-ok-subject.txt.hbs \
- default/tape-load-body.txt.hbs \
- default/tape-load-subject.txt.hbs \
- default/test-body.txt.hbs \
- default/test-subject.txt.hbs \
- default/verify-err-body.txt.hbs \
- default/verify-ok-body.txt.hbs \
- default/verify-err-subject.txt.hbs \
- default/verify-ok-subject.txt.hbs \
+NOTIFICATION_TEMPLATES= \
+ default/acme-err-body.txt.hbs \
+ default/acme-err-subject.txt.hbs \
+ default/cert-refresh-body.txt.hbs \
+ default/cert-refresh-subject.txt.hbs \
+ default/cert-upcoming-refresh-body.txt.hbs \
+ default/cert-upcoming-refresh-subject.txt.hbs \
+ default/gc-err-body.txt.hbs \
+ default/gc-ok-body.txt.hbs \
+ default/gc-err-subject.txt.hbs \
+ default/gc-ok-subject.txt.hbs \
+ default/package-updates-body.txt.hbs \
+ default/package-updates-subject.txt.hbs \
+ default/prune-err-body.txt.hbs \
+ default/prune-ok-body.txt.hbs \
+ default/prune-err-subject.txt.hbs \
+ default/prune-ok-subject.txt.hbs \
+ default/sync-err-body.txt.hbs \
+ default/sync-ok-body.txt.hbs \
+ default/sync-err-subject.txt.hbs \
+ default/sync-ok-subject.txt.hbs \
+ default/tape-backup-err-body.txt.hbs \
+ default/tape-backup-err-subject.txt.hbs \
+ default/tape-backup-ok-body.txt.hbs \
+ default/tape-backup-ok-subject.txt.hbs \
+ default/tape-load-body.txt.hbs \
+ default/tape-load-subject.txt.hbs \
+ default/test-body.txt.hbs \
+ default/test-subject.txt.hbs \
+ default/verify-err-body.txt.hbs \
+ default/verify-ok-body.txt.hbs \
+ default/verify-err-subject.txt.hbs \
+ default/verify-ok-subject.txt.hbs \
all:
diff --git a/templates/default/cert-refresh-body.txt.hbs b/templates/default/cert-refresh-body.txt.hbs
new file mode 100644
index 00000000..608ba30c
--- /dev/null
+++ b/templates/default/cert-refresh-body.txt.hbs
@@ -0,0 +1,8 @@
+Proxmox Backup Server has refreshed its self-signed TLS certificate.
+
+The new certificate is now active. Please update any clients relying on the
+certificate fingerprint to verify their connection to the server.
+
+Please visit the web interface for further details:
+
+<{{base-url}}/#pbsCertificateConfiguration>
diff --git a/templates/default/cert-refresh-subject.txt.hbs b/templates/default/cert-refresh-subject.txt.hbs
new file mode 100644
index 00000000..e57f5cd4
--- /dev/null
+++ b/templates/default/cert-refresh-subject.txt.hbs
@@ -0,0 +1 @@
+Self-Signed Certificate Has Been Refreshed
diff --git a/templates/default/cert-upcoming-refresh-body.txt.hbs b/templates/default/cert-upcoming-refresh-body.txt.hbs
new file mode 100644
index 00000000..8d199c9f
--- /dev/null
+++ b/templates/default/cert-upcoming-refresh-body.txt.hbs
@@ -0,0 +1,9 @@
+Proxmox Backup Server will refresh its TLS certificate within the next 30 days.
+
+If you rely on the certificate's fingerprint to verify TLS sessions between the
+server and a client, please update the fingerprint once the certificate was
+updated. Otherwise, no action is required.
+
+Please visit the web interface for further details:
+
+<{{base-url}}/#pbsCertificateConfiguration>
diff --git a/templates/default/cert-upcoming-refresh-subject.txt.hbs b/templates/default/cert-upcoming-refresh-subject.txt.hbs
new file mode 100644
index 00000000..f188e391
--- /dev/null
+++ b/templates/default/cert-upcoming-refresh-subject.txt.hbs
@@ -0,0 +1 @@
+Self-signed Certificate is About to be Refreshed
--
2.47.3
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH proxmox-backup 05/10] daily-update/docs: warn on excessive self-signed certificate lifetime
2026-04-07 13:57 [RFC datacenter-manager/proxmox{,-backup} 00/10] TLS Certificate Rotation Shannon Sterz
` (3 preceding siblings ...)
2026-04-07 13:57 ` [PATCH proxmox-backup 04/10] config/server/api: add certificate renewal logic including notifications Shannon Sterz
@ 2026-04-07 13:57 ` Shannon Sterz
2026-04-07 13:57 ` [PATCH proxmox-backup 06/10] backup-manager cli: `cert update` can create auth and csrf key Shannon Sterz
` (4 subsequent siblings)
9 siblings, 0 replies; 12+ messages in thread
From: Shannon Sterz @ 2026-04-07 13:57 UTC (permalink / raw)
To: pbs-devel
and document how to renew it. an excessive lifetime is reported when
the lifetime of the certificate exceeds 3650 days (almost ten years),
which corresponds to the default lifetime generated by
proxmox-acme-api.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
docs/certificate-management.rst | 31 +++++++++++++++++++++++++++++++
src/bin/proxmox-daily-update.rs | 4 ++++
2 files changed, 35 insertions(+)
diff --git a/docs/certificate-management.rst b/docs/certificate-management.rst
index 903d0efc..b08133d5 100644
--- a/docs/certificate-management.rst
+++ b/docs/certificate-management.rst
@@ -333,3 +333,34 @@ Test your new certificate, using your browser.
.. [1]
acme.sh https://github.com/acmesh-official/acme.sh
+
+Manually Renew Self-signed Certificates
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Proxmox Backup Server creates and renews a self-signed certificate if no custom
+or ACME certificate is provided. Older versions issued a certificate that was
+valid for almost 1000 years and did not renew this certificate. Beginning with
+version 4.2, new setups use shorter lived certificates that will be regularly
+renewed. Old self-signed certificates are not replaced in order to not disrupt
+existing backup setups. In such cases, the following line is logged:
+
+.. code-block:: console
+
+ Apr 04 12:17:51 pbs proxmox-daily-update[1170]: Self-signed certificate is valid for an excessive amount of time. Please renew it.
+
+To manually renew a certificate, navigate to Configuration -> Certificates.
+Select the certificate ``proxy.pem``. Then click the "Delete Custom
+Certificate" button. Alternatively, you can run the following command:
+
+.. code-block:: shell
+
+ proxmox-backup-manager cert update --force
+
+.. WARNING:: Any client using a fingerprint to verify TLS sessions with the
+ server will need to be updated with the new fingerprint. This includes any
+ Proxmox VE instance that may use it as a backup destination.
+
+After manually renewing the certificate once, Proxmox Backup Server will start
+renewing the certificate itself. A certificate will be renewed at the earliest
+15 days before it expires. Starting from 30 days before it expires,
+notifications will be issued with a reminder about the upcoming renewal.
diff --git a/src/bin/proxmox-daily-update.rs b/src/bin/proxmox-daily-update.rs
index 49159d24..43d35f4a 100644
--- a/src/bin/proxmox-daily-update.rs
+++ b/src/bin/proxmox-daily-update.rs
@@ -112,6 +112,10 @@ async fn renew_self_signed_certificate() -> Result<(), Error> {
} else if days <= 30 {
log::info!("Certificate expires within 30 days, notify about renewal.");
send_upcoming_self_signed_renewal_notification()?;
+ } else if days > 365 * 10 {
+ log::warn!(
+ "Self-signed certificate is valid for an excessive amount of time. Please renew it."
+ );
}
Ok(())
--
2.47.3
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH proxmox-backup 06/10] backup-manager cli: `cert update` can create auth and csrf key
2026-04-07 13:57 [RFC datacenter-manager/proxmox{,-backup} 00/10] TLS Certificate Rotation Shannon Sterz
` (4 preceding siblings ...)
2026-04-07 13:57 ` [PATCH proxmox-backup 05/10] daily-update/docs: warn on excessive self-signed certificate lifetime Shannon Sterz
@ 2026-04-07 13:57 ` Shannon Sterz
2026-04-07 13:57 ` [PATCH datacenter-manager 07/10] certs: adapt to api change in proxmox_acme_api, add expiry paramter Shannon Sterz
` (3 subsequent siblings)
9 siblings, 0 replies; 12+ messages in thread
From: Shannon Sterz @ 2026-04-07 13:57 UTC (permalink / raw)
To: pbs-devel
when these don't exist yet, document that behavior.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
src/bin/proxmox_backup_manager/cert.rs | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/bin/proxmox_backup_manager/cert.rs b/src/bin/proxmox_backup_manager/cert.rs
index bfc08d3d..9addb982 100644
--- a/src/bin/proxmox_backup_manager/cert.rs
+++ b/src/bin/proxmox_backup_manager/cert.rs
@@ -56,6 +56,8 @@ fn cert_info() -> Result<(), Error> {
},
)]
/// Update node certificates and generate all needed files/directories.
+/// If either the authentication key or CSRF secret key does not exist, each will be generated.
+/// These two keys will go into effect the next time the `proxmox-backup.service` is started.
fn update_certs(force: Option<bool>) -> Result<(), Error> {
config::create_configdir()?;
--
2.47.3
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH datacenter-manager 07/10] certs: adapt to api change in proxmox_acme_api, add expiry paramter
2026-04-07 13:57 [RFC datacenter-manager/proxmox{,-backup} 00/10] TLS Certificate Rotation Shannon Sterz
` (5 preceding siblings ...)
2026-04-07 13:57 ` [PATCH proxmox-backup 06/10] backup-manager cli: `cert update` can create auth and csrf key Shannon Sterz
@ 2026-04-07 13:57 ` Shannon Sterz
2026-04-07 13:57 ` [PATCH datacenter-manager 08/10] api/auth/bin: add certificate renewal logic Shannon Sterz
` (2 subsequent siblings)
9 siblings, 0 replies; 12+ messages in thread
From: Shannon Sterz @ 2026-04-07 13:57 UTC (permalink / raw)
To: pbs-devel
using `None` means that we use the default expiry of 3650 days, almost
ten years.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
server/src/auth/certs.rs | 1 +
1 file changed, 1 insertion(+)
diff --git a/server/src/auth/certs.rs b/server/src/auth/certs.rs
index 542c142..c310593 100644
--- a/server/src/auth/certs.rs
+++ b/server/src/auth/certs.rs
@@ -23,6 +23,7 @@ pub fn update_self_signed_cert(force: bool) -> Result<(), Error> {
"Proxmox Datacenter Manager",
proxmox_sys::nodename(),
resolv_conf.search.as_deref(),
+ None,
)?;
let cert_pem = cert.to_pem()?;
--
2.47.3
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH datacenter-manager 08/10] api/auth/bin: add certificate renewal logic
2026-04-07 13:57 [RFC datacenter-manager/proxmox{,-backup} 00/10] TLS Certificate Rotation Shannon Sterz
` (6 preceding siblings ...)
2026-04-07 13:57 ` [PATCH datacenter-manager 07/10] certs: adapt to api change in proxmox_acme_api, add expiry paramter Shannon Sterz
@ 2026-04-07 13:57 ` Shannon Sterz
2026-04-07 13:57 ` [PATCH datacenter-manager 09/10] cli: expose certificate management endpoints via the cli Shannon Sterz
2026-04-07 13:57 ` [PATCH datacenter-manager 10/10] daily-update/docs: warn on excessive tls certificate validity periods Shannon Sterz
9 siblings, 0 replies; 12+ messages in thread
From: Shannon Sterz @ 2026-04-07 13:57 UTC (permalink / raw)
To: pbs-devel
the daily-update service is used to check whether a self-signed
certificate is in use and renews it if it would expire within the next
15 days.
a self-signed certificate is detected by parsing the issuer field of
the certificate. if it aligns with how self-signed certificates are
created by this instance of proxmox datacenter manager, it will be
deemed self-signed.
15 days was chosen as that should give a reasonable trade-off between
not failing if the daily-update service can't run on the specific day
that the certificate would expire and not refreshing the certificate
too often. note that 15 days before expiry corresponds to let's
encrypt's recommendation for when to update new certificates that are
valid for 45 days:
> Acceptable behavior includes renewing certificates at approximately
> two thirds of the way through the current certificate’s lifetime.
>
> - https://letsencrypt.org/2025/12/02/from-90-to-45#action-required
15 days before expiry is about two thirds of the lifetime of a
certificate that lasts for 45 days.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
server/Cargo.toml | 1 +
server/src/api/nodes/certificates.rs | 48 +++++++++++++++++++
server/src/auth/certs.rs | 3 +-
...proxmox-datacenter-manager-daily-update.rs | 26 ++++++++++
4 files changed, 77 insertions(+), 1 deletion(-)
diff --git a/server/Cargo.toml b/server/Cargo.toml
index 6969549..0874acd 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -25,6 +25,7 @@ nix.workspace = true
once_cell.workspace = true
openssl.workspace = true
percent-encoding.workspace = true
+regex.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_plain.workspace = true
diff --git a/server/src/api/nodes/certificates.rs b/server/src/api/nodes/certificates.rs
index 47aef7a..3b43f2e 100644
--- a/server/src/api/nodes/certificates.rs
+++ b/server/src/api/nodes/certificates.rs
@@ -1,7 +1,10 @@
use anyhow::{bail, format_err, Context, Error};
+use const_format::concatcp;
+use openssl::asn1::Asn1Time;
use openssl::pkey::PKey;
use openssl::x509::X509;
+use proxmox_dns_api::read_etc_resolv_conf;
use proxmox_log::info;
use proxmox_router::list_subdirs_api_method;
use proxmox_router::SubdirMap;
@@ -17,6 +20,14 @@ use pdm_api_types::PRIV_SYS_MODIFY;
use crate::auth::certs::{API_CERT_FN, API_KEY_FN};
+proxmox_schema::const_regex! {
+ pub SELF_SIGNED_REGEX = concatcp!(
+ r#"^O\s?=\s?"#,
+ crate::auth::certs::PRODUCT_NAME,
+ r#", OU\s?=\s?[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}, CN\s?=\s?(?<fqdn>.*)$"#
+ );
+}
+
pub const ROUTER: Router = Router::new()
.get(&list_subdirs_api_method!(SUBDIRS))
.subdirs(SUBDIRS);
@@ -247,6 +258,43 @@ pub fn cert_expires_soon() -> Result<bool, Error> {
.map_err(|err| format_err!("Failed to check certificate expiration date: {}", err))
}
+/// Check whether the current certificate is self-signed and returns the remaining days the
+/// certificate is valid for.
+pub fn self_signed_cert_expires_in() -> Result<Option<i32>, Error> {
+ let cert = get_certificate_info()?;
+
+ let Some(captures) = (SELF_SIGNED_REGEX.regex_obj)().captures(&cert.issuer) else {
+ return Ok(None);
+ };
+
+ let mut fqdn = proxmox_sys::nodename().to_owned();
+ let resolv_conf = read_etc_resolv_conf(None)?.config;
+
+ if let Some(domain) = resolv_conf.search {
+ fqdn.push('.');
+ fqdn.push_str(&domain);
+ }
+
+ if captures["fqdn"] != fqdn {
+ return Ok(None);
+ }
+
+ let now = Asn1Time::from_unix(proxmox_time::epoch_i64())?;
+ let not_after = Asn1Time::from_unix(cert.notafter.ok_or_else(|| {
+ format_err!("Could not get \"not after\" epoch for current certificate.")
+ })?)?;
+
+ let diff = now.diff(¬_after)?;
+ Ok(Some(diff.days))
+}
+
+/// Renews the self signed certificate. The caller needs to make sure the current certificate is
+/// really a self-signed certificate and not an ACME or custom certificate.
+pub async fn renew_self_signed_cert() -> Result<(), Error> {
+ crate::auth::certs::update_self_signed_cert(true)?;
+ crate::reload_api_certificate().await
+}
+
fn spawn_certificate_worker(
name: &'static str,
force: bool,
diff --git a/server/src/auth/certs.rs b/server/src/auth/certs.rs
index c310593..ea561d0 100644
--- a/server/src/auth/certs.rs
+++ b/server/src/auth/certs.rs
@@ -7,6 +7,7 @@ use pdm_buildcfg::configdir;
pub const API_KEY_FN: &str = configdir!("/auth/api.key");
pub const API_CERT_FN: &str = configdir!("/auth/api.pem");
+pub const PRODUCT_NAME: &str = "Proxmox Datacenter Manager";
/// Update self signed node certificate.
pub fn update_self_signed_cert(force: bool) -> Result<(), Error> {
@@ -20,7 +21,7 @@ pub fn update_self_signed_cert(force: bool) -> Result<(), Error> {
let resolv_conf = read_etc_resolv_conf(None)?.config;
let (priv_key, cert) = proxmox_acme_api::create_self_signed_cert(
- "Proxmox Datacenter Manager",
+ PRODUCT_NAME,
proxmox_sys::nodename(),
resolv_conf.search.as_deref(),
None,
diff --git a/server/src/bin/proxmox-datacenter-manager-daily-update.rs b/server/src/bin/proxmox-datacenter-manager-daily-update.rs
index 8b6641c..deed0be 100644
--- a/server/src/bin/proxmox-datacenter-manager-daily-update.rs
+++ b/server/src/bin/proxmox-datacenter-manager-daily-update.rs
@@ -59,6 +59,11 @@ async fn do_update(rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> {
log::error!("error checking certificates: {err}");
}
+ println!("check if self-signed certificate requires renewal");
+ if let Err(err) = renew_self_signed_certificate().await {
+ log::error!("error checking self-signed certificate renewal: {err:#}");
+ }
+
// TODO: cleanup tasks like in PVE?
Ok(())
@@ -87,6 +92,27 @@ async fn check_acme_certificates(rpcenv: &mut dyn RpcEnvironment) -> Result<(),
Ok(())
}
+async fn renew_self_signed_certificate() -> Result<(), Error> {
+ let days = match api::nodes::certificates::self_signed_cert_expires_in()? {
+ None => {
+ log::debug!("Certificate is not self-signed, nothing to do.");
+ return Ok(());
+ }
+ Some(days) => days,
+ };
+
+ if days <= 15 {
+ log::info!("Certificate expires within 15 days, renewing certificate...");
+ let _err = &api::nodes::certificates::renew_self_signed_cert().await;
+ // fixme: send_self_signed_renewal_notification(&err)?;
+ } else if days <= 30 {
+ log::info!("Certificate expires within 30 days.");
+ // fixme: send_upcoming_self_signed_renewal_notification()?;
+ }
+
+ Ok(())
+}
+
async fn run(rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> {
let api_user = pdm_config::api_user()?;
let file_opts = CreateOptions::new().owner(api_user.uid).group(api_user.gid);
--
2.47.3
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH datacenter-manager 09/10] cli: expose certificate management endpoints via the cli
2026-04-07 13:57 [RFC datacenter-manager/proxmox{,-backup} 00/10] TLS Certificate Rotation Shannon Sterz
` (7 preceding siblings ...)
2026-04-07 13:57 ` [PATCH datacenter-manager 08/10] api/auth/bin: add certificate renewal logic Shannon Sterz
@ 2026-04-07 13:57 ` Shannon Sterz
2026-04-07 13:57 ` [PATCH datacenter-manager 10/10] daily-update/docs: warn on excessive tls certificate validity periods Shannon Sterz
9 siblings, 0 replies; 12+ messages in thread
From: Shannon Sterz @ 2026-04-07 13:57 UTC (permalink / raw)
To: pbs-devel
analogous to how this is handled in proxmox-backup-server.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
cli/admin/Cargo.toml | 2 +
cli/admin/src/cert.rs | 86 +++++++++++++++++++++++++++++++++++++++++++
cli/admin/src/main.rs | 2 +
3 files changed, 90 insertions(+)
create mode 100644 cli/admin/src/cert.rs
diff --git a/cli/admin/Cargo.toml b/cli/admin/Cargo.toml
index 01afc88..7aa0d2e 100644
--- a/cli/admin/Cargo.toml
+++ b/cli/admin/Cargo.toml
@@ -10,6 +10,7 @@ repository.workspace = true
[dependencies]
anyhow.workspace = true
+openssl.workspace = true
serde.workspace = true
serde_json.workspace = true
@@ -29,3 +30,4 @@ pdm-api-types.workspace = true
pdm-config.workspace = true
pdm-buildcfg.workspace = true
server.workspace = true
+proxmox-time.workspace = true
diff --git a/cli/admin/src/cert.rs b/cli/admin/src/cert.rs
new file mode 100644
index 0000000..358ab56
--- /dev/null
+++ b/cli/admin/src/cert.rs
@@ -0,0 +1,86 @@
+use anyhow::{bail, Error};
+
+use proxmox_router::cli::*;
+use proxmox_schema::api;
+use server::api;
+use server::auth::certs::update_self_signed_cert;
+use server::auth::csrf::generate_csrf_key;
+use server::auth::key::generate_auth_key;
+
+#[api]
+/// Display node certificate information.
+fn cert_info() -> Result<(), Error> {
+ let Some(cert) = api::nodes::certificates::get_info()?.pop() else {
+ return Ok(());
+ };
+
+ println!("Subject: {}", cert.subject);
+
+ for name in cert.san {
+ println!(" {name}");
+ }
+
+ let not_before = cert
+ .notbefore
+ .and_then(|e| proxmox_time::strftime_utc("%b %e %T %Y %Z", e).ok());
+
+ let not_after = cert
+ .notafter
+ .and_then(|e| proxmox_time::strftime_utc("%b %e %T %Y %Z", e).ok());
+
+ println!("Issuer: {}", cert.issuer);
+ println!("Validity:");
+ println!(" Not Before: {}", not_before.unwrap_or_default());
+ println!(" Not After : {}", not_after.unwrap_or_default());
+
+ println!(
+ "Fingerprint (sha256): {}",
+ cert.fingerprint.unwrap_or_default()
+ );
+
+ println!("Public key type: {}", cert.public_key_type);
+ println!(
+ "Public key bits: {}",
+ cert.public_key_bits.unwrap_or_default()
+ );
+
+ Ok(())
+}
+
+#[api(
+ input: {
+ properties: {
+ force: {
+ description: "Force generation of new SSL certificate.",
+ type: Boolean,
+ optional:true,
+ },
+ }
+ },
+)]
+/// Update node certificates and generate all needed files/directories.
+/// If no authentication key or CSRF secret key exist, this will also generate new ones. These two
+/// keys will go into effect the next time the `proxmox-backup.service` is started.
+fn update_certs(force: Option<bool>) -> Result<(), Error> {
+ pdm_config::setup::create_configdir()?;
+
+ if let Err(err) = generate_auth_key() {
+ bail!("unable to generate auth key - {err}");
+ }
+
+ if let Err(err) = generate_csrf_key() {
+ bail!("unable to generate csrf key - {err}");
+ }
+
+ update_self_signed_cert(force.unwrap_or(false))?;
+
+ Ok(())
+}
+
+pub fn cert_mgmt_cli() -> CommandLineInterface {
+ let cmd_def = CliCommandMap::new()
+ .insert("info", CliCommand::new(&API_METHOD_CERT_INFO))
+ .insert("update", CliCommand::new(&API_METHOD_UPDATE_CERTS));
+
+ cmd_def.into()
+}
diff --git a/cli/admin/src/main.rs b/cli/admin/src/main.rs
index 7f0b339..66d5423 100644
--- a/cli/admin/src/main.rs
+++ b/cli/admin/src/main.rs
@@ -12,6 +12,7 @@ use proxmox_schema::api;
use proxmox_sys::fs::CreateOptions;
mod acme;
+mod cert;
mod remotes;
mod support_status;
@@ -38,6 +39,7 @@ async fn run() -> Result<(), Error> {
let cmd_def = CliCommandMap::new()
.insert("acme", acme::acme_mgmt_cli())
+ .insert("cert", cert::cert_mgmt_cli())
.insert("remote", remotes::cli())
.insert(
"report",
--
2.47.3
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH datacenter-manager 10/10] daily-update/docs: warn on excessive tls certificate validity periods
2026-04-07 13:57 [RFC datacenter-manager/proxmox{,-backup} 00/10] TLS Certificate Rotation Shannon Sterz
` (8 preceding siblings ...)
2026-04-07 13:57 ` [PATCH datacenter-manager 09/10] cli: expose certificate management endpoints via the cli Shannon Sterz
@ 2026-04-07 13:57 ` Shannon Sterz
2026-04-07 15:29 ` Shannon Sterz
9 siblings, 1 reply; 12+ messages in thread
From: Shannon Sterz @ 2026-04-07 13:57 UTC (permalink / raw)
To: pbs-devel
and document how to update the certificate manually. an excessive
lifetime is reported when the lifetime of the certificate exceeds 3650
days (almost ten years), which corresponds to the default lifetime
generated by proxmox-acme-api.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
docs/certificate-management.rst | 31 +++++++++++++++++++
...proxmox-datacenter-manager-daily-update.rs | 4 +++
2 files changed, 35 insertions(+)
diff --git a/docs/certificate-management.rst b/docs/certificate-management.rst
index 652f6ca..a9a12cf 100644
--- a/docs/certificate-management.rst
+++ b/docs/certificate-management.rst
@@ -303,3 +303,34 @@ Test your new certificate, using your browser.
.. [1]
acme.sh https://github.com/acmesh-official/acme.sh
+
+Manually Renew Self-signed Certificates
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Proxmox Datacenter Manager creates and renews a self-signed certificate if no
+custom or ACME certificate is provided. Older versions issued a certificate
+that was valid for almost 1000 years and did not renew this certificate.
+Beginning with version 1.1, new setups use shorter lived certificates that will
+be regularly renewed. Old self-signed certificates are not replaced in order to
+not disrupt existing backup setups. In such cases, the following line is
+logged:
+
+.. code-block:: console
+
+ Apr 04 12:17:51 pdm proxmox-datacenter-manager-daily-update[1170]: Self-signed certificate is valid for an excessive amount of time. Please renew it.
+
+To manually renew a certificate, navigate to Configuration -> Certificates.
+Select the certificate ``proxy.pem``. Then click the "Delete Custom
+Certificate" button. Alternatively, you can run the following command:
+
+.. code-block:: shell
+
+ proxmox-datacenter-manager-admin cert update --force
+
+.. WARNING:: Any client using a fingerprint to verify TLS sessions with the
+ server will need to be updated with the new fingerprint.
+
+After manually renewing the certificate once, Proxmox Datacenter Manager will
+start renewing the certificate itself. A certificate will be renewed at the
+earliest 15 days before it expires. Starting from 30 days before it expires,
+notifications will be issued with a reminder about the upcoming renewal.
diff --git a/server/src/bin/proxmox-datacenter-manager-daily-update.rs b/server/src/bin/proxmox-datacenter-manager-daily-update.rs
index deed0be..8e5f67a 100644
--- a/server/src/bin/proxmox-datacenter-manager-daily-update.rs
+++ b/server/src/bin/proxmox-datacenter-manager-daily-update.rs
@@ -108,6 +108,10 @@ async fn renew_self_signed_certificate() -> Result<(), Error> {
} else if days <= 30 {
log::info!("Certificate expires within 30 days.");
// fixme: send_upcoming_self_signed_renewal_notification()?;
+ } else if days > 365 * 10 {
+ log::warn!(
+ "Self-signed certificate is valid for an excessive amount of time. Please renew it."
+ );
}
Ok(())
--
2.47.3
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH datacenter-manager 10/10] daily-update/docs: warn on excessive tls certificate validity periods
2026-04-07 13:57 ` [PATCH datacenter-manager 10/10] daily-update/docs: warn on excessive tls certificate validity periods Shannon Sterz
@ 2026-04-07 15:29 ` Shannon Sterz
0 siblings, 0 replies; 12+ messages in thread
From: Shannon Sterz @ 2026-04-07 15:29 UTC (permalink / raw)
To: Shannon Sterz, pbs-devel
On Tue Apr 7, 2026 at 3:57 PM CEST, Shannon Sterz wrote:
> and document how to update the certificate manually. an excessive
> lifetime is reported when the lifetime of the certificate exceeds 3650
> days (almost ten years), which corresponds to the default lifetime
> generated by proxmox-acme-api.
>
> Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
> ---
> docs/certificate-management.rst | 31 +++++++++++++++++++
> ...proxmox-datacenter-manager-daily-update.rs | 4 +++
> 2 files changed, 35 insertions(+)
>
> diff --git a/docs/certificate-management.rst b/docs/certificate-management.rst
> index 652f6ca..a9a12cf 100644
> --- a/docs/certificate-management.rst
> +++ b/docs/certificate-management.rst
> @@ -303,3 +303,34 @@ Test your new certificate, using your browser.
>
> .. [1]
> acme.sh https://github.com/acmesh-official/acme.sh
> +
> +Manually Renew Self-signed Certificates
> +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
> +
> +Proxmox Datacenter Manager creates and renews a self-signed certificate if no
> +custom or ACME certificate is provided. Older versions issued a certificate
> +that was valid for almost 1000 years and did not renew this certificate.
> +Beginning with version 1.1, new setups use shorter lived certificates that will
> +be regularly renewed. Old self-signed certificates are not replaced in order to
> +not disrupt existing backup setups. In such cases, the following line is
the "backup" here should be dropped ofc. i'll include that in the next
version. sorry for any inconvenience.
> +logged:
> +
> +.. code-block:: console
> +
> + Apr 04 12:17:51 pdm proxmox-datacenter-manager-daily-update[1170]: Self-signed certificate is valid for an excessive amount of time. Please renew it.
> +
> +To manually renew a certificate, navigate to Configuration -> Certificates.
> +Select the certificate ``proxy.pem``. Then click the "Delete Custom
on a sidenote: i just noticed that in pdm the file is actually
`/etc/proxmox-datacenter-manager/auth/api.pem`. `proxy.pem` is still
used here because we hard-code the certificate name [1,2]. i'd add
patches to fix that to in a next version of this series.
[1]: https://git.proxmox.com/?p=proxmox-datacenter-manager.git;a=blob;f=server/src/api/nodes/certificates.rs;h=47aef7ad;hb=HEAD#l51
[2]: https://git.proxmox.com/?p=ui/proxmox-yew-comp.git;a=blob;f=src/acme/certificate_list.rs;h=2553bac6;hb=HEAD#l125
> +Certificate" button. Alternatively, you can run the following command:
> +
> +.. code-block:: shell
> +
> + proxmox-datacenter-manager-admin cert update --force
> +
> +.. WARNING:: Any client using a fingerprint to verify TLS sessions with the
> + server will need to be updated with the new fingerprint.
> +
> +After manually renewing the certificate once, Proxmox Datacenter Manager will
> +start renewing the certificate itself. A certificate will be renewed at the
> +earliest 15 days before it expires. Starting from 30 days before it expires,
> +notifications will be issued with a reminder about the upcoming renewal.
> diff --git a/server/src/bin/proxmox-datacenter-manager-daily-update.rs b/server/src/bin/proxmox-datacenter-manager-daily-update.rs
> index deed0be..8e5f67a 100644
> --- a/server/src/bin/proxmox-datacenter-manager-daily-update.rs
> +++ b/server/src/bin/proxmox-datacenter-manager-daily-update.rs
> @@ -108,6 +108,10 @@ async fn renew_self_signed_certificate() -> Result<(), Error> {
> } else if days <= 30 {
> log::info!("Certificate expires within 30 days.");
> // fixme: send_upcoming_self_signed_renewal_notification()?;
> + } else if days > 365 * 10 {
> + log::warn!(
> + "Self-signed certificate is valid for an excessive amount of time. Please renew it."
> + );
> }
>
> Ok(())
^ permalink raw reply [flat|nested] 12+ messages in thread
end of thread, other threads:[~2026-04-07 15:29 UTC | newest]
Thread overview: 12+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-04-07 13:57 [RFC datacenter-manager/proxmox{,-backup} 00/10] TLS Certificate Rotation Shannon Sterz
2026-04-07 13:57 ` [PATCH proxmox 01/10] acme-api: make self-signed certificate expiry configurable Shannon Sterz
2026-04-07 13:57 ` [PATCH proxmox-backup 02/10] config: use proxmox_acme_api for generating self-signed certificates Shannon Sterz
2026-04-07 13:57 ` [PATCH proxmox-backup 03/10] config: adapt to api change in proxmox_acme_api, add expiry paramter Shannon Sterz
2026-04-07 13:57 ` [PATCH proxmox-backup 04/10] config/server/api: add certificate renewal logic including notifications Shannon Sterz
2026-04-07 13:57 ` [PATCH proxmox-backup 05/10] daily-update/docs: warn on excessive self-signed certificate lifetime Shannon Sterz
2026-04-07 13:57 ` [PATCH proxmox-backup 06/10] backup-manager cli: `cert update` can create auth and csrf key Shannon Sterz
2026-04-07 13:57 ` [PATCH datacenter-manager 07/10] certs: adapt to api change in proxmox_acme_api, add expiry paramter Shannon Sterz
2026-04-07 13:57 ` [PATCH datacenter-manager 08/10] api/auth/bin: add certificate renewal logic Shannon Sterz
2026-04-07 13:57 ` [PATCH datacenter-manager 09/10] cli: expose certificate management endpoints via the cli Shannon Sterz
2026-04-07 13:57 ` [PATCH datacenter-manager 10/10] daily-update/docs: warn on excessive tls certificate validity periods Shannon Sterz
2026-04-07 15:29 ` Shannon Sterz
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox