From f2f89b5fe36497ebe618fbe060f59cdc194d7f86 Mon Sep 17 00:00:00 2001 From: archipelago Date: Sun, 2 Aug 2026 14:25:05 -0400 Subject: [PATCH] =?UTF-8?q?docs(10-02):=20record=20C-6=20evidence=20so=20f?= =?UTF-8?q?ar=20=E2=80=94=20probe-method=20correction,=203=20transports=20?= =?UTF-8?q?still=20open?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- docs/security/KEY-01-ON-NODE-VERIFICATION.md | 224 +++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 docs/security/KEY-01-ON-NODE-VERIFICATION.md diff --git a/docs/security/KEY-01-ON-NODE-VERIFICATION.md b/docs/security/KEY-01-ON-NODE-VERIFICATION.md new file mode 100644 index 00000000..ae5109c9 --- /dev/null +++ b/docs/security/KEY-01-ON-NODE-VERIFICATION.md @@ -0,0 +1,224 @@ +# 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.