Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88d5d56b50 | ||
|
|
5ecdd3d521 | ||
|
|
3ea208b8fb |
No files matched your search
+16
-3
@@ -1,4 +1,17 @@
|
||||
## Unreleased (2026-11-15) — a cut write of `agent.json` cannot leave a blind box (R-1380; found before the release)
|
||||
## 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
|
||||
`agent_config_update`. The three sections below were built on 2026-11-14 night and 2026-11-15 morning.
|
||||
|
||||
### A cut write of `agent.json` cannot leave a blind box (R-1380; found before the release, 2026-11-15)
|
||||
|
||||
- **`config.ReplaceInPlace` + `config.RecoverCutWrite`** (`internal/config/cutwrite.go`). `agent.json` is the agent's
|
||||
file in a root-owned folder, so the trust move, the PBS seed and the backup-target move write it in place
|
||||
@@ -9,7 +22,7 @@
|
||||
four steps, the wiring in `main`, and a source pin that the three writers use the helper and open no file by hand.
|
||||
Red-proofs: felhom.eu `documentation/audits/release-2b-3-2026-11-15/part0/`. Not proven: a real power cut.
|
||||
|
||||
## Unreleased (2026-11-14 night) — release 3 („the hub tells you”) and the clock-step fix, the agent's parts, built and NOT released
|
||||
### Release 3 („the hub tells you”) and the clock-step fix, the agent's parts (2026-11-14 night)
|
||||
|
||||
Commit only. Each part can be released alone; none needs release 2b. No `configs/` change in this section.
|
||||
|
||||
@@ -66,7 +79,7 @@ Commit only. Each part can be released alone; none needs release 2b. No `configs
|
||||
compares it with its clock and alarms above one minute. Test `TestReport_StampsSentAtAtSending`; 3 mutations seen
|
||||
red (felhom.eu `documentation/audits/night-build-2026-11-14/B/redproof-agent-sent-at.txt`).
|
||||
|
||||
## Unreleased (2026-11-14 night) — release 2b, built and NOT released: trust in Proxmox by a copy of the box's own root CA (R-1380, D80, felhom.eu `09` §3 decision 303; design `03-host-agent.md` §12a)
|
||||
### Release 2b (2026-11-14 night): trust in Proxmox by a copy of the box's own root CA (R-1380, D80, felhom.eu `09` §3 decision 303; design `03-host-agent.md` §12a)
|
||||
|
||||
Commit only. No box ran any of this; the proof on a box (demo-hp, a forced new certificate, twice) is the next attended
|
||||
session's. `configs/` changes, so the release needs BOTH signed jobs.
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -246,6 +247,19 @@ func (m *Manager) Ensure(ctx context.Context, why string) bool {
|
||||
c, err := m.EnsureCopy(cctx)
|
||||
ccancel()
|
||||
if err != nil {
|
||||
m.mu.Lock()
|
||||
recorded := m.status.Problem
|
||||
m.mu.Unlock()
|
||||
if rootStepTooOld(err) && cur.Fingerprint != "" && !isTrustCause(why) && !isTrustCause(recorded) {
|
||||
// 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. NEVER on a run that a failed call started, and never over a recorded
|
||||
// trust failure: then the pin does NOT work, and the box must say so (the commit's security review).
|
||||
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 +418,20 @@ 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(), `"R11"`) && strings.Contains(err.Error(), "unknown mode")
|
||||
}
|
||||
|
||||
// isTrustCause says whether s is one of the causes proxmox.TrustProblem names for a FAILED call.
|
||||
func isTrustCause(s string) bool {
|
||||
switch s {
|
||||
case proxmox.TrustPinnedCertChanged, proxmox.TrustCAMismatch, proxmox.TrustCertExpired,
|
||||
proxmox.TrustCAFileUnreadable, proxmox.TrustNameMismatch:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -487,3 +487,80 @@ 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)
|
||||
}
|
||||
|
||||
// a refusal with ANOTHER code whose text happens to hold the words is still named: the code decides
|
||||
b4 := newBox(t, srv, proxmox.TLSConfig{Fingerprint: fp(leaf)}, nil)
|
||||
b4.m.EnsureCopy = func(context.Context) (Copy, error) {
|
||||
return Copy{}, errors.New(`pve-ca-copy refused: {"code": "T3", "reason": "the source has an unknown mode"}`)
|
||||
}
|
||||
b4.m.Ensure(context.Background(), "start")
|
||||
if st := b4.m.Status(); st.Problem != ProblemCopyUnavailable {
|
||||
t.Fatalf("a refusal with another code was read as older root files: %+v", st)
|
||||
}
|
||||
}
|
||||
|
||||
// The quiet branch must never hide a box whose pin does NOT work: a run started by a failed call, or a run after a
|
||||
// failed call was recorded, names the problem even when the root files are older than the agent.
|
||||
func TestRootStepOlderThanTheAgent_NeverHidesAFailedPin(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'"}`)
|
||||
|
||||
// (1) the run was started by a failed call
|
||||
b := newBox(t, srv, proxmox.TLSConfig{Fingerprint: fp(leaf)}, nil)
|
||||
b.m.EnsureCopy = func(context.Context) (Copy, error) { return Copy{}, old }
|
||||
b.m.Ensure(context.Background(), proxmox.TrustPinnedCertChanged)
|
||||
if st := b.m.Status(); st.Problem == "" {
|
||||
t.Fatalf("a run started by a failed pin reports no problem: %+v", st)
|
||||
}
|
||||
|
||||
// (2) a failed call was recorded, then the timer runs
|
||||
b2 := newBox(t, srv, proxmox.TLSConfig{Fingerprint: fp(leaf)}, nil)
|
||||
b2.m.EnsureCopy = func(context.Context) (Copy, error) { return Copy{}, old }
|
||||
b2.m.mu.Lock()
|
||||
b2.m.status.Problem = proxmox.TrustPinnedCertChanged
|
||||
b2.m.mu.Unlock()
|
||||
b2.m.Ensure(context.Background(), "timer")
|
||||
if st := b2.m.Status(); st.Problem == "" {
|
||||
t.Fatalf("the timer cleared a recorded pin failure: %+v", st)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user