dr: recovered WG-key install + host_loss directive→restore-PLAN (S5 safe halves)
wgtunnel.InstallRecoveredKey: write an escrow-recovered WG private key (create-
only, refuse-overwrite) so the tunnel re-establishes with the same identity/pubkey
(same /32), no keygen. Wired into identity-consume -install-wg-key (opt-in;
pre-S3 blob → logged fresh-keygen fallback). Value never logged.
internal/dr (new): consume the host_loss restore_directive (was logged-ignored)
into an inspectable RestorePlan via the AddConsumer raw seam — per guest
{vmid,archive,target,sizing} + per drive {durable_id→mount} + offsite PBS coord.
DERIVE-AND-SURFACE only; the Consumer has no restore/destroy dependency (execute-
nothing is structural). guest_loss/absent → no plan.
Tests + red-proofs (WG create-only overwrite; plan mode-gate). No secrets on
argv/stdout/logs. The destructive in-place restore is a separate operator-present
STOP-gated drill.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -29,6 +29,7 @@ import (
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/capability"
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/config"
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/desired"
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/dr"
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/escrow"
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/guesthook"
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/hub"
|
||||
@@ -106,6 +107,7 @@ func main() {
|
||||
blobPath string
|
||||
expectedFP string
|
||||
keyDest string
|
||||
installWGKey bool
|
||||
idBundlePath string
|
||||
directivePath string
|
||||
swapImage string
|
||||
@@ -133,6 +135,7 @@ func main() {
|
||||
flag.StringVar(&blobPath, "blob", "", "for --selftest=escrow-consume: path to the R-wrapped escrow blob file")
|
||||
flag.StringVar(&expectedFP, "fingerprint", "", "for --selftest=escrow-consume: the EXPECTED key fingerprint (the gate target)")
|
||||
flag.StringVar(&keyDest, "keydest", "", "for --selftest=escrow-consume: where to install the recovered key (0600)")
|
||||
flag.BoolVar(&installWGKey, "install-wg-key", false, "for --selftest=identity-consume: ALSO install the recovered wg_private_key into wgtunnel's key file (S5 DR; create-only, refuses to overwrite)")
|
||||
flag.StringVar(&idBundlePath, "identity-bundle", "", "for --selftest=escrow-create: a 0600 JSON file {tunnel_token,pbs_token} to ALSO escrow under R (10D)")
|
||||
flag.StringVar(&directivePath, "directive", "", "for --selftest=escrow-create: a JSON file with the non-secret DR directive (pbs repo/ns, expected fingerprint, tunnel id)")
|
||||
flag.StringVar(&custID, "customer-id", "", "for --selftest=provision: the customer id — the hub config-pull target, baked into the guest's bootstrap")
|
||||
@@ -198,7 +201,7 @@ func main() {
|
||||
case "escrow-consume":
|
||||
os.Exit(runSelftestEscrowConsume(context.Background(), logger, blobPath, expectedFP, keyDest))
|
||||
case "identity-consume":
|
||||
os.Exit(runSelftestIdentityConsume(context.Background(), logger, blobPath, keyDest))
|
||||
os.Exit(runSelftestIdentityConsume(context.Background(), cfg, logger, blobPath, keyDest, installWGKey))
|
||||
case "controller-swap":
|
||||
os.Exit(runSelftestControllerSwap(context.Background(), cfg, logger, vmid, swapImage))
|
||||
}
|
||||
@@ -428,6 +431,15 @@ func runDaemon(cfg config.Config, logger *slog.Logger) int {
|
||||
// The "Down" channel sync hook: on each heartbeat, fetch desired-state when the generation
|
||||
// advances. The loop calls it via the EnvelopeObserver seam (hub does not import desired).
|
||||
desiredSyncer := desired.NewSyncer(client, desiredProvider, logger)
|
||||
// S5: consume a host_loss restore_directive into an inspectable restore PLAN (derive + surface,
|
||||
// execute nothing). The recipe is fetched on-demand (rare directive) via a fresh Collect.
|
||||
desiredSyncer.AddConsumer(dr.NewConsumer(func(ctx context.Context) *hub.DRRecipeHostHalf {
|
||||
r, err := collector.Collect(ctx)
|
||||
if err != nil || r == nil {
|
||||
return nil
|
||||
}
|
||||
return r.DRRecipe
|
||||
}, cfg.Backup.RestoreStorage, logger))
|
||||
// The signed-jobs runner (slice 10B) is wired as a SECOND envelope observer below (after the
|
||||
// gate is built) — when the heartbeat flags pending signed ops, it fetches + verifies + executes.
|
||||
|
||||
@@ -1702,7 +1714,7 @@ func runSelftestEscrowConsume(ctx context.Context, logger *slog.Logger, blobPath
|
||||
// and writes the recovered {tunnel_token, pbs_token} JSON to -keydest (0600). R is taken BY HAND from
|
||||
// FELHOM_RECOVERY_CODE (off the command line); the recovered tokens are never logged. The drill then
|
||||
// uses the tunnel token to re-establish the tunnel + the pbs token for steady-state.
|
||||
func runSelftestIdentityConsume(ctx context.Context, logger *slog.Logger, blobPath, keyDest string) int {
|
||||
func runSelftestIdentityConsume(ctx context.Context, cfg config.Config, logger *slog.Logger, blobPath, keyDest string, installWGKey bool) int {
|
||||
if blobPath == "" || keyDest == "" {
|
||||
fmt.Fprintln(os.Stderr, "selftest=identity-consume requires -blob and -keydest (R via env FELHOM_RECOVERY_CODE)")
|
||||
return 2
|
||||
@@ -1732,6 +1744,28 @@ func runSelftestIdentityConsume(ctx context.Context, logger *slog.Logger, blobPa
|
||||
return 1
|
||||
}
|
||||
fmt.Printf(" [OK] identity recovered (tunnel_token + pbs_token) → %s (0600) — never printed\n", keyDest)
|
||||
|
||||
// S5 DR: install the recovered WG private key so the tunnel re-establishes with the SAME
|
||||
// identity/pubkey (→ the same hub /32), no fresh keygen. Create-only (refuses to overwrite a
|
||||
// present key). The VALUE is never printed — field NAME only.
|
||||
if installWGKey {
|
||||
stateDir := cfg.WGTunnel.WithDefaults().StateDir
|
||||
switch {
|
||||
case bundle.WGPrivateKey == "":
|
||||
fmt.Printf(" [WARN] -install-wg-key set but the recovered bundle has NO wg_private_key (pre-S3 blob) — "+
|
||||
"DR falls back to fresh keygen + re-register (keeps the /32 via hub re-key-in-place). key path: %s\n",
|
||||
wgtunnel.KeyFilePath(stateDir))
|
||||
default:
|
||||
if err := wgtunnel.InstallRecoveredKey(stateDir, bundle.WGPrivateKey); err != nil {
|
||||
fmt.Fprintln(os.Stderr, " [FAIL] install recovered wg key:", err) // never contains the key value
|
||||
return 1
|
||||
}
|
||||
logger.Info("escrow: installed recovered identity field", "field", "wg_private_key",
|
||||
"key_path", wgtunnel.KeyFilePath(stateDir)) // NAME only, never the value
|
||||
fmt.Printf(" [OK] recovered wg_private_key installed at %s (0600, create-only) — tunnel will re-establish with the same identity\n",
|
||||
wgtunnel.KeyFilePath(stateDir))
|
||||
}
|
||||
}
|
||||
fmt.Println("=== selftest=identity-consume OK ===")
|
||||
return 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user