v0.107.0: key-auth-first bridge + staged-secret wipe on escrow confirm

Key-auth-first: a KeyAuthProber seam lets the bridge skip consume+install
when the already-installed key still authenticates (pinned to the freshly
verified host key) — descriptor changes on provisioned guests no longer
loop on consume-404. Fingerprint verify still precedes everything.

Wipe-on-escrowed: confirm-escrow now calls the agent's new
DELETE /escrow/stage-secret (v0.78.0) best-effort, closing the hygiene gap
where a ceremony-less confirm left the staged password file behind.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-09 22:26:53 +02:00
parent 42af088308
commit a38c743926
9 changed files with 241 additions and 1 deletions
@@ -43,6 +43,14 @@ type (
OffboxEnabler interface {
ConfigureOffbox(ctx context.Context, host, user string, port int, repoPath, privPEM, knownHosts string) error
}
// KeyAuthProber checks whether an ALREADY-INSTALLED key authenticates to the target (pinned to the
// freshly-scanned knownHosts). ok=true returns that key's PEM so the descriptor change is applied by
// re-pinning + reconfiguring WITHOUT consuming a one-time password (key-auth-first — kills the
// stale-descriptor consume-404 loop and shrinks the re-issue blast radius to genuinely-fresh guests).
// ok=false (no key / auth refused) → the caller falls through to the full consume+install path.
KeyAuthProber interface {
Probe(ctx context.Context, host, user string, port int, knownHosts string) (privPEM string, ok bool)
}
)
// Bridge reconciles the offsite descriptor into a configured offbox target.
@@ -53,7 +61,8 @@ type Bridge struct {
KeyGen KeyGenerator
Installer KeyInstaller
Enabler OffboxEnabler
MarkerPath string // where the applied-descriptor-hash is persisted (e.g. <dataDir>/offbox/applied_marker)
Prober KeyAuthProber // optional: key-auth-first (nil → always the full consume+install path)
MarkerPath string // where the applied-descriptor-hash is persisted (e.g. <dataDir>/offbox/applied_marker)
Logger *log.Logger
}
@@ -121,6 +130,24 @@ func (b *Bridge) Reconcile(ctx context.Context) error {
return fmt.Errorf("offsite-apply: host-key MISMATCH for %s (got %s, want %s) — refusing to pin/install (possible MITM)", o.Host, scannedFP, o.HostFingerprint)
}
// 1b) Key-auth-first: if an already-installed key still authenticates (pinned to the key we JUST
// verified — the probe never weakens the identity check), the descriptor change is applied by
// re-pinning + reconfiguring alone. NO one-time password is consumed — a stale/re-scanned descriptor
// on an already-provisioned guest no longer loops on consume-404.
if b.Prober != nil {
if privPEM, ok := b.Prober.Probe(ctx, o.Host, o.User, port, knownHostsLine); ok {
if err := b.Enabler.ConfigureOffbox(ctx, o.Host, o.User, port, o.RepoPath, privPEM, knownHostsLine); err != nil {
return fmt.Errorf("offsite-apply: reconfigure (key-auth-first): %w", err)
}
if err := b.writeMarker(h); err != nil {
b.logf("[WARN] [offsite-apply] key-auth-first applied for %s but failed to persist the marker: %v", o.Host, err)
return err
}
b.logf("[INFO] [offsite-apply] existing key still authenticates to %s@%s — re-pinned + reconfigured without consuming a password", o.User, o.Host)
return nil
}
}
// 2) Generate the controller keypair.
privPEM, pubAuthorized, err := b.KeyGen.Generate()
if err != nil {