Files
archy/.planning/phases/10-key-material-hardening/10-DISCUSSION-LOG.md
T

8.3 KiB

Phase 10: Key-Material Hardening - Discussion Log

Audit trail only. Do not use as input to planning, research, or execution agents. Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.

Date: 2026-08-01 Phase: 10-key-material-hardening Areas discussed: Re-key policy for a live node, Gate signal, First-boot fail-closed behavior, Fleet scope, Wallet migration, Signing path, Rollout, 10-01 scope


Re-key policy for a live node (KEY-01)

Option Description Selected
Hard refuse; reset first Endpoints permanently closed once the node holds identity keys; re-key via the authenticated system.factory-reset, then normal onboarding restore. No authorization logic on the pre-auth path at all.
Allow with an authenticated session Keep endpoints usable post-onboarding behind a session + password re-entry. Convenient for in-place recovery, but a session-fixation or CSRF bug becomes a node takeover.
Allow only with physical presence Local console / button-press window. Strongest guarantee, needs new plumbing, awkward for headless nodes reached over Tailscale.

User's choice: Hard refuse; reset first Notes: Verified mid-discussion that system.factory-reset (dispatcher.rs:469) and auth.resetOnboarding (auth.rs:272) are both absent from UNAUTHENTICATED_METHODS, i.e. already authenticated. This made "reset first" a real existing path rather than something the phase would have to build, and confirmed it is not itself a gate bypass.


Gate signal (KEY-01)

Option Description Selected
Refuse if either says onboarded Check both key_exists (on-disk key file) and is_onboarding_complete (JSON flag); refuse if either indicates onboarded. Fails safe when they disagree.
Key file on disk only Trust only key_exists — the artefact actually being protected, unflippable without filesystem access.
Onboarding flag only Trust only is_onboarding_complete — matches how the rest of the app reasons, but the flag is writable by auth.resetOnboarding and can drift.

User's choice: Refuse if either says onboarded Notes: The drift case is real, not hypothetical — auth.rs:196-207 already contains auto-heal logic that infers completion from setup_complete + password_hash precisely because the flag can be wrong.


First-boot fail-closed behavior (KEY-02)

Option Description Selected
Retry, then fail closed Retry with backoff; on continued failure refuse to start and show a loud error. Survives transient causes without ever silently shipping fleet-shared keys.
Fail closed immediately First failure refuses to start. Absolutely safe; a transient condition bricks a brand-new node with no self-recovery.
Boot locked, with a visible alert Starts but refuses onboarding/network exposure until fixed. Most forgiving; risks a dismissed warning and a node running on shared keys.

User's choice: Retry, then fail closed Notes: Current behaviour is the inverse of all three options — fail-open, with the completion marker written even when regeneration fails (build-auto-installer-iso.sh:1647, :1659, :1663).


Fleet scope (KEY-02)

Option Description Selected
Fix builder + remediate existing Fix the ISO builder for future flashes and add boot-time detection + one-time regeneration reaching existing nodes via OTA.
Fix the ISO builder only Future flashes get unique keys; deployed nodes keep what they have.
Builder now, remediation as its own phase Ship the builder fix here; scope live-node rotation separately since it can break known_hosts and Tailscale-based access.

User's choice: Fix builder + remediate existing Notes: Chosen as the only option that ends the exposure rather than stopping it from growing. The known_hosts / remote-access risk raised in option 3 does not disappear with this choice — it was carried into CONTEXT.md as D-06's one-way reversibility rating with a required checkpoint, rather than being deferred.


Wallet migration (KEY-03)

Option Description Selected
Migrate with parity proof Recreate watch-only (disable_private_keys=true, xpub + key origin), verify balance and UTXO-set parity before/after, retain old wallet.dat for rollback.
New wallets only Future initialisations are watch-only; existing nodes keep the private key until the user re-creates their wallet.
Migrate, but user-confirmed per node Same migration behind an explicit UI opt-in.

User's choice: Migrate with parity proof Notes: Matches the audit's R-04 and CLAUDE.md's "migrations never destroy data". New-wallets-only was rejected because it leaves the exposure on exactly the nodes holding real funds.


Signing path (KEY-03)

Option Description Selected
Daemon signs PSBTs, air-gap opt-in Daemon signs with the seed it already holds in the encrypted envelope; send UX unchanged; the spending key exists in one place instead of two. External-signer path stays opt-in.
Daemon signs, nothing else Remove the duplicate copy, one hot signing path, defer all external-signer work.
External signer required Core goes watch-only, signing happens off-node. Strongest posture, changes how every user spends, needs hardware.

User's choice: Daemon signs PSBTs, air-gap opt-in Notes: Because the opt-in path must actually be reachable, the missing [fingerprint/derivation] key-origin annotation on the current descriptors (bitcoin.rs:230-231) was pulled into scope as D-09 rather than left as a follow-up — without it no hardware signer can locate its key in a PSBT.


Rollout

Option Description Selected
Emergency point release Cut a release carrying 10-01 alone once verified on the dev pair.
Dev pair now, decide after Deploy and verify, then choose emergency vs scheduled after seeing the real diff.
Ride the next scheduled OTA Fold into the next planned release with the rest of Phase 10 and pending 1.7.119 work.

User's choice: Ride the next scheduled OTA Notes: Claude recommended the emergency release; the user chose the scheduled OTA. Recorded in CONTEXT.md as D-10 with its consequence stated plainly — the exposure window for a Critical, remotely-reachable finding is now set by OTA cadence rather than by when the fix is verified. D-11 keeps 10-01 independently shippable so this stays a scheduling choice, not a technical constraint, if the timing changes.


10-01 scope

Option Description Selected
One gate over all identity-mutating endpoints Sweep every UNAUTHENTICATED_METHODS entry that mutates identity or credentials behind the same gate with one shared test suite.
Only seed.generate + seed.restore Keep 10-01 minimal — exactly the endpoints F-01 names — others get their own plan.
Gate the mutating ones, document the rest Gate writers; record an evidence-backed verdict for read-only-ish endpoints.

User's choice: One gate over all identity-mutating endpoints Notes: The "document the rest" behaviour from option 3 was folded into the chosen option for endpoints that turn out not to mutate — a verdict with evidence rather than a behaviour change.


Claude's Discretion

  • Error code / JSON-RPC response shape for a refused call.
  • Rate-limit shape and thresholds (constrained by the retry-storm trap).
  • Test organisation and file placement.
  • Whether the shared gate is middleware-layer, a per-handler helper, or both.

Deferred Ideas

  • F-04 mnemonic-over-RPC exposure (R-07) — PHASE-sized in its own right.
  • F-05 Argon2 params vs ADR-005 (R-06) — needs a versioned envelope migration or an ADR amendment.
  • F-06 ceremony mnemonic via env/stdout (R-08) — deliberately scheduled separately.
  • F-07 no cargo audit/cargo deny in CI (R-05).
  • F-09 / F-10 / F-11 low+informational items (R-12, R-13 — blocked on another agent's uncommitted work — and R-14).
  • archi-dev-box as a fresh test node, shapes A and B — sequenced after this phase; shape A is the natural harness for KEY-04.