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 |
|
|
|
|
|
|
|
|
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+artvector (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.statusreturned 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:80and[::]: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 LANEXPOSEDresult 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 emits000when no response arrives, so the fallback appended a second000. Every verdict then fell through to the wildcard branch and an unreachable host was reported asseed.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 reportsUNREACHABLEon 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 httpsprobe would have reported a falseUNREACHABLE, 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→ mode755(-rwxr-xr-x).grep -c READONLY_METHODS→ 22 (≥1 required);grep -c DESTRUCTIVE→ 5 (≥2 required).- Missing
--target→ exit 2; unknown argument → exit 2; unreachable target → allUNREACHABLE, exit 0; live daemon →401on the enforcement control, exit 0. shellcheckis 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=Dover 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
- LAN:
rpc-exposure-probe.sh --target <node-lan-ip> --scheme http --port 80 --label lanfrom a second machine. - Tor:
torsocks rpc-exposure-probe.sh --target <onion> --label tor. - Mesh:
rpc-exposure-probe.sh --target <fips-ula> --label meshfrom a peer node. - Deploy
879de59e+ to a disposable node, capturesudo sha256sum /var/lib/archipelago/identity/{node_key,nostr_secret}, run--destructive --label refusalfrom a second machine, re-capture the digests. Response must carryNot supported:and the digests must match character for character. - 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. - Sweep the remaining fleet for
onboarding.jsoncomplete-true withoutuser.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
527f6023—feat(10-02): add read-only-by-default RPC exposure probe (C-6 / KEY-01)—scripts/security/rpc-exposure-probe.shonly.f2f89b5f—docs(10-02): record C-6 evidence so far — probe-method correction, 3 transports still open—docs/security/KEY-01-ON-NODE-VERIFICATION.mdonly.
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.