Scope cluster TLS fingerprints to their own endpoints (#1199)

This commit is contained in:
rcourtman 2026-03-25 12:10:09 +00:00
parent 2acf2e9ef9
commit ffaeea18d6
2 changed files with 47 additions and 7 deletions

View file

@ -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()

View file

@ -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)
}
}