controller v0.196.0: the recovered key installs itself (R-200 plumbing half) -- MinAgent 0.125.0
gates / gates (push) Successful in 8s

--recover-offsite-install is the sibling of --recover-offsite-check: same fetch/unseal path
through the agent, same STDIN discipline for R, but it PLACES the recovered repository
password via InjectOffboxPassword so a rebuilt box reopens the history it inherited.

Doing this by hand would put the offsite DATA key through a terminal, a clipboard and shell
history. In-process the value goes agent -> this process -> the 0600 file and is rendered
nowhere.

The confirmation is a SECOND invocation: without --confirm-install it prints both hashes and
writes nothing, so the operator sees the comparison before any write is possible.

Three outcomes, named distinctly: installed (no local password -- the rebuilt-box shape),
unchanged (identical key already present, nothing written), refused (a DIFFERENT key present;
installing would clobber the key the current repository is encrypted under, and no force
option is offered). Exit 2 for the refusal, distinct from 1 for a failed step.

Red-proof: removing the confirmation gate makes the dry run write, failing the test. The
R-persistence test carries a positive control -- a planted copy is found, then removed and not
found -- because an absence check is worth only what its sensitivity is.
This commit is contained in:
2026-08-04 14:27:38 +02:00
parent bdab80c933
commit 1b1366bb6e
4 changed files with 322 additions and 0 deletions
@@ -195,3 +195,155 @@ func newBareManager(t *testing.T) *Manager {
cfg.Paths.SystemDataPath = filepath.Join(dataDir, "sys")
return NewManager(cfg, sett, logger)
}
// R-200 Part 0 — the INSTALL sibling. What is asserted is the three outcomes, the confirmation gate,
// and that R does not survive either path.
// An install on a box with NO local password writes it — the rebuilt-box shape, which is the only
// situation this command exists for.
// RED-PROOF: drop the `confirm` check so an unconfirmed run installs → the dry-run case below FAILS.
func TestRecoverAndInstall_InstallsOnABareBox(t *testing.T) {
m := newBareManager(t)
pw := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
rec := &fakeRecoverer{pw: pw, sha: HashResticPassword(pw)}
// 1) DRY RUN — prints the hashes, writes nothing.
var out, errb bytes.Buffer
if got := RecoverAndInstall(RecoveryCheckDeps{Manager: m, Recoverer: rec, In: strings.NewReader("code\n"), Out: &out, Err: &errb}, false); got != 0 {
t.Fatalf("dry run exit = %d, want 0 (%s / %s)", got, out.String(), errb.String())
}
if _, present := m.OffboxRepoPasswordHash(); present {
t.Fatal("the DRY RUN wrote the password — the confirmation gate does not hold, which is the " +
"whole reason the operator gets to see the hashes before anything exists to undo")
}
if !strings.Contains(out.String(), "DRY RUN") {
t.Errorf("the dry run must say so, got %q", out.String())
}
// 2) CONFIRMED — writes it, and it reads back identical.
out.Reset()
errb.Reset()
if got := RecoverAndInstall(RecoveryCheckDeps{Manager: m, Recoverer: rec, In: strings.NewReader("code\n"), Out: &out, Err: &errb}, true); got != 0 {
t.Fatalf("confirmed exit = %d, want 0 (%s / %s)", got, out.String(), errb.String())
}
got, present := m.OffboxRepoPasswordHash()
if !present || got != HashResticPassword(pw) {
t.Fatalf("the recovered password was not placed (present=%v hash=%q)", present, got)
}
if !strings.Contains(out.String(), "INSTALLED") {
t.Errorf("a successful install must say so, got %q", out.String())
}
// The VALUE must not have been printed on either stream.
if strings.Contains(out.String()+errb.String(), pw) {
t.Fatal("the repository password was printed")
}
}
// An identical key already present is "unchanged", not "installed" and not an error — and nothing is
// written, so a re-run is harmless.
func TestRecoverAndInstall_UnchangedWhenIdentical(t *testing.T) {
m, _ := newOffboxManager(t)
localHash, _ := m.OffboxRepoPasswordHash()
before, err := os.ReadFile(m.offboxPwPath())
if err != nil {
t.Fatal(err)
}
var out, errb bytes.Buffer
got := RecoverAndInstall(RecoveryCheckDeps{
Manager: m, Recoverer: &fakeRecoverer{pw: "x", sha: localHash},
In: strings.NewReader("code\n"), Out: &out, Err: &errb,
}, true)
if got != 0 {
t.Fatalf("exit = %d, want 0", got)
}
if !strings.Contains(out.String(), "UNCHANGED") {
t.Errorf("an identical key must report UNCHANGED, got %q", out.String())
}
after, _ := os.ReadFile(m.offboxPwPath())
if !bytes.Equal(before, after) {
t.Fatal("an UNCHANGED outcome rewrote the file")
}
}
// A DIFFERENT key already present is REFUSED — installing would clobber the key the box's current
// repository is encrypted under, and which history to keep is not this command's decision.
func TestRecoverAndInstall_RefusesToClobberADifferentKey(t *testing.T) {
m, _ := newOffboxManager(t)
before, err := os.ReadFile(m.offboxPwPath())
if err != nil {
t.Fatal(err)
}
var out, errb bytes.Buffer
got := RecoverAndInstall(RecoveryCheckDeps{
Manager: m,
Recoverer: &fakeRecoverer{pw: "y", sha: "0000000000000000000000000000000000000000000000000000000000000000"},
In: strings.NewReader("code\n"), Out: &out, Err: &errb,
}, true)
if got != 2 {
t.Fatalf("exit = %d, want 2 (a refusal is its own outcome, not a generic failure)", got)
}
if !strings.Contains(errb.String(), "REFUSED") {
t.Errorf("the refusal must say so, got %q", errb.String())
}
after, _ := os.ReadFile(m.offboxPwPath())
if !bytes.Equal(before, after) {
t.Fatal("a REFUSED install clobbered the existing key — the exact outcome the refusal exists to prevent")
}
}
// R must not survive either path, and the recovery code must never be printed.
func TestRecoverAndInstall_RLeavesNoTrace(t *testing.T) {
const code = "correct horse battery staple sedative anaconda wobbly kingdom placard yodel"
for _, tc := range []struct {
name string
confirm bool
}{{"dry run", false}, {"confirmed", true}} {
t.Run(tc.name, func(t *testing.T) {
m := newBareManager(t)
pw := "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
var out, errb bytes.Buffer
RecoverAndInstall(RecoveryCheckDeps{
Manager: m, Recoverer: &fakeRecoverer{pw: pw, sha: HashResticPassword(pw)},
In: strings.NewReader(code + "\n"), Out: &out, Err: &errb,
}, tc.confirm)
combined := out.String() + errb.String()
if strings.Contains(combined, code) {
t.Errorf("the recovery code was printed: %s", combined)
}
if strings.Contains(combined, pw) {
t.Errorf("the repository password was printed: %s", combined)
}
// POSITIVE CONTROL for the sweep below: plant R in the data dir, prove the walk finds it,
// remove it. An absence check is worth only what its sensitivity is.
ctrl := filepath.Join(m.cfg.Paths.DataDir, ".planted-control")
if err := os.WriteFile(ctrl, []byte(code), 0o600); err != nil {
t.Fatal(err)
}
if n := countFilesContaining(t, m.cfg.Paths.DataDir, code); n != 1 {
t.Fatalf("positive control: the sweep found %d planted copies, want 1 — the sweep is not sensitive", n)
}
if err := os.Remove(ctrl); err != nil {
t.Fatal(err)
}
if n := countFilesContaining(t, m.cfg.Paths.DataDir, code); n != 0 {
t.Fatalf("the recovery code survived in %d file(s) under the data dir", n)
}
})
}
}
func countFilesContaining(t *testing.T, root, needle string) int {
t.Helper()
n := 0
_ = filepath.Walk(root, func(p string, info os.FileInfo, err error) error {
if err != nil || info == nil || info.IsDir() {
return nil
}
body, rerr := os.ReadFile(p)
if rerr == nil && strings.Contains(string(body), needle) {
n++
}
return nil
})
return n
}
@@ -116,3 +116,110 @@ func RunRecoveryCheck(d RecoveryCheckDeps) int {
fmt.Fprintln(out, " Nothing was written: this check compares and never installs.")
return 0
}
// RecoverAndInstall is the sibling of RunRecoveryCheck that PLACES the recovered repository password,
// so a rebuilt box can reopen the off-site history it inherited (R-200's remaining plumbing half).
//
// WHY THIS IS CODE AND NOT A MANUAL STEP. The alternative — recover the password, read it off a
// terminal, and paste it into the injection endpoint by hand — puts the offsite DATA key through a
// human's screen, clipboard and shell history. Doing it in-process is both simpler and strictly
// safer: the value goes agent → this process → the 0600 file and is never rendered anywhere.
//
// THE CONFIRMATION IS A SEPARATE INVOCATION, ON PURPOSE. Without `confirm` this prints the two hashes
// and writes nothing — the operator sees the comparison BEFORE any write exists as a possibility.
// With `confirm` it prints the same hashes and then installs. A single interactive prompt would have
// had to share stdin with R, which is where R must not be competing for attention.
//
// THREE OUTCOMES, NAMED DISTINCTLY, because "it did nothing" and "it refused" are different facts:
//
// installed — this box had NO repository password (the rebuilt-box shape). The recovered one is placed.
// unchanged — a password is present and is byte-identical to the recovered one. Nothing is written.
// refused — a password is present and DIFFERS. Installing would clobber the key this box's CURRENT
// repository is encrypted under, so it is refused. No force option is offered here: that
// decision needs a human who knows which history they intend to keep.
func RecoverAndInstall(d RecoveryCheckDeps, confirm bool) int {
out, errw := d.Out, d.Err
if out == nil {
out = os.Stdout
}
if errw == nil {
errw = os.Stderr
}
if d.Manager == nil || d.Recoverer == nil {
fmt.Fprintln(errw, "recover-offsite-install: not configured (no backup manager or no agent channel)")
return 1
}
in := d.In
if in == nil {
in = os.Stdin
}
br := bufio.NewReader(io.LimitReader(in, 4096))
line, rerr := br.ReadString('\n')
R := strings.TrimRight(line, "\r\n")
if R == "" {
fmt.Fprintln(errw, "recover-offsite-install: no recovery code on stdin. Pipe it in:")
fmt.Fprintln(errw, " docker exec -i felhom-controller /usr/local/bin/felhom-controller --recover-offsite-install [--confirm-install] < /path/to/code")
if rerr != nil && rerr != io.EOF {
fmt.Fprintf(errw, " (read error: %v)\n", rerr)
}
return 1
}
timeout := d.Timeout
if timeout == 0 {
timeout = 90 * time.Second
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
fmt.Fprintln(out, "=== offsite key recovery INSTALL (R-200) ===")
// The recovery itself is the same call the check makes, so there is exactly one fetch+unseal path
// in this codebase and no chance of the two drifting.
pw, recoveredHash, err := d.Recoverer.RecoverOffsiteRepoPassword(ctx, R)
R = "" // cleared immediately, on every path below
if err != nil {
fmt.Fprintf(errw, " [FAIL] %v\n", err)
fmt.Fprintln(errw, " nothing was written.")
return 1
}
localHash, localPresent := d.Manager.OffboxRepoPasswordHash()
if localPresent {
fmt.Fprintf(out, " on-disk sha256: %s\n", localHash)
} else {
fmt.Fprintln(out, " on-disk sha256: (none — this box has no repository password)")
}
fmt.Fprintf(out, " recovered sha256: %s\n", recoveredHash)
switch {
case localPresent && localHash == recoveredHash:
pw = ""
fmt.Fprintln(out, " [UNCHANGED] the box already holds exactly this key. Nothing written.")
return 0
case localPresent:
pw = ""
fmt.Fprintln(errw, " [REFUSED] a DIFFERENT repository password is already present.")
fmt.Fprintln(errw, " Installing would clobber the key this box's current repository is encrypted under,")
fmt.Fprintln(errw, " and which history to keep is not a decision this command may take. Nothing written.")
return 2
}
if !confirm {
pw = ""
fmt.Fprintln(out, " [DRY RUN] nothing written. The recovered key is ready to install.")
fmt.Fprintln(out, " Re-run with --confirm-install to place it.")
return 0
}
if err := d.Manager.InjectOffboxPassword(pw, false); err != nil {
pw = ""
fmt.Fprintf(errw, " [FAIL] placing the recovered password: %v\n", err)
return 1
}
pw = ""
// Re-read from disk rather than trusting what we just wrote — the observable is the file's state.
afterHash, ok := d.Manager.OffboxRepoPasswordHash()
if !ok || afterHash != recoveredHash {
fmt.Fprintf(errw, " [FAIL] the password was written but does not read back as expected (on-disk %q)\n", afterHash)
return 1
}
fmt.Fprintln(out, " [INSTALLED] the recovered repository password is in place and reads back identical.")
fmt.Fprintln(out, " Re-apply the offsite target and run a backup: the existing repository should open.")
return 0
}