controller v0.196.0: the recovered key installs itself (R-200 plumbing half) -- MinAgent 0.125.0
gates / gates (push) Successful in 8s
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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user