Files
archy/.planning/phases/10-key-material-hardening/10-02-SUMMARY.md

14 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, decisions, metrics, status
phase plan subsystem tags requires provides affects tech-stack key-files decisions metrics status
10-key-material-hardening 02 security-verification
security
KEY-01
KEY-04
F-01
C-6
probe
unverified
10-01 — api::rpc::onboarding_gate (the gate whose refusal this plan must prove on hardware)
scripts/security/rpc-exposure-probe.sh — repeatable, read-only-by-default RPC exposure probe
docs/security/KEY-01-ON-NODE-VERIFICATION.md — the evidence record for C-6 and the F-01 refusal
The probe-method correction: auth.isOnboardingComplete, not seed.status, is the C-6 exposure signal
audit item C-6 (still UNVERIFIED — narrowed, not closed)
D-10 OTA gating: the refusal check is now provably blocked on deployment, not on repo work
added patterns
Read-only by construction: probe methods come from a fixed array, never from an argument
Mutating requests confined to a single explicit --destructive branch
Published BIP-39 test vector instead of minting real mnemonics (the C-5 anti-pattern)
created modified
scripts/security/rpc-exposure-probe.sh
docs/security/KEY-01-ON-NODE-VERIFICATION.md
C-6 recorded as NOT VERIFIED rather than closed on loopback evidence — probing from the node measures the local stack, not the LAN
The --destructive refusal check was NOT run: no node in the fleet carries 10-01's gate yet, so it would have replaced the dev-box's identity instead of being refused
The /rpc/ nginx block returns 404 — the unauthenticated surface is reachable through /rpc/v1 only
duration completed
~35m 2026-08-02
blocked

Phase 10 Plan 02: On-Node Verification of KEY-01 / C-6 Summary

The probe exists, is safe by construction, and has been run — but audit item C-6 is still UNVERIFIED and the KEY-01 refusal is still unproven on hardware, because no node in the fleet is running 10-01's gate and no second machine was available to probe from.

Status: Task 1 complete · Tasks 2 and 3 BLOCKED on unmet preconditions

Task Type Outcome
1. Build the read-only-by-default probe auto Done, committed 527f6023
2. Measure C-6 on real nodes, prove the refusal checkpoint (blocking) BLOCKED — precondition unmet
3. Fresh-node onboarding non-regression checkpoint (blocking) BLOCKED — precondition unmet

Per the plan's autonomous: false posture, neither checkpoint was auto-approved and no result was recorded that was not observed.

What Was Built

scripts/security/rpc-exposure-probe.sh (new, 296 lines, mode 755)

Flag / symbol Contract
--target <host> required; host, onion or ULA. Bare IPv6 is bracketed automatically so the mesh ULA can be probed
--scheme http|https · --port N · --label <name> defaults http / 80 / unlabelled
--insecure added beyond the plan — accept a self-signed cert on https; without it every https vantage point is a false UNREACHABLE
--destructive the single mutating branch: the KEY-01 refusal check
READONLY_METHODS health, auth.isOnboardingComplete, seed.status — the only methods the default path can call
exit 0 / non-zero all controls as expected / seed.status was not 401, or --destructive was not refused

