From: Christoph Heiss <c.heiss@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [PATCH installer 08/16] post-hook: generate and retrieve node certificate fingerprint
Date: Fri, 31 Jul 2026 16:35:31 +0200 [thread overview]
Message-ID: <20260731143910.936881-9-c.heiss@proxmox.com> (raw)
In-Reply-To: <20260731143910.936881-1-c.heiss@proxmox.com>
The node certificate is created unconditionally created for all
products, as having the fingerprint is useful in general.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Depends on the `proxmox-installer-types` changes and an accompanying
dependency bump.
Cargo.toml | 2 +
proxmox-installer-common/Cargo.toml | 4 +-
proxmox-post-hook/Cargo.toml | 2 +
proxmox-post-hook/src/main.rs | 172 ++++++++++++++++++++++++++--
4 files changed, 169 insertions(+), 11 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index af0655d..7ba4a94 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -21,9 +21,11 @@ anyhow = "1.0"
log = "0.4.20"
pico-args = "0.5"
regex = "1.7"
+rustls = "0.23"
serde = "1.0"
serde_json = "1.0"
serde_plain = "1.0"
+sha2 = "0.10"
toml = "0.8"
proxmox-auto-installer.path = "./proxmox-auto-installer"
proxmox-installer-common.path = "./proxmox-installer-common"
diff --git a/proxmox-installer-common/Cargo.toml b/proxmox-installer-common/Cargo.toml
index 7682680..ec4cdb8 100644
--- a/proxmox-installer-common/Cargo.toml
+++ b/proxmox-installer-common/Cargo.toml
@@ -19,9 +19,9 @@ proxmox-installer-types.workspace = true
# `http` feature
hex = { version = "0.4", optional = true }
native-tls = { version = "0.2", optional = true }
-rustls = { version = "0.23", optional = true }
+rustls = { workspace = true, optional = true }
rustls-native-certs = { version = "0.6", optional = true }
-sha2 = { version = "0.10", optional = true }
+sha2 = { workspace = true, optional = true }
ureq = { version = "3", features = [ "platform-verifier" ], optional = true }
# `cli` feature
diff --git a/proxmox-post-hook/Cargo.toml b/proxmox-post-hook/Cargo.toml
index 748b922..b6d9ce5 100644
--- a/proxmox-post-hook/Cargo.toml
+++ b/proxmox-post-hook/Cargo.toml
@@ -15,6 +15,8 @@ anyhow.workspace = true
proxmox-installer-common = { workspace = true, features = ["http"] }
proxmox-network-types.workspace = true
proxmox-installer-types.workspace = true
+rustls.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
+sha2.workspace = true
toml.workspace = true
diff --git a/proxmox-post-hook/src/main.rs b/proxmox-post-hook/src/main.rs
index d0fb297..760ab53 100644
--- a/proxmox-post-hook/src/main.rs
+++ b/proxmox-post-hook/src/main.rs
@@ -17,21 +17,26 @@ use std::{
};
use proxmox_installer_common::http::{self, header::HeaderMap};
-use proxmox_installer_types::answer::{AutoInstallerConfig, PostNotificationHookInfo};
+use proxmox_installer_types::answer::{
+ AutoInstallerConfig, PostNotificationHookInfo, SchemaVersion,
+};
/// Current version of the schema sent by this implementation.
-const POST_HOOK_SCHEMA_VERSION: &str = "1.2";
+const POST_HOOK_SCHEMA_VERSION: SchemaVersion = SchemaVersion(1, 3);
mod detail {
use anyhow::{Context, Result, anyhow, bail};
+ use rustls::pki_types::{CertificateDer, pem::PemObject};
+ use sha2::{Digest, Sha256};
use std::{
collections::{HashMap, HashSet},
ffi::CStr,
fs::{self, File},
- io::BufReader,
- os::unix::fs::FileExt,
- path::PathBuf,
- process::Command,
+ io::{BufReader, ErrorKind},
+ os::unix::fs::{FileExt, MetadataExt},
+ path::{Path, PathBuf},
+ process::{Command, Stdio},
+ time::Duration,
};
use proxmox_installer_common::{
@@ -40,10 +45,12 @@ mod detail {
};
use proxmox_installer_types::{
ProxmoxProduct, SystemDMI, UdevInfo,
- answer::{AutoInstallerConfig, FqdnConfig, FqdnFromDhcpConfig, FqdnSourceMode},
+ answer::{
+ AutoInstallerConfig, FqdnConfig, FqdnFromDhcpConfig, FqdnSourceMode, SchemaVersion,
+ },
post_hook::{
BootInfo, CpuInfo, DiskInfo, KernelVersionInformation, NetworkInterfaceInfo,
- PostHookInfo, PostHookInfoSchema, ProductInfo, SshPublicHostKeys,
+ PostHookInfo, PostHookInfoSchema, ProductInfo, SchemaVersion, SshPublicHostKeys,
},
};
@@ -93,8 +100,15 @@ mod detail {
.arg(target_path)
.args(cmd)
.output()
+ .map_err(|err| anyhow!(err))
+ .and_then(|r| {
+ if r.status.success() {
+ Ok(String::from_utf8(r.stdout)?)
+ } else {
+ Err(anyhow!("{}", String::from_utf8(r.stderr)?))
+ }
+ })
.with_context(|| format!("failed to run '{cmd:?}'"))
- .and_then(|r| Ok(String::from_utf8(r.stdout)?))
};
let fqdn = match &answer.global.fqdn {
@@ -110,6 +124,30 @@ mod detail {
.to_string(),
};
+ let schema_1_3_supported = answer
+ .post_installation_webhook
+ .as_ref()
+ .and_then(|p| p.max_schema_version)
+ .map(|v| v >= SchemaVersion(1, 3))
+ .unwrap_or_default();
+
+ let cert_fingerprint = if schema_1_3_supported {
+ match gather_node_cert_fingerprint(
+ target_path,
+ setup_info.config.product,
+ &open_file,
+ &run_cmd,
+ ) {
+ Ok(fp) => fp,
+ Err(err) => {
+ eprintln!("could not retrieve node certificate: {err:#}");
+ None
+ }
+ }
+ } else {
+ None
+ };
+
Ok(PostHookInfo {
schema: PostHookInfoSchema {
version: super::POST_HOOK_SCHEMA_VERSION.to_owned(),
@@ -181,6 +219,8 @@ mod detail {
}),
},
reboot_mode: answer.global.reboot_mode,
+ cert_fingerprint,
+ api_token: None,
})
}
@@ -568,6 +608,119 @@ mod detail {
result
}
+ /// First creates the initial node certificate, then computes the fingerprint from the freshly
+ /// initialized proxy certificate.
+ fn gather_node_cert_fingerprint(
+ target_path: &str,
+ product: ProxmoxProduct,
+ open_file: &dyn Fn(&str) -> Result<File>,
+ run_cmd: &dyn Fn(&[&str]) -> Result<String>,
+ ) -> Result<Option<String>> {
+ println!("Generating node certificates ..");
+
+ match product {
+ ProxmoxProduct::Pve => with_pmxcfs(target_path, |_| {
+ run_cmd(&["pvecm", "updatecerts"])
+ .context("failed to generate node certificates")?;
+ retrieve_cert_fingerprint(open_file("/etc/pve/local/pve-ssl.pem")?).map(Some)
+ }),
+ ProxmoxProduct::Pbs => {
+ run_cmd(&["proxmox-backup-manager", "cert", "update"])
+ .context("failed to generate node certificates")?;
+ retrieve_cert_fingerprint(open_file("/etc/proxmox-backup/proxy.pem")?).map(Some)
+ }
+ ProxmoxProduct::Pmg => {
+ run_cmd(&["pmgconfig", "apicert"])
+ .context("failed to generate node certificates")?;
+ retrieve_cert_fingerprint(open_file("/etc/pmg/pmg-api.pem")?).map(Some)
+ }
+ _ => Ok(None),
+ }
+ }
+
+ fn is_path_a_mountpoint(path: impl AsRef<Path>) -> Result<bool> {
+ let path = path.as_ref();
+
+ let parent = match path.parent() {
+ Some(p) => p,
+ None => return Ok(true), // / is always a mount
+ };
+
+ let dev = match fs::metadata(path).map(|md| md.dev()) {
+ Ok(dev) => dev,
+ Err(err) if err.kind() == ErrorKind::NotFound => return Ok(false),
+ Err(err) => return Err(err).with_context(|| format!("stat {}", path.display())),
+ };
+
+ let parent_dev = match fs::metadata(parent).map(|md| md.dev()) {
+ Ok(dev) => dev,
+ Err(err) if err.kind() == ErrorKind::NotFound => return Ok(false),
+ Err(err) => return Err(err).with_context(|| format!("stat {}", parent.display())),
+ };
+
+ // If the devices differ, it's a filesystem boundary
+ Ok(dev != parent_dev)
+ }
+
+ fn with_pmxcfs<R, F: FnOnce(&str) -> Result<R>>(target_path: &str, callback: F) -> Result<R> {
+ // Don't consider failing to start pmxcfs fatal, as detail::gather() will handle unavailable
+ // files and commands gracefully.
+
+ let mut pmxcfs = Command::new("chroot")
+ .args([target_path, "/usr/bin/pmxcfs", "--local", "--foreground"])
+ .stdin(Stdio::piped())
+ .spawn()
+ .context("starting pmxcfs")?;
+
+ // Wait until the cluster filesystem is mounted under /etc/pve
+ // Typically takes at least 1-2 seconds, so give it some time beforehand to avoid superfluous
+ // polling
+ std::thread::sleep(Duration::from_secs(1));
+
+ let pmxcfs_path = Path::new(target_path).join("etc/pve");
+ let result = loop {
+ match is_path_a_mountpoint(&pmxcfs_path) {
+ Ok(true) => break callback(target_path),
+ Ok(false) => {
+ std::thread::sleep(Duration::from_millis(500));
+ }
+ Err(err) => break Err(err),
+ }
+ };
+
+ // TODO: Switch to std::os::unix::process::ChildExt::send_signal() once that becomes stable
+ let kill_cmd = Command::new("/usr/bin/kill")
+ .arg("-TERM")
+ .arg(pmxcfs.id().to_string())
+ .status();
+
+ // gracefully shut down pmxcfs
+ if let Err(err) = kill_cmd {
+ eprintln!("failed to gracefully terminate pmxcfs ({err}), killing ..");
+ let _ = pmxcfs.kill();
+ }
+
+ if let Err(err) = pmxcfs.wait() {
+ eprintln!("failed to wait for pmxcfs to terminate: {err}");
+ }
+
+ result
+ }
+
+ fn retrieve_cert_fingerprint(file: File) -> Result<String> {
+ let reader = CertificateDer::from_pem_reader(file)?;
+
+ let mut hasher = Sha256::new();
+ hasher.update(reader);
+
+ Ok(hasher
+ .finalize()
+ .iter()
+ .fold(String::new(), |acc, b| format!("{acc}:{b:02X}"))
+ .trim_start_matches(':')
+ .to_owned())
+ }
+
#[cfg(test)]
mod tests {
use super::{
@@ -919,6 +1072,7 @@ fn do_main() -> Result<()> {
url,
cert_fingerprint,
auth_token,
+ ..
}) = &answer.post_installation_webhook
{
println!("Found post-installation-webhook; sending POST request to '{url}'.");
--
2.54.0
next prev parent reply other threads:[~2026-07-31 14:40 UTC|newest]
Thread overview: 17+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-31 14:35 [PATCH proxmox/installer/datacenter-manager 00/16] auto-installer: add installed target systems as new remotes Christoph Heiss
2026-07-31 14:35 ` [PATCH proxmox 01/16] installer-types: drop unnecessary clippy attribute Christoph Heiss
2026-07-31 14:35 ` [PATCH proxmox 02/16] installer-types: post-hook: factor schema version into proper struct Christoph Heiss
2026-07-31 14:35 ` [PATCH proxmox 03/16] installer-types: post-hook: allow additional properties on api schema Christoph Heiss
2026-07-31 14:35 ` [PATCH proxmox 04/16] installer-types: post-hook: add api-token and cert-fingerprint options Christoph Heiss
2026-07-31 14:35 ` [PATCH proxmox 05/16] installer-types: systeminfo: add check for API token creation capability Christoph Heiss
2026-07-31 14:35 ` [PATCH installer 06/16] chroot: print full error if bind-mounting fails Christoph Heiss
2026-07-31 14:35 ` [PATCH installer 07/16] post-hook: re-use low-level config retrieval from proxmox-chroot Christoph Heiss
2026-07-31 14:35 ` Christoph Heiss [this message]
2026-07-31 14:35 ` [PATCH installer 09/16] post-hook: support creating API token if requested in answer file Christoph Heiss
2026-07-31 14:35 ` [PATCH installer 10/16] auto: enforce https for post hook when generating an API token Christoph Heiss
2026-07-31 14:35 ` [PATCH installer 11/16] assistant: validate-answer: also verify post-hook settings if set Christoph Heiss
2026-07-31 14:35 ` [PATCH datacenter-manager 12/16] ui: auto-installer: spell out Proxmox Datacenter Manager Christoph Heiss
2026-07-31 14:35 ` [PATCH datacenter-manager 13/16] config: auto-install: add optional `post-hook-add-as-remote` field Christoph Heiss
2026-07-31 14:35 ` [PATCH datacenter-manager 14/16] api: auto-installer: add option for adding new remotes to PDM Christoph Heiss
2026-07-31 14:35 ` [PATCH datacenter-manager 15/16] ui: auto-installer: wizard: add checkbox to add target as new remote Christoph Heiss
2026-07-31 14:35 ` [PATCH datacenter-manager 16/16] docs: auto-installer: document adding targets as remotes afterwards Christoph Heiss
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=20260731143910.936881-9-c.heiss@proxmox.com \
--to=c.heiss@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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.