diff --git a/internal/monitoring/monitor.go b/internal/monitoring/monitor.go index 01d048aa5..3b39feb88 100644 --- a/internal/monitoring/monitor.go +++ b/internal/monitoring/monitor.go @@ -586,13 +586,13 @@ func ensureClusterEndpointURL(raw string) string { return "https://" + net.JoinHostPort(value, "8006") } -func clusterEndpointEffectiveURL(endpoint config.ClusterEndpoint, verifySSL bool, baseFingerprint string) string { - // When TLS hostname verification is required (VerifySSL=true and no fingerprint), - // prefer hostname over IP to ensure certificate CN/SAN validation works correctly. - // When TLS is not verified (VerifySSL=false) or a fingerprint is provided (which - // bypasses hostname checks), prefer IP to reduce DNS lookups (refs #620). - hasFingerprint := strings.TrimSpace(endpoint.Fingerprint) != "" || strings.TrimSpace(baseFingerprint) != "" - requiresHostnameForTLS := verifySSL && !hasFingerprint +func clusterEndpointEffectiveURL(endpoint config.ClusterEndpoint, verifySSL bool, _ string) string { + // A fingerprint only applies to the specific endpoint it was captured from. + // The primary node's fingerprint must not be treated as valid for every + // cluster member, otherwise we incorrectly route to per-node IPs while + // pinning the wrong certificate. Refs: #1199 + hasEndpointFingerprint := strings.TrimSpace(endpoint.Fingerprint) != "" + requiresHostnameForTLS := verifySSL && !hasEndpointFingerprint // Use EffectiveIP() which prefers user-specified IPOverride over auto-discovered IP effectiveIP := endpoint.EffectiveIP() diff --git a/internal/monitoring/monitor_additional_test.go b/internal/monitoring/monitor_additional_test.go index 967f2bf8f..7ad93de74 100644 --- a/internal/monitoring/monitor_additional_test.go +++ b/internal/monitoring/monitor_additional_test.go @@ -173,6 +173,11 @@ func TestClusterEndpointEffectiveURL(t *testing.T) { t.Fatalf("per-endpoint fingerprint should allow IP override, got %q", got) } + endpoint.Fingerprint = "" + if got := clusterEndpointEffectiveURL(endpoint, true, "cluster-base-fingerprint"); got != "https://node.local:8006" { + t.Fatalf("base fingerprint must not force IP routing for other cluster nodes, got %q", got) + } + endpoint = config.ClusterEndpoint{} if got := clusterEndpointEffectiveURL(endpoint, true, ""); got != "" { t.Fatalf("empty endpoint = %q, want empty", got) @@ -209,3 +214,38 @@ func TestBuildClusterClientEndpoints_PrefersOverrideWhenEndpointFingerprintPrese t.Fatalf("expected fingerprint to follow effective endpoint URL, got %q", fingerprints["https://10.15.2.11:8006"]) } } + +func TestBuildClusterClientEndpoints_FallsBackToMainHostWhenOnlyBaseFingerprintExists(t *testing.T) { + pve := config.PVEInstance{ + Name: "cluster-a", + Host: "https://cluster-a.example.com:8006", + Fingerprint: "cluster-base-fp", + VerifySSL: true, + IsCluster: true, + ClusterName: "cluster-a", + ClusterEndpoints: []config.ClusterEndpoint{ + { + NodeName: "node1", + Host: "node1", + IP: "10.15.5.11", + }, + { + NodeName: "node2", + Host: "node2", + IP: "10.15.5.12", + }, + }, + } + + endpoints, fingerprints := buildClusterClientEndpoints(pve) + + if len(endpoints) != 1 { + t.Fatalf("expected only the main host fallback endpoint, got %d", len(endpoints)) + } + if endpoints[0] != "https://cluster-a.example.com:8006" { + t.Fatalf("expected main host fallback, got %q", endpoints[0]) + } + if len(fingerprints) != 0 { + t.Fatalf("expected no per-endpoint fingerprints, got %v", fingerprints) + } +}