C-6 is NOT closed by this commit and is not marked verified.
Measured (read-only, on-node):
- loopback and self-LAN-IP: auth.isOnboardingComplete 200 (EXPOSED),
seed.status 401 (session enforcement intact) — no stop-the-plan finding.
- /rpc/ returns 404: nginx's second proxy block is not a second door, so
the unauthenticated surface is reachable through /rpc/v1 only.
NOT measured — needs a second machine: LAN, Tor, FIPS mesh ULA.
NOT performed — the KEY-01 refusal check and the fresh-node onboarding
walkthrough. No node runs 10-01's gate yet: the installed binary was built
at 06:37 and 879de59e landed at 13:05, and the refusal string is absent
from it. A --destructive run against the dev-box would replace its identity
rather than be refused, so it was not made.
Also carries 10-01's pre-OTA fleet check (onboarding.json complete-true
without user.json): dev-box safe, rest of fleet unchecked.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
12 KiB
KEY-01 on-node verification — audit item C-6 and the F-01 refusal proof
Status: INCOMPLETE — C-6 is NOT yet verified.
Opened: 2026-08-02 · Phase: 10 (key-material hardening) · Plan: 10-02
Probe: scripts/security/rpc-exposure-probe.sh
This document records on-node evidence for
docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md §6 item C-6 ("Is the RPC endpoint
reachable unauthenticated from the LAN?") and for the KEY-01 / F-01 refusal shipped by
plan 10-01 (core/archipelago/src/api/rpc/onboarding_gate.rs, commit 879de59e).
Nothing below is recorded unless it was actually executed and its output observed. Rows marked NOT MEASURED are open work, not assumptions. Per threat T-10-13 this document records node labels and status codes only — never raw LAN addresses, onion addresses or mesh ULAs, because this repository is being prepared for open-sourcing.
Probe-method correction
The audit's own C-6 command cannot detect the condition it claims to test. Do not re-derive this; it has now been checked against the code twice.
ENTROPY-SEED-AUDIT-2026-07-31.md:890-901 probes with seed.status and declares
200 a failure. But seed.status is not in UNAUTHENTICATED_METHODS
(core/archipelago/src/api/rpc/middleware.rs:5-38, which lists seed.generate,
seed.verify, seed.restore and seed.save-encrypted — not seed.status). An
unauthenticated seed.status is therefore rejected at
core/archipelago/src/api/rpc/mod.rs:293 with a 401 by design. The audit's "Fail:
200" criterion can never fire, so the probe would report the unauthenticated surface as
closed while F-01's actual door stands open.
scripts/security/rpc-exposure-probe.sh measures the two facts separately:
| Signal | Method | Why | Reading |
|---|---|---|---|
| Exposure | auth.isOnboardingComplete |
genuinely unauthenticated (middleware.rs:9), read-only, no side effects |
200 = the unauthenticated RPC surface is reachable from this vantage point. This is the honest C-6 result. |
| Session enforcement | seed.status |
deliberately not allowlisted | 401 = the session check is working. Anything else is a worse finding than C-6 and halts the phase. |
The probe reports a reachable unauthenticated surface as EXPOSED, not FAIL: on the LAN
this is the current expected posture, and the purpose of C-6 is to measure the surface,
not to assert it is already closed.
C-6 — unauthenticated RPC reachability
Result table
| Transport | Label | health |
auth.isOnboardingComplete (exposure) |
seed.status (enforcement) |
Status |
|---|---|---|---|---|---|
| Loopback | loopback |
200 | 200 — EXPOSED | 401 — PASS | measured 2026-08-02 |
| Node's own LAN address, probed from the node itself | self-lan-ip |
200 | 200 — EXPOSED | 401 — PASS | measured 2026-08-02 |
| LAN, from a second machine | lan |
— | — | — | NOT MEASURED |
| Tor onion | tor |
— | — | — | NOT MEASURED |
| FIPS mesh ULA, from a peer node | mesh |
— | — | — | NOT MEASURED |
seed.status returned 401 on every vantage point actually tested. No
stop-the-plan condition was observed.
Why the two measured rows are NOT a C-6 result
Both runs originated on the node under test. Packets to the node's own addresses are delivered by the local stack and never traverse the LAN, so neither run exercises the external path an attacker would use, and neither run passes through any host or upstream filtering that applies only to foreign packets. They are recorded because they establish two real facts — the probe works against a live daemon, and session enforcement is intact — but C-6 asks specifically whether a different machine can reach the surface, and that question is still open.
Verbatim probe output (measured rows)
$ bash scripts/security/rpc-exposure-probe.sh --target 127.0.0.1 --scheme http --port 80 --label loopback
RPC exposure probe — label=loopback endpoint=http://127.0.0.1:80
audit item C-6 · KEY-01 (F-01) · read-only mode
[loopback] health 200 REACHABLE endpoint answers from this vantage point
[loopback] auth.isOnboardingComplete 200 EXPOSED unauthenticated RPC surface IS reachable from here (C-6 result)
[loopback] seed.status 401 PASS session enforcement active for non-allowlisted methods
[loopback] auth.isOnboardingComplete (/rpc/) 404 NOT-EXPOSED alternate proxy path did not answer 200
exit=0
$ bash scripts/security/rpc-exposure-probe.sh --target <node-lan-ip> --scheme http --port 80 --label self-lan-ip
RPC exposure probe — label=self-lan-ip endpoint=http://<node-lan-ip>:80
audit item C-6 · KEY-01 (F-01) · read-only mode
[self-lan-ip] health 200 REACHABLE endpoint answers from this vantage point
[self-lan-ip] auth.isOnboardingComplete 200 EXPOSED unauthenticated RPC surface IS reachable from here (C-6 result)
[self-lan-ip] seed.status 401 PASS session enforcement active for non-allowlisted methods
[self-lan-ip] auth.isOnboardingComplete (/rpc/) 404 NOT-EXPOSED alternate proxy path did not answer 200
exit=0
Corroborating host state (observed, but NOT a substitute for the LAN measurement)
Recorded because it predicts the LAN result and tells the operator what to expect:
- nginx listens on
0.0.0.0:80and[::]:80(ss -ltn), i.e. on every interface, not on loopback only. The daemon itself is bound loopback-only on127.0.0.1:5678, so all external reachability is via nginx. - The host packet filter does not block port 80:
iptables -S INPUTis-P INPUT ACCEPTwith a single jump into Tailscale's chain, and thenftruleset contains only Tailscale'sts-input/ts-forwardchains — no rule matching tcp/80.
Together these make an EXPOSED LAN result very likely. That is a prediction, not a
measurement, and C-6 stays open until a second machine produces the status code.
Incidental finding — /rpc/ is not a second door
auth.isOnboardingComplete on nginx's location /rpc/ block
(image-recipe/configs/nginx-archipelago.conf:192) returned 404 from both vantage
points. The block proxies the full URI to the backend, which only routes /rpc/v1, so
the unauthenticated surface is reachable through exactly one path. This narrows F-01's
exposure surface by one path and should be re-checked if the nginx config changes.
KEY-01 refusal check — NOT PERFORMED
Requirement: on a node running 10-01's gate, an unauthenticated seed.restore
carrying attacker-supplied words is refused, and identity/node_key and
identity/nostr_secret are byte-identical afterwards.
Blocker — no node in the fleet is running 10-01's gate yet. Verified on the dev-box rather than assumed:
$ 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='%H %ci' 879de59e
879de59eccb489d590c8e0fca6ae79098df68200 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 at 06:37; 10-01 landed at 13:05 the same day, and the
gate's refusal string is absent from the running binary. A --destructive run against
this node would therefore not be refused — it would replace the node's identity. The
dev-box is a live dev-pair deploy target in real use, so the run was not made.
This check is blocked on deployment, which the phase brief explicitly excludes from this plan. It cannot be closed by any amount of work inside the repository.
Fresh-node onboarding non-regression — NOT PERFORMED
Requirement: a genuinely un-onboarded instance completes the whole wizard with 10-01's
gate in place (the anti-brick proof for correctness trap 1 and the D-03a signal
correction), then refuses seed.restore immediately afterwards.
Blocker — no un-onboarded instance exists. The intended 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/ARCHIPELAGO_BIND/
ARCHIPELAGO_PORT_OFFSET), and that todo is still pending — the harness has not been
built. It would additionally need a binary built from 879de59e or later, which the
running daemon is not.
Note for whoever builds it: that todo records that several constants ignore
ARCHIPELAGO_DATA_DIR and point at /var/lib/archipelago literally
(bitcoin_rpc.rs:10, container/lnd.rs:131, electrs_status.rs:15,
api/rpc/package/pine_ha.rs:34-36, bootstrap.rs:242, disk_monitor.rs:41), so a shape-A
instance must not install Bitcoin, LND, electrumx or Pine/HA — it would read and write the
live node's files. The onboarding walkthrough this check needs does not install apps, so
the hazard is avoidable, not blocking.
Pre-OTA fleet check carried over from 10-01
10-01's summary records a state that its gate makes unrecoverable: a node with
onboarding.json = {"complete": true} but no user.json can no longer call
auth.setup, and the recovery path needs a session it cannot create. Recovery is one SSH
command (rm /var/lib/archipelago/onboarding.json), but the fleet must be checked
before the OTA ships (D-10).
| Node label | user.json |
onboarding.json |
Verdict |
|---|---|---|---|
| dev-box | PRESENT | {"complete": true} |
safe — provisioned normally; the gate refuses re-keying, which is the intent |
| rest of fleet | — | — | NOT CHECKED |
Command to run per node:
ls -l /var/lib/archipelago/user.json /var/lib/archipelago/onboarding.json 2>&1
cat /var/lib/archipelago/onboarding.json 2>/dev/null
A node is at risk only if onboarding.json says complete: true and user.json is
absent.
What is still required to close C-6 and KEY-01
Every item below needs an operator with fleet access; none can be done from the repository.
- LAN exposure. From a second machine on the node's LAN:
bash scripts/security/rpc-exposure-probe.sh --target <node-lan-ip> --scheme http --port 80 --label lan - Tor exposure.
torsocks bash scripts/security/rpc-exposure-probe.sh --target <onion> --scheme http --port 80 --label tor - Mesh exposure. From a peer node over the FIPS mesh ULA:
bash scripts/security/rpc-exposure-probe.sh --target <fips-ula> --scheme http --port 80 --label mesh(the peer listener allows/rpc/v1—core/archipelago/src/server.rs:1270-1296— so a200confirms the mesh half of F-01's reachability claim). An unreachable transport is recorded asUNREACHABLEwith its error, never omitted. - Deploy 10-01 to a disposable node, then, from a second machine:
bash scripts/security/rpc-exposure-probe.sh --target <disposable-node> --destructive --label refusalwithsudo sha256sum /var/lib/archipelago/identity/node_key /var/lib/archipelago/identity/nostr_secretcaptured on the node immediately before and after. The response must carry theNot supported:prefix and the two digests must match character for character. - Build shape (A) and walk the wizard end to end on a 10-01 binary
(intro → options → path → seed → seed-verify → did → identity → backup → verify → done,
then set the password), reloading once on the seed screen to confirm the same 24 words
return. No
Not supported:and noRate limit exceededmay appear at any point. Then re-run step 4 against that same instance to confirm the door closed behind onboarding. - Check the remaining fleet for the
onboarding.json-without-user.jsonstate above.
Until items 1–3 are done, audit item C-6 remains UNVERIFIED. Until item 4 is done, the KEY-01 refusal is proven only by 10-01's unit tests against temp directories, never against a running daemon over HTTP.