Four requests per read-only run (well under 10-01's 10-per-300s floor, so T-10-15 does not fire): the three methods on /rpc/v1, plus the exposure signal repeated on nginx's /rpc/ block.

Safety properties, as required by the threat model:

  • T-10-11: the method string is built from the fixed array, never from an argument; every mutating request is inside one if [ "$DESTRUCTIVE" = "1" ] branch behind a red disposable-nodes-only banner.
  • T-10-12: the refusal check uses the published BIP-39 all-abandon + art vector (32 zero bytes). The script never generates and never prints a mnemonic — the deliberate difference from audit item C-5, which mints real ones.
  • T-10-13: no node address, onion address, username or password is embedded (grep -nE '([0-9]{1,3}\.){3}[0-9]{1,3}|\.onion|password' matches only the safety comment that forbids them).

The before/after byte-identity check is not attempted by the script (it has no node-local file access); it prints the two sha256sum commands so they land in the operator's transcript.

The Probe-Method Correction (the substantive finding)

The audit's C-6 command (ENTROPY-SEED-AUDIT-2026-07-31.md:890-901) probes with seed.status and calls 200 a failure. seed.status is not in UNAUTHENTICATED_METHODS (middleware.rs:5-38), so it is rejected at api/rpc/mod.rs:293 with a 401 by design — the audit's failure criterion can never fire, and the probe reports the surface CLOSED while F-01's door stands open. The probe therefore measures exposure with auth.isOnboardingComplete (genuinely allowlisted at middleware.rs:9, read-only) and keeps seed.status as the session-enforcement control. Recorded in the evidence document so it is not re-derived a third time.

What Was Actually Measured

Both runs originated on the node under test, so neither is a C-6 result — they are recorded as loopback and self-lan-ip, not lan.

Vantage health auth.isOnboardingComplete seed.status /rpc/
loopback 200 200 — EXPOSED 401 — PASS 404
self-lan-ip 200 200 — EXPOSED 401 — PASS 404
  • seed.status returned 401 on every vantage tested — no stop-the-plan finding.
  • Incidental finding: /rpc/ returns 404. nginx's second proxy block (nginx-archipelago.conf:192) forwards the full URI and the backend routes only /rpc/v1, so the unauthenticated surface has exactly one path. This narrows F-01's exposure surface.
  • Corroborating, not measurement: nginx binds 0.0.0.0:80 and [::]:80, the daemon is loopback-only on :5678, and the host filter has no rule matching tcp/80 (-P INPUT ACCEPT, nft ruleset is Tailscale chains only). A LAN EXPOSED result is very likely — but likely is not measured, and C-6 stays open.

Why Tasks 2 and 3 Are Blocked (verified, not assumed)

No node in the fleet is running 10-01's gate. Checked on the dev-box rather than inferred:

$ ls -l /usr/local/bin/archipelago
-rwxr-xr-x 1 root root 53437536 Aug  2 06:37 /usr/local/bin/archipelago
$ git log -1 --format='%ci' 879de59e
2026-08-02 13:05:35 -0400
$ grep -qa "Not supported: this node is already provisioned" /usr/local/bin/archipelago && echo PRESENT || echo ABSENT
ABSENT

The installed binary was built ~6.5h before 10-01 landed, and the gate's refusal string is absent from it. A --destructive run against this node would therefore not be refused — it would replace node_key, nostr_secret and fips_key on a live dev-pair deploy target that is gated before every OTA. It was not run. The plan's own threat model (T-10-11) and the phase brief (which excludes deployment) make this a hard block, not a judgement call.

Task 3's harness is shape (A) of .planning/todos/pending/2026-08-01-archi-dev-box-as-fresh-test-node-without-iso.md — a second daemon under its own ARCHIPELAGO_DATA_DIR. That todo is still pending: the harness does not exist, and it would additionally need a binary built from 879de59e or later.

Task 2 also requires a second machine on the LAN. This session ran on the node itself, and probing fleet nodes uninvited is out of bounds (.228 is in real use).

Pre-OTA Fleet Check Carried Over from 10-01

10-01 flagged a state its gate makes unrecoverable — onboarding.json complete-true with no user.json — and asked 10-02 to sweep the fleet before the OTA (D-10).

Node user.json onboarding.json Verdict
dev-box PRESENT {"complete": true} safe — provisioned normally
rest of fleet NOT CHECKED

Deviations from Plan

1. [Rule 1 — Bug] curl failure fallback produced a 000000 status code

  • Found during: Task 1 verification against a dead port.
  • Issue: HTTP_CODE=$(curl … -w '%{http_code}' … || echo "000") — curl already emits 000 when no response arrives, so the fallback appended a second 000. Every verdict then fell through to the wildcard branch and an unreachable host was reported as seed.status … CRITICAL — session enforcement is NOT working, exit 1. A false critical on an unreachable transport is exactly the misreading this plan exists to prevent.
  • Fix: the fallback now replaces rather than appends (if ! HTTP_CODE=$(curl …); then HTTP_CODE="000"; fi) plus a three-digit normalisation. Retested: an unreachable target now reports UNREACHABLE on all four lines and exits 0, since an unreachable vantage point is a result, not a control failure.
  • Commit: 527f6023 (fixed before the commit).

2. [Rule 2 — Missing critical functionality] --insecure flag added

  • Issue: the plan's CLI contract has no way to accept a self-signed certificate, but Archipelago nodes serve https with one. Every --scheme https probe would have reported a false UNREACHABLE, silently under-measuring the exposure surface.
  • Fix: opt-in --insecure, off by default, documented in --help. It does not alter the plan's flag contract.

3. Tasks 2 and 3 not executed — see the blocked section above. No result was recorded that was not observed; nothing was marked verified.

Verification

  • bash -n scripts/security/rpc-exposure-probe.sh → exits 0.
  • bash scripts/security/rpc-exposure-probe.sh --help → prints usage, exits 0.
  • test -x → mode 755 (-rwxr-xr-x).
  • grep -c READONLY_METHODS22 (≥1 required); grep -c DESTRUCTIVE5 (≥2 required).
  • Missing --target → exit 2; unknown argument → exit 2; unreachable target → all UNREACHABLE, exit 0; live daemon → 401 on the enforcement control, exit 0.
  • shellcheck is NOT installed on this host (command -v shellcheck → empty). The acceptance criterion's shellcheck run was therefore not performed, recorded here rather than silently skipped.
  • Both commits stage exactly one file each by explicit path; git diff --diff-filter=D over both is empty. Three other agents are working in this tree (plans 10-04, 10-06, 01-18) and none of their files were staged, reverted or modified.

Known Stubs

None in code. The evidence document is deliberately incomplete and says so in its first line — five of its rows are NOT MEASURED / NOT PERFORMED with the exact command needed to close each.

Audit Checklist Movement

Item Before After
C-6 UNVERIFIED still UNVERIFIED — method corrected, tooling built, 2 non-qualifying vantage points recorded, 3 transports outstanding
KEY-01 refusal on hardware unproven still unproven — blocked on deploying 10-01
Fresh-node onboarding non-regression unproven still unproven — blocked on the shape-A harness

Nothing moved from UNVERIFIED to VERIFIED in this plan. What changed is that the remaining work is now mechanised (one command per transport), correctly specified (the method correction), and provably blocked on deployment rather than on anything doable in the repository.

What Is Still Required

  1. LAN: rpc-exposure-probe.sh --target <node-lan-ip> --scheme http --port 80 --label lan from a second machine.
  2. Tor: torsocks rpc-exposure-probe.sh --target <onion> --label tor.
  3. Mesh: rpc-exposure-probe.sh --target <fips-ula> --label mesh from a peer node.
  4. Deploy 879de59e+ to a disposable node, capture sudo sha256sum /var/lib/archipelago/identity/{node_key,nostr_secret}, run --destructive --label refusal from a second machine, re-capture the digests. Response must carry Not supported: and the digests must match character for character.
  5. Build shape (A), walk the wizard end to end on a 10-01 binary (reloading once on the seed screen to confirm the same 24 words return, and seeing no Not supported: / Rate limit exceeded), then re-run step 4 against it.
  6. Sweep the remaining fleet for onboarding.json complete-true without user.json.

Threat Flags

None. No new endpoint, no dependency (T-10-SC did not fire — curl, grep and mktemp are pre-existing host tools), no schema change. The one new surface is the --destructive branch, which is the plan's own T-10-11 and is mitigated as specified.

Commits

  • 527f6023feat(10-02): add read-only-by-default RPC exposure probe (C-6 / KEY-01)scripts/security/rpc-exposure-probe.sh only.
  • f2f89b5fdocs(10-02): record C-6 evidence so far — probe-method correction, 3 transports still opendocs/security/KEY-01-ON-NODE-VERIFICATION.md only.

Not pushed — the orchestrator pushes.

Self-Check: PASSED

Both files exist on disk (scripts/security/rpc-exposure-probe.sh mode 755, 296 lines; docs/security/KEY-01-ON-NODE-VERIFICATION.md 224 lines, contains C-6 and rpc-exposure-probe); both commits 527f6023 and f2f89b5f are present in git log; the probe contains DESTRUCTIVE (5) and READONLY_METHODS (22); neither file contains a node address, onion address or credential.

Not done by this agent, deliberately: STATE.md / ROADMAP.md / REQUIREMENTS.md updates. Three other agents are executing concurrently in this shared tree and .planning/STATE.md already carries an uncommitted edit that is not mine; mutating shared planning state here would entangle their work. Requirements KEY-01 and KEY-04 must not be marked complete — this plan did not close them.