Files
felhom-controller/controller/internal/backup/offbox_recovery_cli.go
T
admin 9640e51321
gates / gates (push) Successful in 10s
controller v0.195.0: prove the offsite key comes back (R-200 plumbing half) -- MinAgent 0.125.0
--recover-offsite-check is a docker exec diagnostic in the shape of --print-reset-code: it
reads the customer's recovery code from STDIN, asks the agent to fetch this host's sealed
bundle and open it, and reports whether the recovered key matches the one on disk BY SHA256.
Two hashes and a verdict; never a password, never R, never a blob.

R comes from stdin and not a flag because a flag value is visible in ps, in shell history, in
a container's command line and in any transcript of the session that ran it.

IT COMPARES; IT DOES NOT INSTALL. The recovered password is never written to
offbox/repo_password -- installing changes a live box on a path nobody has walked, and that
link is next session's, with the drill around it. A test asserts the data dir is byte-unchanged
after a check; its red-proof (adding the install call) fails it.

Exit codes: 0 match, 2 clean MISMATCH, 1 a step failed -- "it failed" and "it worked and
disagreed" must never share a status. A box with no local password reports distinctly: that is
the rebuilt-box shape, where the next step is to install rather than compare.

Nothing customer-reachable ships here: no card, no form, no preview.
2026-08-04 13:42:50 +02:00

119 lines
5.0 KiB
Go

package backup
import (
"bufio"
"context"
"fmt"
"io"
"os"
"strings"
"time"
)
// R-200 — the operator-facing entry point for the recovery check, and the ONLY one this session
// ships. Deliberately a `docker exec` escape hatch in the shape of `--print-reset-code`, not a page,
// a card or an API a browser can reach: the customer-facing flow is designed on top of a chain that
// has been walked, and this is the walk.
//
// WHY R COMES FROM STDIN AND NOT A FLAG. A flag value is visible in `ps`, in the shell history, in a
// container's command line and in any transcript of the session that ran it. R is the one secret in
// this system that cannot be rotated, re-issued or recovered. It is read from stdin, held in one
// string, and cleared before the function returns — on the success path and on every failure path.
//
// docker exec -i felhom-controller /app/felhom-controller --recover-offsite-check < /root/r.txt
//
// WHAT IT PRINTS: two sha256 hashes and a verdict. Never a password, never R, never a blob. The
// hashes are of 256-bit random secrets and are non-reversible — the same value the hub already stores
// and serves in report ACKs.
// RecoveryCheckDeps is what the CLI needs; injected so the entry point is testable without a live
// agent, a live hub or real crypto.
type RecoveryCheckDeps struct {
// Manager owns the on-disk repo password hash.
Manager *Manager
// Recoverer is the agent seam (agentapi.Client satisfies it).
Recoverer OffsiteKeyRecoverer
// In is where R is read from (os.Stdin in production).
In io.Reader
// Out / Err are the report streams (os.Stdout / os.Stderr in production).
Out, Err io.Writer
// Timeout bounds the whole check. 0 → 90s (an unseal shells out to age and a fetch crosses the WAN).
Timeout time.Duration
}
// RunRecoveryCheck reads R from stdin, recovers the offsite repository password through the agent,
// and reports whether it matches the one on disk — BY HASH. Returns a process exit code:
//
// 0 = the hashes matched (the key is recoverable)
// 1 = a step failed (fetch, unseal, or no local password to compare against)
// 2 = the check ran cleanly and the hashes DIFFER — the loud case, and the one that would mean the
// sealed bundle does not carry what four weeks of documents say it carries
//
// A distinct code for the mismatch on purpose: "it failed" and "it worked and disagreed" must never
// share an exit status, because only one of them is a finding about the system rather than about the
// run.
func RunRecoveryCheck(d RecoveryCheckDeps) 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-check: not configured (no backup manager or no agent channel)")
return 1
}
in := d.In
if in == nil {
in = os.Stdin
}
// Read R: the first line of stdin, trimmed. A 10-word EFF code contains spaces, so only the
// line ending is stripped — never internal whitespace.
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-check: no recovery code on stdin. Pipe it in:")
fmt.Fprintln(errw, " docker exec -i felhom-controller /app/felhom-controller --recover-offsite-check < /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 check (R-200) — compares, never installs ===")
res, err := d.Manager.CheckOffsiteKeyRecoverable(ctx, d.Recoverer, R)
R = "" // cleared before anything else, on every path below
if err != nil {
fmt.Fprintf(errw, " [FAIL] %v\n", err) // the agent's message names the step; it carries no secret
fmt.Fprintln(errw, " nothing was written.")
return 1
}
if !res.LocalPresent {
fmt.Fprintln(errw, " [FAIL] there is no repository password on this box to compare against")
fmt.Fprintf(out, " recovered sha256: %s\n", res.RecoveredSHA256)
fmt.Fprintln(errw, " (the recovery itself SUCCEEDED — this box simply has no local key. That is the")
fmt.Fprintln(errw, " rebuilt-box shape, where the next step is to INSTALL rather than compare.)")
return 1
}
fmt.Fprintf(out, " on-disk sha256: %s\n", res.LocalSHA256)
fmt.Fprintf(out, " recovered sha256: %s\n", res.RecoveredSHA256)
if !res.Match {
fmt.Fprintln(errw, " [MISMATCH] the recovered key is NOT the key this box uses.")
fmt.Fprintln(errw, " This is a finding about the system, not about the run: the sealed bundle does not")
fmt.Fprintln(errw, " carry the repository password this box's off-site history is encrypted under.")
return 2
}
fmt.Fprintln(out, " [MATCH] the offsite repository password IS recoverable from the sealed escrow.")
fmt.Fprintln(out, " Nothing was written: this check compares and never installs.")
return 0
}