public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Shannon Sterz <s.sterz@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [PATCH datacenter-manager 15/17] server: connection: rotate in staged fingerprints when encountering them
Date: Thu, 11 Jun 2026 14:03:25 +0200	[thread overview]
Message-ID: <20260611120327.257523-16-s.sterz@proxmox.com> (raw)
In-Reply-To: <20260611120327.257523-1-s.sterz@proxmox.com>

if a staged fingerprint is encountered when connecting to a node, move
the new fingerprint into the active position. the staged fingerprints
will also be updated to remove the new fingerprint from the staged
list.

Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
 server/src/connection.rs | 104 ++++++++++++++++++++++++++++++++++++---
 1 file changed, 98 insertions(+), 6 deletions(-)

diff --git a/server/src/connection.rs b/server/src/connection.rs
index f8b277da..500ad6c6 100644
--- a/server/src/connection.rs
+++ b/server/src/connection.rs
@@ -53,18 +53,108 @@ impl ConnectInfo {
         }
     }
 }
-///
+
+/// Updates the current fingerprint of a node of a given remote. Will also remove the fingerprint
+/// from the staged fingerprint list.
+fn update_current_fp(
+    remote_id: &str,
+    hostname: &str,
+    current_fingerprint: &Option<Fingerprint>,
+    new_fingerprint: &Fingerprint,
+) -> Result<(), Error> {
+    let _lock = pdm_config::remotes::lock_config()?;
+    let (mut config, _digest) = pdm_config::remotes::config()?;
+
+    let Some(remote) = config.get_mut(remote_id) else {
+        log::debug!("Remote '{remote_id}' vanished while updating current fingerprint.");
+        return Ok(());
+    };
+
+    for node in &mut remote.nodes {
+        if node.hostname == *hostname
+            && node.fingerprint.as_ref().and_then(|f| f.parse().ok()) == *current_fingerprint
+        {
+            node.fingerprint = Some(new_fingerprint.to_string());
+
+            if let Some(staged_fingerprints) = &mut node.staged_fingerprints {
+                staged_fingerprints.retain(|f| f != new_fingerprint);
+            }
+        }
+    }
+
+    pdm_config::remotes::save_config(config)
+}
+
 /// Returns a [`proxmox_client::Client`] set up to connect to a specific node.
 fn prepare_connect_client_to_node(
+    remote_id: &str,
     node: &NodeUrl,
     default_port: u16,
     pve_compat: bool,
 ) -> Result<Client, Error> {
-    let mut options = TlsOptions::default();
+    let fingerprint = node
+        .fingerprint
+        .as_ref()
+        .map(|fp| fp.parse::<Fingerprint>())
+        .transpose()?;
 
-    if let Some(fp) = &node.fingerprint {
-        options = TlsOptions::parse_fingerprint(fp)?;
-    }
+    let options = TlsOptions::Callback(Box::new({
+        let remote_id = remote_id.to_owned();
+        let staged_fingerprints = node.staged_fingerprints.clone();
+        let node = node.hostname.clone();
+        move |valid: bool, chain: &mut X509StoreContextRef| {
+            // If we have no fingerprint and no staged fingerprints, fall back to the system's
+            // trust store.
+            if fingerprint.is_none() && staged_fingerprints.is_none() {
+                return valid;
+            }
+
+            let Some(cert) = chain.current_cert() else {
+                log::error!("Could not get current certificate when connecting to node '{node}'.");
+                return false;
+            };
+
+            let cert_fp = match cert.digest(MessageDigest::sha256()) {
+                // A valid SHA-256 digest is by definition a valid Fingerprint. So the `expect`
+                // below must always succeed.
+                Ok(fp) => Fingerprint::try_from(&*fp)
+                    .expect("Could not get fingerprint from SHA256 digest."),
+                Err(e) => {
+                    log::error!("Could not compute remote certificate digest: {e:#}");
+                    return false;
+                }
+            };
+
+            // If the stored fingerprint matches, the connection is valid.
+            if let Some(fingerprint) = &fingerprint {
+                if *fingerprint == cert_fp {
+                    return true;
+                }
+            }
+
+            // If we have staged fingerprints and one of them matches the certificate, promote it
+            // to the current fingerprint and mark the connection as valid.
+            if let Some(staged_fingerprints) = &staged_fingerprints {
+                if staged_fingerprints.contains(&cert_fp) {
+                    // Update active fingerprint; handle this in a separate task, since this
+                    // requires locking, reading and writing the remotes configuration. There is no
+                    // need to handle this while establishing a connection.
+                    let (remote_id, node, fingerprint) =
+                        (remote_id.clone(), node.clone(), fingerprint.clone());
+                    tokio::spawn(async move {
+                        if let Err(e) = update_current_fp(&remote_id, &node, &fingerprint, &cert_fp)
+                        {
+                            log::error!("Could not update current fingerprint: {e:#}");
+                        }
+                    });
+                    return true;
+                }
+            }
+
+            // Otherwise, the connection is invalid.
+            false
+        }
+    }));
 
     let host_port: Authority = node.hostname.parse()?;
 
@@ -101,7 +191,8 @@ fn prepare_connect_client(
 
     let info = ConnectInfo::for_remote(remote);
 
-    let client = prepare_connect_client_to_node(node, info.default_port, info.pve_compat)?;
+    let client =
+        prepare_connect_client_to_node(&remote.id, node, info.default_port, info.pve_compat)?;
 
     Ok((client, info))
 }
@@ -137,6 +228,7 @@ fn prepare_connect_multi_client(remote: &Remote) -> Result<(MultiClient, Connect
     for node in &remote.nodes {
         clients.push(MultiClientEntry {
             client: Arc::new(prepare_connect_client_to_node(
+                &remote.id,
                 node,
                 info.default_port,
                 info.pve_compat,
-- 
2.47.3





  parent reply	other threads:[~2026-06-11 12:03 UTC|newest]

Thread overview: 18+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-06-11 12:03 [RFC cluster/datacenter-manager/manager/proxmox 00/17] TLS Certificate Staging Shannon Sterz
2026-06-11 12:03 ` [PATCH cluster 01/17] setup: allow caller to provide the certificate filename Shannon Sterz
2026-06-11 12:03 ` [PATCH manager 02/17] bin/api: add a new staged certificate when renewing self-signed cert Shannon Sterz
2026-06-11 12:03 ` [PATCH manager 03/17] api: certificates: if node parameter is 'localhost' return local certs Shannon Sterz
2026-06-11 12:03 ` [PATCH proxmox 04/17] client: ignore certificate trust store validation result on fp option Shannon Sterz
2026-06-11 12:03 ` [PATCH proxmox 05/17] pve-api-types: expose certificates info endpoint Shannon Sterz
2026-06-11 12:03 ` [PATCH datacenter-manager 06/17] client: don't short-circuit on valid certificate when tls fp exists Shannon Sterz
2026-06-11 12:03 ` [PATCH datacenter-manager 07/17] client: allow users to update a changed fingerprint interactively Shannon Sterz
2026-06-11 12:03 ` [PATCH datacenter-manager 08/17] cli/api-types: move Fingerprint to common api type crate Shannon Sterz
2026-06-11 12:03 ` [PATCH datacenter-manager 09/17] server: connection: report mismatching fingerprint as untrusted on probe Shannon Sterz
2026-06-11 12:03 ` [PATCH datacenter-manager 10/17] ui: wizzard: add context if a provided fingerprint did not match remote Shannon Sterz
2026-06-11 12:03 ` [PATCH datacenter-manager 11/17] ui: wizzard: nodes page: always update fingerprints on user confirmation Shannon Sterz
2026-06-11 12:03 ` [PATCH datacenter-manager 12/17] pdm-api-types: implement ApiType for Fingerprint Shannon Sterz
2026-06-11 12:03 ` [PATCH datacenter-manager 13/17] pdm-api-types: add staged_fingerprints field to NodeUrl Shannon Sterz
2026-06-11 12:03 ` [PATCH datacenter-manager 14/17] server: remotes: lock remotes config when updating it Shannon Sterz
2026-06-11 12:03 ` Shannon Sterz [this message]
2026-06-11 12:03 ` [PATCH datacenter-manager 16/17] server: api: tasks: move `spawn_aborted_on_shutdown()` to super module Shannon Sterz
2026-06-11 12:03 ` [PATCH datacenter-manager 17/17] server: bin: api: tasks: add task to discover new staged certificates Shannon Sterz

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=20260611120327.257523-16-s.sterz@proxmox.com \
    --to=s.sterz@proxmox.com \
    --cc=pdm-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