Files
pulse/internal/monitoring/monitor_node_tls_fingerprint_test.go
T
courtmanr@gmail.com 07d2f98455 Gate same-name cross-instance node aggregation on TLS identity
The config layer already refuses to consolidate two same-named clusters
whose TOFU-captured TLS fingerprints contradict, and node aggregation
keeps any two same-named clusters from different connection instances
apart unconditionally. Two gaps remained one layer down. First, the
endpoint-IP agent match bypassed the contradiction guard entirely, so
two sites reusing RFC1918 addressing (the MSP support case: pve01 on
192.168.1.11 at both sites) still bound the second site's node to the
first site's host agent, attaching the wrong machine's telemetry.
Second, the unconditional split had no way to recognize the legitimate
duplicate - the same cluster added twice through different member
addresses with no config-level endpoint overlap could never fold back
into one node slot.

The aggregation layer now receives the config layer's identity
evidence: each PVE node carries the TLS certificate fingerprint of its
own named endpoint record (standalone nodes carry the instance
fingerprint; a cluster member never inherits the instance-level
fingerprint, which pins whichever member the connection URL reaches).
Same-named clusters from different instances merge only when both views
carry the same fingerprint; contradicting or unknown evidence keeps the
fail-safe split. Agent binding applies the identical doctrine: hostname
and address matches are rejected when the candidate agent's linked
nodes live in a different named cluster or carry a different
fingerprint, closing the previously unguarded endpoint-IP path.

Reported via support by an MSP whose sites reuse cluster names, node
names, and RFC1918 ranges.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 10:00:33 +01:00

54 lines
1.8 KiB
Go

package monitoring
import (
"testing"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
)
// The TLS fingerprint propagated onto a node is per-machine identity
// evidence: a standalone node inherits the instance fingerprint, a cluster
// node inherits only its own named endpoint's fingerprint, and ambiguous or
// absent records stay unknown rather than asserting identity.
func TestPVENodeTLSFingerprint(t *testing.T) {
standalone := &config.PVEInstance{Fingerprint: "AA:BB"}
if got := pveNodeTLSFingerprint(standalone, "pve01"); got != "AA:BB" {
t.Fatalf("standalone fingerprint = %q, want AA:BB", got)
}
cluster := &config.PVEInstance{
IsCluster: true,
Fingerprint: "CC:DD",
ClusterEndpoints: []config.ClusterEndpoint{
{NodeName: "pve01", Fingerprint: "AA:BB"},
{NodeName: "pve02"},
},
}
if got := pveNodeTLSFingerprint(cluster, "pve01"); got != "AA:BB" {
t.Fatalf("cluster endpoint fingerprint = %q, want AA:BB", got)
}
// The instance-level fingerprint pins whichever member the connection URL
// reaches; it must never be attributed to a different named member.
if got := pveNodeTLSFingerprint(cluster, "pve02"); got != "" {
t.Fatalf("member without endpoint fingerprint = %q, want unknown", got)
}
if got := pveNodeTLSFingerprint(cluster, "pve99"); got != "" {
t.Fatalf("unknown member = %q, want unknown", got)
}
ambiguous := &config.PVEInstance{
IsCluster: true,
ClusterEndpoints: []config.ClusterEndpoint{
{NodeName: "pve01", Fingerprint: "AA:BB"},
{NodeName: "PVE01", Fingerprint: "EE:FF"},
},
}
if got := pveNodeTLSFingerprint(ambiguous, "pve01"); got != "" {
t.Fatalf("ambiguous duplicate endpoints = %q, want unknown", got)
}
if got := pveNodeTLSFingerprint(nil, "pve01"); got != "" {
t.Fatalf("nil instance = %q, want unknown", got)
}
}