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
+31
View File
@@ -76,6 +76,8 @@ func main() {
printResetCode := flag.Bool("print-reset-code", false, "Customer-claim escape hatch (v0.122.0, F-4): print a fresh one-time local claim/reset code to stdout, then exit. Root-gated by reachability (docker exec). Same gate consumes it.")
printInfraImages := flag.Bool("print-infra-images", false, "Print every controller-managed infra image (one per line) and exit. Read by the golden bake (felhom-agent configs/build-golden.sh) so the appliance image pre-pulls exactly what THIS controller version will request.")
recoverOffsiteCheck := flag.Bool("recover-offsite-check", false, "R-200 diagnostic: read the customer recovery code from STDIN, recover the offsite repository password from the hub-held sealed escrow via the agent, and report whether it matches the one on disk — BY HASH. Compares, never installs; writes nothing. Exit 0 = match, 2 = clean mismatch, 1 = a step failed.")
recoverOffsiteInstall := flag.Bool("recover-offsite-install", false, "R-200: read the customer recovery code from STDIN, recover the offsite repository password, print both hashes, and — with --confirm-install — PLACE it so the existing repository reopens. Without --confirm-install it is a dry run that writes nothing.")
confirmInstall := flag.Bool("confirm-install", false, "Required alongside --recover-offsite-install to actually write the recovered repository password. Deliberately a second invocation so the hashes are seen before any write is possible.")
flag.Parse()
if *showVersion {
@@ -92,6 +94,35 @@ func main() {
os.Exit(0)
}
// R-200 (v0.196.0) — the INSTALL sibling. Same fetch/unseal path, same stdin discipline for R;
// the difference is that it places the recovered password so a rebuilt box reopens the off-site
// history it inherited. Two invocations by design: without --confirm-install it prints the hashes
// and writes nothing.
if *recoverOffsiteInstall {
cfg, err := config.LoadPermissive(*configPath)
if err != nil {
fmt.Fprintf(os.Stderr, "recover-offsite-install: loading config: %v\n", err)
os.Exit(1)
}
sett, err := settings.Load(cfg.Paths.DataDir+"/settings.json", log.New(os.Stderr, "", 0))
if err != nil {
fmt.Fprintf(os.Stderr, "recover-offsite-install: loading settings: %v\n", err)
os.Exit(1)
}
ac, err := agentapi.New(cfg.LocalAPI.Endpoint, cfg.LocalAPI.Token, cfg.LocalAPI.Fingerprint)
if err != nil {
fmt.Fprintf(os.Stderr, "recover-offsite-install: agent channel: %v\n", err)
os.Exit(1)
}
os.Exit(backup.RecoverAndInstall(backup.RecoveryCheckDeps{
Manager: backup.NewManager(cfg, sett, log.New(os.Stderr, "", 0)),
Recoverer: ac,
In: os.Stdin,
Out: os.Stdout,
Err: os.Stderr,
}, *confirmInstall))
}
// R-200 (v0.195.0) — the offsite-key recovery check. A `docker exec` escape hatch in the shape of
// --print-reset-code above, and deliberately NOT a page or a browser-reachable API: the
// customer-facing flow is designed on top of a chain that has been walked, and this is the walk.