# 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 --scheme http --port 80 --label self-lan-ip RPC exposure probe — label=self-lan-ip endpoint=http://: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: ```bash 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 --scheme http --port 80 --label lan` 2. **Tor exposure.** `torsocks bash scripts/security/rpc-exposure-probe.sh --target --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 --scheme http --port 80 --label mesh` (the peer listener allows `/rpc/v1` — `core/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 --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 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.