From: "Fabian Grünbichler" <f.gruenbichler@proxmox.com>
To: pdm-devel@lists.proxmox.com, Shannon Sterz <s.sterz@proxmox.com>
Subject: Re: [PATCH manager v2 03/16] bin/api: add a new staged certificate when renewing self-signed cert
Date: Thu, 06 Aug 2026 17:39:03 +0200 [thread overview]
Message-ID: <1786028636.awgbclb3om.astroid@yuna.none> (raw)
In-Reply-To: <20260805131838.254723-5-s.sterz@proxmox.com>
On August 5, 2026 3:18 pm, Shannon Sterz wrote:
> when a certificate is about to expire (within the next four weeks, up
> to two weeks before it expires), create a new staged "next"
> certificate. by including this certificate when querying the api,
> clients can prepare for when the certificate is about to be rotated.
> this allows them to update to the new fingerprint, without needing an
> additional out-of-band communication channel.
>
> Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
> ---
> PVE/API2/Certificates.pm | 5 ++-
> PVE/CertHelpers.pm | 6 ++++
> bin/pveupdate | 72 +++++++++++++++++++++++++++++++++-------
> 3 files changed, 70 insertions(+), 13 deletions(-)
>
> diff --git a/PVE/API2/Certificates.pm b/PVE/API2/Certificates.pm
> index de8762c53..bcd650c3b 100644
> --- a/PVE/API2/Certificates.pm
> +++ b/PVE/API2/Certificates.pm
> @@ -70,7 +70,10 @@ __PACKAGE__->register_method({
>
> my $res = [];
> my $cert_paths = [
> - '/etc/pve/pve-root-ca.pem', "$node_path/pve-ssl.pem", "$node_path/pveproxy-ssl.pem",
> + '/etc/pve/pve-root-ca.pem',
> + "$node_path/pve-ssl.pem",
> + "$node_path/pveproxy-ssl.pem",
> + "$node_path/pve-ssl-next.pem",
should we add some flags to the CertificateInfo?
e.g., the product code here knows which certificate has which purpose:
- CA
- signed-by-ca
- custom/acme
- currently-used
- staged
> ];
> for my $path (@$cert_paths) {
> eval {
> diff --git a/PVE/CertHelpers.pm b/PVE/CertHelpers.pm
> index 202dec0ee..4272eaf57 100644
> --- a/PVE/CertHelpers.pm
> +++ b/PVE/CertHelpers.pm
> @@ -55,6 +55,12 @@ sub default_cert_path_prefix {
> return "/etc/pve/nodes/${node}/pve-ssl";
> }
>
> +sub default_next_cert_path_prefix {
> + my ($node) = @_;
> +
> + return "/etc/pve/nodes/${node}/pve-ssl-next";
> +}
high-level question - do we want to rotate the key as well?
what about CA rotation? (#5215)
> +
> sub cert_lock {
> my ($timeout, $code, @param) = @_;
>
> diff --git a/bin/pveupdate b/bin/pveupdate
> index b1960c353..e44c614ee 100755
> --- a/bin/pveupdate
> +++ b/bin/pveupdate
> @@ -103,10 +103,12 @@ syslog('err', "Renewing ACME certificate failed: $@") if $@;
>
> eval {
> my $certpath = PVE::CertHelpers::default_cert_path_prefix($nodename) . ".pem";
> + my $next_certpath = PVE::CertHelpers::default_next_cert_path_prefix($nodename) . ".pem";
> my $capath = "/etc/pve/pve-root-ca.pem";
> + my $now = time();
>
> my $renew = sub {
> - my ($msg) = @_;
> + my ($msg, $use_later) = @_;
>
> # get CA info
> my $cainfo = PVE::Certificate::get_certificate_info($capath);
> @@ -127,21 +129,67 @@ eval {
> print "PVE certificate $msg\n";
> # create new certificate
> my $ip = PVE::Cluster::remote_node_ip($nodename);
> - PVE::Cluster::Setup::gen_pve_ssl_cert(1, $nodename, $ip);
>
> - print "Restarting pveproxy after renewing certificate\n";
> - PVE::Tools::run_command(['systemctl', 'reload-or-restart', 'pveproxy']);
> + if ($use_later) {
> + PVE::Cluster::Setup::gen_pve_ssl_cert(1, $nodename, $ip, $next_certpath);
> + } else {
> + PVE::Cluster::Setup::gen_pve_ssl_cert(1, $nodename, $ip);
> + print "Restarting pveproxy after renewing certificate\n";
> + PVE::Tools::run_command(['systemctl', 'reload-or-restart', 'pveproxy']);
> + }
> };
>
> - if (PVE::Certificate::check_expiry($certpath)) {
> - # already expired
> - $renew->("expired, renewing...");
> - } elsif (PVE::Certificate::check_expiry($certpath, time() + 14 * 24 * 60 * 60)) {
> - # expires in next 2 weeks
> - $renew->("expires soon, renewing...");
> - } elsif (!PVE::Certificate::check_expiry($certpath, time() + 2 * 365 * 24 * 60 * 60)) {
> + my $rotate_in_next_certificate_or_renew = sub {
> + my ($msg) = @_;
> +
> + # if a staged certificate:
> + #
> + # 1. exists
> + # 2. does not expire within two weeks
> + # 3. does expire within two years
> + # 4. and is signed by the currently active ca
> + #
> + # rotate it in, otherwise create a new certificate
> + if (
> + -f $next_certpath
> + && !PVE::Certificate::check_expiry($next_certpath, $now + 14 * 24 * 60 * 60)
> + && PVE::Certificate::check_expiry($next_certpath, $now + 2 * 365 * 24 * 60 * 60)
these constants are also used below, they should be in a single place..
the comments from the other (rotation) series apply here as well I
guess.
> + && PVE::Certificate::check_certificate_signed_by_ca($next_certpath, $capath)
> + ) {
> + rename($next_certpath, $certpath);
> + print("Restarting pveproxy after rotating certificate\n");
> + PVE::Tools::run_command(['systemctl', 'reload-or-restart', 'pveproxy']);
these two lines could maybe go into their own helper?
> + } else {
> + $renew->($msg);
> + # clean up the now useless staged certificate, if there is one
> + if (-f $next_certpath) {
> + unlink $next_certpath
> + or $!{ENOENT}
> + or warn "failed to clean-up $next_certpath - $!\n";
this should be a fat warning in any case - it should never happen, and
if it does, we possibly just prevented all existing clients pinning the
previous and the staged fingerprints from connecting..
> + }
> + }
> +
> + };
> +
> + if (PVE::Certificate::check_expiry($certpath, $now + 14 * 24 * 60 * 60)) {
> + $rotate_in_next_certificate_or_renew->("expired or expires soon, renewing...");
I know this was there before, but it's kind of weird to call this
"renewing".. we should probably let the called helper decide the message
here, since rotating in a staged certificate vs. skipping that and
putting in a newly generated one is pretty different?
> + } elsif (PVE::Certificate::check_expiry($certpath, $now + 28 * 24 * 60 * 60)) {
> + # if the certificate expires in 4 weeks, stage a new one if no
> + # certificate has been staged yet or the staged one is invalid by now
> + if (
> + !-f $next_certpath
> + || PVE::Certificate::check_expiry($next_certpath, $now + 28 * 24 * 60 * 60)
> + || !PVE::Certificate::check_expiry($next_certpath, $now + 2 * 365 * 24 * 60 * 60)
> + || !PVE::Certificate::check_certificate_signed_by_ca($next_certpath, $capath)
> + ) {
> + unlink $next_certpath
> + or $!{ENOENT}
> + or warn "failed to clean-up $next_certpath - $!\n";
> + $renew->("staging new certificate...", 1);
this messages is incomplete ("expires soon, staging new ceriticate..." ?)
> + }
> + } elsif (!PVE::Certificate::check_expiry($certpath, $now + 2 * 365 * 24 * 60 * 60)) {
> # expires in more than 2 years
> - $renew->(
> + $rotate_in_next_certificate_or_renew->(
> "expires in more than 2 years, renewing to reduce certificate life-span for client compatibility..."
> );
this should also stage a new one and handle rotating it in two weeks
later?
> }
> --
> 2.47.3
>
>
>
>
>
>
next prev parent reply other threads:[~2026-08-06 15:39 UTC|newest]
Thread overview: 22+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-05 13:18 [PATCH cluster/common/datacenter-manager/manager/proxmox v2 00/16] TLS Certificate Staging Shannon Sterz
2026-08-05 13:18 ` [PATCH cluster v2 01/16] setup: allow caller to provide the certificate filename Shannon Sterz
2026-08-05 13:18 ` [PATCH pve-common v2 02/16] certificate: add helper to verify that a certificate was signed by a ca Shannon Sterz
2026-08-06 15:02 ` Fabian Grünbichler
2026-08-05 13:18 ` [PATCH manager v2 03/16] bin/api: add a new staged certificate when renewing self-signed cert Shannon Sterz
2026-08-06 15:39 ` Fabian Grünbichler [this message]
2026-08-05 13:18 ` [PATCH manager v2 04/16] api: certificates: if node parameter is 'localhost' return local certs Shannon Sterz
2026-08-05 13:18 ` [PATCH proxmox v2 05/16] pve-api-types: expose certificates info endpoint Shannon Sterz
2026-08-05 13:18 ` [PATCH datacenter-manager v2 06/16] client: allow users to update a changed fingerprint interactively Shannon Sterz
2026-08-05 13:18 ` [PATCH datacenter-manager v2 07/16] cli/api-types: move Fingerprint to common api type crate Shannon Sterz
2026-08-06 15:23 ` Fabian Grünbichler
2026-08-05 13:18 ` [PATCH datacenter-manager v2 08/16] server: connection: report mismatching fingerprint as untrusted on probe Shannon Sterz
2026-08-05 13:18 ` [PATCH datacenter-manager v2 09/16] ui: wizard: add context if a provided fingerprint did not match remote Shannon Sterz
2026-08-05 13:18 ` [PATCH datacenter-manager v2 10/16] ui: wizard: nodes page: always update fingerprints on user confirmation Shannon Sterz
2026-08-05 13:18 ` [PATCH datacenter-manager v2 11/16] pdm-api-types: implement ApiType for Fingerprint Shannon Sterz
2026-08-05 13:18 ` [PATCH datacenter-manager v2 12/16] pdm-api-types: add staged_fingerprints field to NodeUrl Shannon Sterz
2026-08-05 13:18 ` [PATCH datacenter-manager v2 13/16] server: remotes: lock remotes config when updating it Shannon Sterz
2026-08-05 13:18 ` [PATCH datacenter-manager v2 14/16] server: connection: rotate in staged fingerprints when encountering them Shannon Sterz
2026-08-05 13:18 ` [PATCH datacenter-manager v2 15/16] server: api: tasks: move `spawn_aborted_on_shutdown()` to super module Shannon Sterz
2026-08-05 13:18 ` [PATCH datacenter-manager v2 16/16] server: bin: api: tasks: add task to discover new staged certificates Shannon Sterz
2026-08-06 15:23 ` partially-applied [PATCH cluster/common/datacenter-manager/manager/proxmox v2 00/16] TLS Certificate Staging Fabian Grünbichler
2026-08-06 15:41 ` 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=1786028636.awgbclb3om.astroid@yuna.none \
--to=f.gruenbichler@proxmox.com \
--cc=pdm-devel@lists.proxmox.com \
--cc=s.sterz@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