diff --git a/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs index 025bc4ee..36968f35 100644 --- a/hive-c0re/src/dashboard.rs +++ b/hive-c0re/src/dashboard.rs @@ -257,10 +257,14 @@ struct StateSnapshot { /// One peer hive for the P33RS dashboard tab. Derived from /// `HYPERHIVE_PEERS` env; `url` is the peer's HTTPS dashboard root. +/// `cert_fingerprint` is `Some("sha256:")` when the peer uses a +/// self-signed cert and the operator pinned its fingerprint in +/// `services.hyperhive.swarm.peers`. #[derive(Serialize)] struct PeerHiveView { name: String, url: String, + cert_fingerprint: Option, } /// `OpQuestion` + computed `question_refs` / `answer_refs`. Built @@ -494,13 +498,38 @@ fn parse_peer_hives() -> Vec { return Vec::new(); }; raw.into_iter() - .map(|r| PeerHiveView { - name: r.domain.clone(), - url: format!("https://{}/", r.domain), + .map(|r| { + let cert_fingerprint = r.cert_fingerprint.and_then(|fp| { + if validate_cert_fingerprint(&fp) { + Some(fp) + } else { + tracing::warn!( + domain = %r.domain, + fingerprint = %fp, + "HYPERHIVE_PEERS: invalid cert_fingerprint format \ + (expected `sha256:<64 hex chars>`); ignoring fingerprint" + ); + None + } + }); + PeerHiveView { + name: r.domain.clone(), + url: format!("https://{}/", r.domain), + cert_fingerprint, + } }) .collect() } +/// Validate a TLS certificate fingerprint string from `HYPERHIVE_PEERS`. +/// Accepts `sha256:<64 hex chars>` (upper or lower case). +fn validate_cert_fingerprint(fp: &str) -> bool { + let Some(hex) = fp.strip_prefix("sha256:") else { + return false; + }; + hex.len() == 64 && hex.chars().all(|c| c.is_ascii_hexdigit()) +} + /// Group live containers by their assigned web UI port; clusters with /// more than one member are port-hash collisions the operator needs /// to resolve by renaming. Manager (fixed at 8000) and sub-agents