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:
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 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.
|
||||
Reference in New Issue
Block a user