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>
This commit is contained in:
archipelago
2026-08-02 14:25:05 -04:00
co-authored by Claude Opus 5
parent 257ca7e6ac
commit f2f89b5fe3
@@ -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 <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:
```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 <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/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 <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.