diff --git a/internal/api/configapi/config_handlers_canonical_auto_register_test.go b/internal/api/configapi/config_handlers_canonical_auto_register_test.go index 5bad3bc29..7c7d4e168 100644 --- a/internal/api/configapi/config_handlers_canonical_auto_register_test.go +++ b/internal/api/configapi/config_handlers_canonical_auto_register_test.go @@ -1557,3 +1557,102 @@ func TestHandleCanonicalAutoRegister_PBSKeepsDistinctSameNameTokenWhenFingerprin t.Fatalf("new site name = %q, want disambiguation from existing %q", registered.Name, existing.Name) } } + +// TestHandleCanonicalAutoRegister_PVEPreservesDisabledVerifySSL covers #2140: +// an operator who disables "Verify SSL certificate" on an existing node must +// not have it silently re-enabled by an agent health-check re-registration. +func TestHandleCanonicalAutoRegister_PVEPreservesDisabledVerifySSL(t *testing.T) { + tempDir := t.TempDir() + t.Setenv("PULSE_DATA_DIR", tempDir) + + server := newIPv4TLSServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + tokenID := "pulse-monitor@pve!" + buildPulseMonitorTokenName("pulse.example.com") + cfg := &config.Config{ + DataPath: tempDir, + ConfigPath: tempDir, + PVEInstances: []config.PVEInstance{ + { + Name: "pve01", + Host: server.URL, + TokenName: tokenID, + TokenValue: "existing-token", + VerifySSL: false, + }, + }, + } + handler := newTestConfigHandlers(t, cfg) + + reqBody := AutoRegisterRequest{ + Type: "pve", + Host: server.URL, + ServerName: "pve01", + TokenID: tokenID, + TokenValue: "rotated-token", + Source: "agent", + } + req := httptest.NewRequest(http.MethodPost, "/api/auto-register", nil) + rec := httptest.NewRecorder() + + handler.handleCanonicalAutoRegister(rec, req, &reqBody, "127.0.0.1") + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want 200: %s", rec.Code, rec.Body.String()) + } + instance := handler.defaultConfig.PVEInstances[0] + if instance.VerifySSL { + t.Fatalf("re-registration re-enabled VerifySSL on an existing node that disabled it (#2140)") + } +} + +// TestHandleCanonicalAutoRegister_PBSReservesDisabledVerifySSL is the PBS twin +// of TestHandleCanonicalAutoRegister_PVEPreservesDisabledVerifySSL. +func TestHandleCanonicalAutoRegister_PBSReservesDisabledVerifySSL(t *testing.T) { + tempDir := t.TempDir() + t.Setenv("PULSE_DATA_DIR", tempDir) + + server := newIPv4TLSServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + tokenID := "pulse-monitor@pbs!" + buildPulseMonitorTokenName("pulse.example.com") + cfg := &config.Config{ + DataPath: tempDir, + ConfigPath: tempDir, + PBSInstances: []config.PBSInstance{ + { + Name: "pbs01", + Host: server.URL, + TokenName: tokenID, + TokenValue: "existing-token", + VerifySSL: false, + }, + }, + } + handler := newTestConfigHandlers(t, cfg) + + reqBody := AutoRegisterRequest{ + Type: "pbs", + Host: server.URL, + ServerName: "pbs01", + TokenID: tokenID, + TokenValue: "rotated-token", + Source: "agent", + } + req := httptest.NewRequest(http.MethodPost, "/api/auto-register", nil) + rec := httptest.NewRecorder() + + handler.handleCanonicalAutoRegister(rec, req, &reqBody, "127.0.0.1") + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want 200: %s", rec.Code, rec.Body.String()) + } + instance := handler.defaultConfig.PBSInstances[0] + if instance.VerifySSL { + t.Fatalf("re-registration re-enabled VerifySSL on an existing node that disabled it (#2140)") + } +} diff --git a/internal/api/configapi/config_setup_handlers.go b/internal/api/configapi/config_setup_handlers.go index 8eae0f591..c58cfd472 100644 --- a/internal/api/configapi/config_setup_handlers.go +++ b/internal/api/configapi/config_setup_handlers.go @@ -2265,7 +2265,16 @@ func (h *ConfigHandlers) handleCanonicalAutoRegister(w http.ResponseWriter, r *h if pveNode.Fingerprint != "" { instance.Fingerprint = pveNode.Fingerprint } - instance.VerifySSL = pveNode.VerifySSL + // Preserve the operator's TLS choice on an existing connection. + // The incoming value records only whether this registration + // captured a fingerprint; applying it unconditionally re-enabled a + // disabled "Verify SSL certificate" setting on every agent + // health-check re-registration (#2140). Heal only the legacy state + // where strict verification is on with no pin, which can never + // connect to a self-signed endpoint (#1303). + if instance.VerifySSL && instance.Fingerprint == "" { + instance.VerifySSL = false + } log.Info().Str("host", host).Str("type", "pve").Msg(canonicalAutoRegisterMatchMessage("host; updated token in-place")) } else if h.adoptCanonicalAutoRegisterClusterMember(r.Context(), serverName, host, fingerprint, fullTokenID, tokenValue, candidateHosts, registrationSource) { // A non-primary cluster member: the cluster connection already @@ -2319,7 +2328,16 @@ func (h *ConfigHandlers) handleCanonicalAutoRegister(w http.ResponseWriter, r *h if pbsNode.Fingerprint != "" { instance.Fingerprint = pbsNode.Fingerprint } - instance.VerifySSL = pbsNode.VerifySSL + // Preserve the operator's TLS choice on an existing connection. + // The incoming value records only whether this registration + // captured a fingerprint; applying it unconditionally re-enabled a + // disabled "Verify SSL certificate" setting on every agent + // health-check re-registration (#2140). Heal only the legacy state + // where strict verification is on with no pin, which can never + // connect to a self-signed endpoint (#1303). + if instance.VerifySSL && instance.Fingerprint == "" { + instance.VerifySSL = false + } log.Info().Str("host", host).Str("type", "pbs").Msg(canonicalAutoRegisterMatchMessage("host; updated token in-place")) } else { // Agent-token auth is restricted to updating existing nodes only.