Files
archy/docs/security/KEY-01-ON-NODE-VERIFICATION.md
T
archipelagoandClaude Opus 5 f2f89b5fe3 docs(10-02): record C-6 evidence so far — probe-method correction, 3 transports still open
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>
2026-08-02 14:25:05 -04:00

12 KiB
Raw Blame History

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:80 and [::]:80 (ss -ltn), i.e. on every interface, not on loopback only. The daemon itself is bound loopback-only on 127.0.0.1:5678, so all external reachability is via nginx.
  • The host packet filter does not block port 80: iptables -S INPUT is -P INPUT ACCEPT with a single jump into Tailscale's chain, and the nft ruleset contains only Tailscale's ts-input/ts-forward chains — 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.

  1. 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
  2. Tor exposure. torsocks bash scripts/security/rpc-exposure-probe.sh --target <onion> --scheme http --port 80 --label tor
  3. 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/v1core/archipelago/src/server.rs:1270-1296 — so a 200 confirms the mesh half of F-01's reachability claim). An unreachable transport is recorded as UNREACHABLE with its error, never omitted.
  4. Deploy 10-01 to a disposable node, then, from a second machine: bash scripts/security/rpc-exposure-probe.sh --target <disposable-node> --destructive --label refusal with sudo sha256sum /var/lib/archipelago/identity/node_key /var/lib/archipelago/identity/nostr_secret captured on the node immediately before and after. The response must carry the Not supported: prefix and the two digests must match character for character.
  5. 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 no Rate limit exceeded may appear at any point. Then re-run step 4 against that same instance to confirm the door closed behind onboarding.
  6. Check the remaining fleet for the onboarding.json-without-user.json state above.

Until items 13 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.