diff --git a/CHANGELOG.md b/CHANGELOG.md index a621737..9ee5653 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## Unreleased (after v0.175.0, 2026-11-15) — root files older than the agent are not a trust problem + +- **`pvetrust.Ensure`: the wrapper's „unknown mode” refusal on a PINNED box names no problem.** Between the signed + agent update and the signed root-files job, the box's wrapper does not know `pve-ca-copy`. 0.175.0 reported + `ca_copy_unavailable` for those minutes and the hub mailed the operator (seen on Tester 1, 2026-11-15 10:04). The + pin works, so nothing is wrong; the bundle install runs the move. An unpinned box, and every other refusal, still + names the problem. Test `TestRootStepOlderThanTheAgent_IsNotATrustProblem`; 3 mutations seen red. + ## v0.175.0 (2026-11-15) — trust in Proxmox by a copy of the box's own root CA (R-1380, release 2b); the agent tells the hub its clock, a refused job, a give-up and what a clean-up did (release 3); a boot key a clock step cannot move (R-1395) — RELEASED 2026-11-15 (release-agent.sh from the clean working clone at `db01ae0`, CI run id 2671; agent sha256 d1a30ed51f6663d96369ad3e88c221194af92773addf4fbb48852ee58be4b19a, bundle sha256 3b4a8b78155fc4385148c98c229da2e61e7c6b02bad9ed9f1fdc4a9ea1370d95); NOT vouched yet `configs/` changed (the wrapper's `pve-ca-copy` mode), so a box needs BOTH signed jobs: `agent_update`, then diff --git a/internal/pvetrust/pvetrust.go b/internal/pvetrust/pvetrust.go index d59a391..4d0412a 100644 --- a/internal/pvetrust/pvetrust.go +++ b/internal/pvetrust/pvetrust.go @@ -29,6 +29,7 @@ import ( "log/slog" "os" "path/filepath" + "strings" "sync" "time" @@ -246,6 +247,15 @@ func (m *Manager) Ensure(ctx context.Context, why string) bool { c, err := m.EnsureCopy(cctx) ccancel() if err != nil { + if rootStepTooOld(err) && cur.Fingerprint != "" { + // The agent is newer than the box's root files: between the agent update and the root-files job the + // wrapper does not know the step yet. The pinned certificate works; this is not a trust problem, and + // naming one mailed the operator at every box's update (seen on Tester 1, 2026-11-15). The bundle + // install calls Ensure again. + lg.Info("pvetrust: the root files are older than this agent (no CA-copy step yet) — the pinned certificate stays; the next root-files update moves the box", "err", err) + set(modeOf(cur), "", "", nil) + return false + } lg.Warn("pvetrust: the root step gave no CA copy — the trust stays as it is", "err", err) set(modeOf(cur), ProblemCopyUnavailable, truncate(err.Error(), 240), nil) return false @@ -404,3 +414,10 @@ func (m *Manager) writeConfig(caPath string) error { } return nil } + +// rootStepTooOld says whether the root step refused because it does not know the CA-copy mode at all (the wrapper's +// R11 „unknown mode”): the box's root files are older than the agent. Pinned by +// TestRootStepOlderThanTheAgent_IsNotATrustProblem. +func rootStepTooOld(err error) bool { + return err != nil && strings.Contains(err.Error(), "unknown mode") +} diff --git a/internal/pvetrust/pvetrust_test.go b/internal/pvetrust/pvetrust_test.go index c745e83..9bd3595 100644 --- a/internal/pvetrust/pvetrust_test.go +++ b/internal/pvetrust/pvetrust_test.go @@ -487,3 +487,42 @@ func TestProofCannotBeMadeNow_NoCauseNamed(t *testing.T) { t.Fatalf("status = %+v moved=%v", st, b.m.Moved()) } } + +// Between the agent update and the root-files job the wrapper does not know the CA-copy step. A pinned box then has +// NO trust problem (the pin works): naming one mails the operator at every box's update. A box with NO working pin +// that gets the same refusal still names the problem. +func TestRootStepOlderThanTheAgent_IsNotATrustProblem(t *testing.T) { + p := newPKI(t, "PVE CA") + leaf := p.leaf() + srv := newPVE(t, leaf) + old := errors.New(`pve-ca-copy refused: {"code": "R11", "reason": "unknown mode 'pve-ca-copy'"}`) + + b := newBox(t, srv, proxmox.TLSConfig{Fingerprint: fp(leaf)}, nil) + b.m.EnsureCopy = func(context.Context) (Copy, error) { return Copy{}, old } + before, _ := os.ReadFile(b.cfgPath) + if b.m.Ensure(context.Background(), "start") { + t.Fatal("the trust changed with no copy") + } + if st := b.m.Status(); st.Problem != "" || st.Mode != "fingerprint" { + t.Fatalf("a pinned box with older root files reports a trust problem: %+v", st) + } + if after, _ := os.ReadFile(b.cfgPath); string(after) != string(before) { + t.Fatal("the config was written") + } + + // the same refusal on a box that is NOT pinned is still a problem: nothing else holds its trust up + b2 := newBox(t, srv, proxmox.TLSConfig{}, nil) + b2.m.EnsureCopy = func(context.Context) (Copy, error) { return Copy{}, old } + b2.m.Ensure(context.Background(), "start") + if st := b2.m.Status(); st.Problem != ProblemCopyUnavailable { + t.Fatalf("an unpinned box with no copy step must name the problem: %+v", st) + } + + // and another refusal on a pinned box is still named + b3 := newBox(t, srv, proxmox.TLSConfig{Fingerprint: fp(leaf)}, nil) + b3.m.EnsureCopy = func(context.Context) (Copy, error) { return Copy{}, errors.New(`pve-ca-copy refused: {"code": "T3"}`) } + b3.m.Ensure(context.Background(), "start") + if st := b3.m.Status(); st.Problem != ProblemCopyUnavailable { + t.Fatalf("a real refusal lost its name: %+v", st) + } +}