On startup reconcile the hub-served offsite: descriptor into a key-only offbox target. internal/offsiteapply.Bridge: verify-pin box host key vs host_fingerprint (NO blind TOFU) → consume the one-time password (single-use, never logged) → sshpass ssh-copy-id -s -f install + verify → configure offbox → EscrowState=pending (fork-4 via Manager.ApplyOffsiteTarget) → persist a descriptor-hash marker LAST. Idempotent + fail-safe. Seams faked in tests; both red-proofs run+reverted. Dockerfile + sshpass. NOT yet live-applied (supervised end-to-end next runbook). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
3.9 KiB
REPORT — controller v0.106.0: offsite provisioning SLICE 2 (apply-bridge)
Date: 2026-07-09 · Class: implementation (controller) + risky. Baseline: main @ fa9362f
(v0.105.0) → v0.106.0. Pairs with hub v0.38.0 (adds HostFingerprint to the offsite descriptor).
What shipped
On startup the controller reconciles the hub-served offsite: descriptor into a working key-only offbox
target — the controller half of hub-driven offsite provisioning.
internal/config.OffsiteConfig— theoffsite:section (mirrors the hub descriptor incl.host_fingerprint).internal/offsiteapply.Bridge.Reconcile— verify-pin (no blind TOFU) → generate keypair → consume the one-time password (single-use, never logged) → install pubkey (sshpass -e ssh-copy-id -p 23 -s -f)- verify → configure offbox →
EscrowState="pending"→ persist the descriptor-hash marker LAST. Idempotent (marker prevents re-consuming a spent password) and fail-safe (any step fails → nothing persisted, retried next cycle; consumed-but-failed install logs a loud "password is spent — reset on the hub").
- verify → configure offbox →
internal/backup.Manager.ApplyOffsiteTarget— reuses the fork-4 enable primitives (best-effort escrow stage).cmd/controller— wires the real seams + runsReconcileasync at startup.Dockerfile+sshpass.
Files changed
internal/config/config.go, internal/offsiteapply/{offsiteapply.go,seams.go,offsiteapply_test.go} (new),
internal/backup/offbox.go, cmd/controller/main.go, Dockerfile, CHANGELOG.md.
Tests + companion red-proofs
Green gate go build ./... && go vet ./... && go test ./... — ALL-GREEN (both repos).
TestBridge_AppliesEndToEnd— consume→verify-pin→install→configure→marker; asserts the enabler got the pinned known_hosts + the private key, and the one-time password never appears in a log line.TestBridge_HostKeyMismatchRefuses— a scanned FP ≠ descriptor FP → refuse (no consume/install/configure/marker). Red-proof run: dropped the verify (if false) → the mismatch proceeded to install a wrong key → test FAILED ("mismatch must refuse, got "). Reverted. No-TOFU is load-bearing.TestBridge_IdempotentNoReconsume— marker matches →Consumepanics if called → clean no-op.TestBridge_InstallFailIsFailSafe— install errors → marker NOT persisted, offbox NOT configured, loud "password is spent" log. Red-proof run: persisted the marker before the install → a failed apply looked done → test FAILED ("marker must NOT be persisted on a failed apply"). Reverted. Marker-after-success is load-bearing.- Hub
internal/offsite: descriptor carriesHostFingerprintfrom a faked scanner; a scan failure fails-closed.
Deploy verification
(To be filled by the deploy step: controller :0.106.0 Up (healthy) on guest 9201 + clean startup logs;
offsite is disabled in the demo config, so the bridge no-ops — the deploy proves the image ships sshpass +
the bridge wiring, not a live apply.)
NOT yet live-applied
The supervised end-to-end (hub provisions on the new pool box u629488/project 15282031 → controller
consumes + installs its key + configures the offbox → EscrowState="pending") is the next runbook, gated
on the hub being wired with the new scoped HETZNER_TOKEN + HETZNER_POOL_BOX_ID. Unit tests (faked seams)
are this slice's proof. Untested-live: the real sshpass/ssh-copy-id -s -f install + the x/crypto/ssh
host-key scan against a live box (both proven in the API spike; re-confirmed in the supervised run).
Observations
- The bridge runs once at startup; the config-refresh self-restart re-runs it after a descriptor change (no separate post-refresh hook needed — the restart is the trigger).
- The escrow stage-push is best-effort (agent-down leaves the offbox configured+pending, re-stage later) — the offbox run-gate still holds until the operator confirms escrow (fork-4).