mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 04:33:17 +00:00
docs(release-control): complete #2140 TLS-preservation contracts
Record the #2140 node TLS-preference fix in the contracts its runtime commits touch: agent-lifecycle, api-contracts, security-privacy and the dependent storage-recovery contract, with the node API payload and persisted-field proofs in internal/api/contract_test.go and internal/config/config_load_test.go. The canonical completion history registers these contract and proof files as the completion of the auto-register and consolidation runtime commits. Change-source: pulse-maintainer
This commit is contained in:
@@ -3423,6 +3423,10 @@ merged `Fingerprint`, so a pin still verifies the peer. Regression tests
|
||||
and `TestHandleUpdateNodePreservesDisabledVerifySSLThroughConsolidation` cover
|
||||
the merge and save paths.
|
||||
|
||||
The persisted `VerifySSLExplicit` marker is internal node configuration. It is
|
||||
not added to the auto-register request or response, nor to the node API
|
||||
response, so existing clients see no payload change.
|
||||
|
||||
### Manual update freshness
|
||||
|
||||
Server update-check freshness is owned by internal/updates and its API adapter.
|
||||
|
||||
@@ -4550,6 +4550,13 @@ replaced by the registration's capture state. New nodes continue to take the
|
||||
captured value. This is a behavioural clarification of the existing endpoint,
|
||||
not a new field or contract delta.
|
||||
|
||||
The node add and update payloads keep `verifySSL` as an optional pointer, so an
|
||||
explicit `false` remains distinguishable from an omitted field. An explicit
|
||||
choice is recorded as operator-owned and is not overridden by automatic PVE
|
||||
consolidation, which runs on save, load and monitor reconciliation. The response
|
||||
shape is unchanged: the persisted explicit marker is internal and is not exposed
|
||||
through the node API.
|
||||
|
||||
### Manual update freshness
|
||||
|
||||
GET /api/updates/check accepts an optional boolean force query alongside the
|
||||
|
||||
@@ -951,6 +951,20 @@ tokens, and path-normalization variants.
|
||||
|
||||
## Current State
|
||||
|
||||
### Node TLS verification preference is operator-owned
|
||||
|
||||
A PVE connection's `VerifySSL` value is security-relevant, so the stored
|
||||
operator choice must not be changed by automatic reconciliation. The persisted
|
||||
`VerifySSLExplicit` field records that a caller supplied `verifySSL` through the
|
||||
node add or update API, distinguishing a deliberate opt-out from the zero
|
||||
value. Automatic PVE consolidation (`ConsolidatePVEInstances`) and canonical
|
||||
auto-registration never override an explicit choice; the historical promotion
|
||||
is retained only when neither side recorded one, and a merged certificate
|
||||
fingerprint still pins the peer. A record written before the field existed
|
||||
loads as non-explicit and is unchanged until the operator saves a choice.
|
||||
`TestPVEInstanceVerifySSLExplicitRoundTrips` in
|
||||
`internal/config/config_load_test.go` pins the persisted default and round-trip.
|
||||
|
||||
### Manual update availability feedback
|
||||
|
||||
The settings state owner has a dedicated proof route to
|
||||
|
||||
@@ -2618,8 +2618,11 @@ preference when the host agent re-registers the node after a disconnect,
|
||||
including when the stored fingerprint was cleared. This prevents a rotating or
|
||||
self-signed certificate from silently re-enabling verification and breaking the
|
||||
monitoring path used for backup and recovery evidence. New nodes still take the
|
||||
captured registration value. See the agent-lifecycle contract for the runtime
|
||||
mechanism.
|
||||
captured registration value. The same preference is preserved when automatic
|
||||
PVE consolidation folds a duplicate cluster or an overlapping standalone into
|
||||
the canonical connection on save, load or monitor reconciliation; a merged
|
||||
certificate fingerprint still pins the peer. See the agent-lifecycle contract
|
||||
for the runtime mechanism.
|
||||
|
||||
### Manual update freshness
|
||||
|
||||
|
||||
@@ -1616,6 +1616,32 @@ func TestContract_NodeConfigUpdateTracksOptionalConnectionFieldsAndRedactedSecre
|
||||
}
|
||||
}
|
||||
|
||||
// TestContract_NodeConfigUpdateVerifySSLIsOptionalExplicitField pins the node
|
||||
// config update payload contract the TLS-preference fix relies on: verifySSL is
|
||||
// an optional pointer, so an explicit false is distinguishable from an omitted
|
||||
// value. The update handler records an explicit choice from the non-nil pointer
|
||||
// and must never treat an omitted field as an opt-out (#2140).
|
||||
func TestContract_NodeConfigUpdateVerifySSLIsOptionalExplicitField(t *testing.T) {
|
||||
var disabled NodeConfigRequest
|
||||
if err := json.Unmarshal([]byte(`{"verifySSL":false}`), &disabled); err != nil {
|
||||
t.Fatalf("decode explicit disable request: %v", err)
|
||||
}
|
||||
if disabled.VerifySSL == nil {
|
||||
t.Fatal("explicit verifySSL:false must decode to a non-nil pointer")
|
||||
}
|
||||
if *disabled.VerifySSL {
|
||||
t.Fatal("explicit verifySSL:false must decode to false")
|
||||
}
|
||||
|
||||
var omitted NodeConfigRequest
|
||||
if err := json.Unmarshal([]byte(`{"name":"cluster"}`), &omitted); err != nil {
|
||||
t.Fatalf("decode request without verifySSL: %v", err)
|
||||
}
|
||||
if omitted.VerifySSL != nil {
|
||||
t.Fatal("omitted verifySSL must stay nil so an existing TLS choice is preserved")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContract_HostedMagicLinkStablePrincipalProof(t *testing.T) {
|
||||
source, err := os.ReadFile(filepath.Clean("magic_link_handlers.go"))
|
||||
if err != nil {
|
||||
|
||||
@@ -3,6 +3,7 @@ package config
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -663,3 +664,29 @@ func TestLoadSystemSettingsWithRetry(t *testing.T) {
|
||||
assert.Equal(t, 1, calls)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPVEInstanceVerifySSLExplicitRoundTrips pins the persisted TLS-choice
|
||||
// field: an operator's explicit selection must survive serialization so
|
||||
// automatic consolidation can distinguish it from the zero value, while a
|
||||
// record written before the field existed loads as the non-explicit default
|
||||
// (#2140).
|
||||
func TestPVEInstanceVerifySSLExplicitRoundTrips(t *testing.T) {
|
||||
original := PVEInstance{
|
||||
Name: "homelab",
|
||||
Host: "https://pve.local:8006",
|
||||
VerifySSL: false,
|
||||
VerifySSLExplicit: true,
|
||||
}
|
||||
encoded, err := json.Marshal(original)
|
||||
require.NoError(t, err)
|
||||
|
||||
var decoded PVEInstance
|
||||
require.NoError(t, json.Unmarshal(encoded, &decoded))
|
||||
assert.False(t, decoded.VerifySSL)
|
||||
assert.True(t, decoded.VerifySSLExplicit, "an explicit TLS choice must survive persistence")
|
||||
|
||||
var legacy PVEInstance
|
||||
require.NoError(t, json.Unmarshal([]byte(`{"Name":"legacy","Host":"https://pve.local:8006"}`), &legacy))
|
||||
assert.False(t, legacy.VerifySSL)
|
||||
assert.False(t, legacy.VerifySSLExplicit, "a record without the field is not an explicit choice")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user