docs(10-02): summary — probe built and committed, C-6 still UNVERIFIED
Task 1 done. Tasks 2 and 3 are blocked on unmet preconditions and were
NOT auto-approved: no fleet node runs 10-01's gate (installed binary
predates 879de59e and lacks the refusal string), and no second machine
was available to probe from. Nothing moved UNVERIFIED -> VERIFIED.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f2f89b5fe3
commit
0214114c7b
@@ -0,0 +1,252 @@
|
||||
---
|
||||
phase: 10-key-material-hardening
|
||||
plan: 02
|
||||
subsystem: security-verification
|
||||
tags: [security, KEY-01, KEY-04, F-01, C-6, probe, unverified]
|
||||
requires:
|
||||
- "10-01 — api::rpc::onboarding_gate (the gate whose refusal this plan must prove on hardware)"
|
||||
provides:
|
||||
- "scripts/security/rpc-exposure-probe.sh — repeatable, read-only-by-default RPC exposure probe"
|
||||
- "docs/security/KEY-01-ON-NODE-VERIFICATION.md — the evidence record for C-6 and the F-01 refusal"
|
||||
- "The probe-method correction: auth.isOnboardingComplete, not seed.status, is the C-6 exposure signal"
|
||||
affects:
|
||||
- "audit item C-6 (still UNVERIFIED — narrowed, not closed)"
|
||||
- "D-10 OTA gating: the refusal check is now provably blocked on deployment, not on repo work"
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "Read-only by construction: probe methods come from a fixed array, never from an argument"
|
||||
- "Mutating requests confined to a single explicit --destructive branch"
|
||||
- "Published BIP-39 test vector instead of minting real mnemonics (the C-5 anti-pattern)"
|
||||
key-files:
|
||||
created:
|
||||
- scripts/security/rpc-exposure-probe.sh
|
||||
- docs/security/KEY-01-ON-NODE-VERIFICATION.md
|
||||
modified: []
|
||||
decisions:
|
||||
- "C-6 recorded as NOT VERIFIED rather than closed on loopback evidence — probing from the node measures the local stack, not the LAN"
|
||||
- "The --destructive refusal check was NOT run: no node in the fleet carries 10-01's gate yet, so it would have replaced the dev-box's identity instead of being refused"
|
||||
- "The /rpc/ nginx block returns 404 — the unauthenticated surface is reachable through /rpc/v1 only"
|
||||
metrics:
|
||||
duration: "~35m"
|
||||
completed: 2026-08-02
|
||||
status: blocked
|
||||
---
|
||||
|
||||
# Phase 10 Plan 02: On-Node Verification of KEY-01 / C-6 Summary
|
||||
|
||||
The probe exists, is safe by construction, and has been run — but **audit item C-6 is still
|
||||
UNVERIFIED and the KEY-01 refusal is still unproven on hardware**, because no node in the fleet
|
||||
is running 10-01's gate and no second machine was available to probe from.
|
||||
|
||||
## Status: Task 1 complete · Tasks 2 and 3 BLOCKED on unmet preconditions
|
||||
|
||||
| Task | Type | Outcome |
|
||||
|---|---|---|
|
||||
| 1. Build the read-only-by-default probe | auto | **Done**, committed `527f6023` |
|
||||
| 2. Measure C-6 on real nodes, prove the refusal | checkpoint (blocking) | **BLOCKED** — precondition unmet |
|
||||
| 3. Fresh-node onboarding non-regression | checkpoint (blocking) | **BLOCKED** — precondition unmet |
|
||||
|
||||
Per the plan's `autonomous: false` posture, neither checkpoint was auto-approved and no result
|
||||
was recorded that was not observed.
|
||||
|
||||
## What Was Built
|
||||
|
||||
**`scripts/security/rpc-exposure-probe.sh`** (new, 296 lines, mode 755)
|
||||
|
||||
| Flag / symbol | Contract |
|
||||
|---|---|
|
||||
| `--target <host>` | required; host, onion or ULA. Bare IPv6 is bracketed automatically so the mesh ULA can be probed |
|
||||
| `--scheme http\|https` · `--port N` · `--label <name>` | defaults `http` / `80` / `unlabelled` |
|
||||
| `--insecure` | **added beyond the plan** — accept a self-signed cert on `https`; without it every https vantage point is a false `UNREACHABLE` |
|
||||
| `--destructive` | the single mutating branch: the KEY-01 refusal check |
|
||||
| `READONLY_METHODS` | `health`, `auth.isOnboardingComplete`, `seed.status` — the only methods the default path can call |
|
||||
| exit `0` / non-zero | all controls as expected / `seed.status` was not 401, or `--destructive` was not refused |
|
||||
|
||||
Four requests per read-only run (well under 10-01's 10-per-300s floor, so T-10-15 does not fire):
|
||||
the three methods on `/rpc/v1`, plus the exposure signal repeated on nginx's `/rpc/` block.
|
||||
|
||||
Safety properties, as required by the threat model:
|
||||
|
||||
- **T-10-11:** the method string is built from the fixed array, never from an argument; every
|
||||
mutating request is inside one `if [ "$DESTRUCTIVE" = "1" ]` branch behind a red
|
||||
disposable-nodes-only banner.
|
||||
- **T-10-12:** the refusal check uses the published BIP-39 all-`abandon` + `art` vector (32 zero
|
||||
bytes). The script never generates and never prints a mnemonic — the deliberate difference from
|
||||
audit item C-5, which mints real ones.
|
||||
- **T-10-13:** no node address, onion address, username or password is embedded
|
||||
(`grep -nE '([0-9]{1,3}\.){3}[0-9]{1,3}|\.onion|password'` matches only the safety comment
|
||||
that forbids them).
|
||||
|
||||
The before/after byte-identity check is not attempted by the script (it has no node-local file
|
||||
access); it prints the two `sha256sum` commands so they land in the operator's transcript.
|
||||
|
||||
## The Probe-Method Correction (the substantive finding)
|
||||
|
||||
The audit's C-6 command (`ENTROPY-SEED-AUDIT-2026-07-31.md:890-901`) probes with `seed.status`
|
||||
and calls `200` a failure. `seed.status` is **not** in `UNAUTHENTICATED_METHODS`
|
||||
(`middleware.rs:5-38`), so it is rejected at `api/rpc/mod.rs:293` with a **401 by design** — the
|
||||
audit's failure criterion can never fire, and the probe reports the surface CLOSED while F-01's
|
||||
door stands open. The probe therefore measures exposure with `auth.isOnboardingComplete`
|
||||
(genuinely allowlisted at `middleware.rs:9`, read-only) and keeps `seed.status` as the
|
||||
session-enforcement control. Recorded in the evidence document so it is not re-derived a third
|
||||
time.
|
||||
|
||||
## What Was Actually Measured
|
||||
|
||||
Both runs originated **on the node under test**, so neither is a C-6 result — they are recorded
|
||||
as `loopback` and `self-lan-ip`, not `lan`.
|
||||
|
||||
| Vantage | `health` | `auth.isOnboardingComplete` | `seed.status` | `/rpc/` |
|
||||
|---|---|---|---|---|
|
||||
| `loopback` | 200 | **200 — EXPOSED** | **401 — PASS** | 404 |
|
||||
| `self-lan-ip` | 200 | **200 — EXPOSED** | **401 — PASS** | 404 |
|
||||
|
||||
- **`seed.status` returned 401 on every vantage tested** — no stop-the-plan finding.
|
||||
- **Incidental finding:** `/rpc/` returns **404**. nginx's second proxy block
|
||||
(`nginx-archipelago.conf:192`) forwards the full URI and the backend routes only `/rpc/v1`, so
|
||||
the unauthenticated surface has exactly one path. This narrows F-01's exposure surface.
|
||||
- **Corroborating, not measurement:** nginx binds `0.0.0.0:80` and `[::]:80`, the daemon is
|
||||
loopback-only on `:5678`, and the host filter has no rule matching tcp/80 (`-P INPUT ACCEPT`,
|
||||
nft ruleset is Tailscale chains only). A LAN `EXPOSED` result is very likely — but likely is
|
||||
not measured, and C-6 stays open.
|
||||
|
||||
## Why Tasks 2 and 3 Are Blocked (verified, not assumed)
|
||||
|
||||
**No node in the fleet is running 10-01's gate.** Checked on the dev-box rather than inferred:
|
||||
|
||||
```
|
||||
$ 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='%ci' 879de59e
|
||||
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 ~6.5h before 10-01 landed, and the gate's refusal string is absent
|
||||
from it. A `--destructive` run against this node would therefore **not** be refused — it would
|
||||
replace `node_key`, `nostr_secret` and `fips_key` on a live dev-pair deploy target that is gated
|
||||
before every OTA. It was not run. The plan's own threat model (T-10-11) and the phase brief
|
||||
(which excludes deployment) make this a hard block, not a judgement call.
|
||||
|
||||
Task 3's 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`. That todo is still **pending**: the harness does not
|
||||
exist, and it would additionally need a binary built from `879de59e` or later.
|
||||
|
||||
Task 2 also requires a second machine on the LAN. This session ran on the node itself, and
|
||||
probing fleet nodes uninvited is out of bounds (`.228` is in real use).
|
||||
|
||||
## Pre-OTA Fleet Check Carried Over from 10-01
|
||||
|
||||
10-01 flagged a state its gate makes unrecoverable — `onboarding.json` complete-true with no
|
||||
`user.json` — and asked 10-02 to sweep the fleet before the OTA (D-10).
|
||||
|
||||
| Node | `user.json` | `onboarding.json` | Verdict |
|
||||
|---|---|---|---|
|
||||
| dev-box | PRESENT | `{"complete": true}` | **safe** — provisioned normally |
|
||||
| rest of fleet | — | — | **NOT CHECKED** |
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
**1. [Rule 1 — Bug] `curl` failure fallback produced a `000000` status code**
|
||||
|
||||
- **Found during:** Task 1 verification against a dead port.
|
||||
- **Issue:** `HTTP_CODE=$(curl … -w '%{http_code}' … || echo "000")` — curl already emits `000`
|
||||
when no response arrives, so the fallback *appended* a second `000`. Every verdict then fell
|
||||
through to the wildcard branch and an unreachable host was reported as
|
||||
`seed.status … CRITICAL — session enforcement is NOT working`, exit 1. A false critical on an
|
||||
unreachable transport is exactly the misreading this plan exists to prevent.
|
||||
- **Fix:** the fallback now replaces rather than appends (`if ! HTTP_CODE=$(curl …); then
|
||||
HTTP_CODE="000"; fi`) plus a three-digit normalisation. Retested: an unreachable target now
|
||||
reports `UNREACHABLE` on all four lines and exits 0, since an unreachable vantage point is a
|
||||
result, not a control failure.
|
||||
- **Commit:** `527f6023` (fixed before the commit).
|
||||
|
||||
**2. [Rule 2 — Missing critical functionality] `--insecure` flag added**
|
||||
|
||||
- **Issue:** the plan's CLI contract has no way to accept a self-signed certificate, but
|
||||
Archipelago nodes serve https with one. Every `--scheme https` probe would have reported a
|
||||
false `UNREACHABLE`, silently under-measuring the exposure surface.
|
||||
- **Fix:** opt-in `--insecure`, off by default, documented in `--help`. It does not alter the
|
||||
plan's flag contract.
|
||||
|
||||
**3. Tasks 2 and 3 not executed** — see the blocked section above. No result was recorded that
|
||||
was not observed; nothing was marked verified.
|
||||
|
||||
## Verification
|
||||
|
||||
- `bash -n scripts/security/rpc-exposure-probe.sh` → exits 0.
|
||||
- `bash scripts/security/rpc-exposure-probe.sh --help` → prints usage, exits 0.
|
||||
- `test -x` → mode `755` (`-rwxr-xr-x`).
|
||||
- `grep -c READONLY_METHODS` → **22** (≥1 required); `grep -c DESTRUCTIVE` → **5** (≥2 required).
|
||||
- Missing `--target` → exit 2; unknown argument → exit 2; unreachable target → all `UNREACHABLE`,
|
||||
exit 0; live daemon → `401` on the enforcement control, exit 0.
|
||||
- **`shellcheck` is NOT installed on this host** (`command -v shellcheck` → empty). The
|
||||
acceptance criterion's shellcheck run was therefore not performed, recorded here rather than
|
||||
silently skipped.
|
||||
- Both commits stage exactly one file each by explicit path; `git diff --diff-filter=D` over both
|
||||
is empty. Three other agents are working in this tree (plans 10-04, 10-06, 01-18) and none of
|
||||
their files were staged, reverted or modified.
|
||||
|
||||
## Known Stubs
|
||||
|
||||
None in code. The **evidence document is deliberately incomplete** and says so in its first
|
||||
line — five of its rows are `NOT MEASURED` / `NOT PERFORMED` with the exact command needed to
|
||||
close each.
|
||||
|
||||
## Audit Checklist Movement
|
||||
|
||||
| Item | Before | After |
|
||||
|---|---|---|
|
||||
| C-6 | UNVERIFIED | **still UNVERIFIED** — method corrected, tooling built, 2 non-qualifying vantage points recorded, 3 transports outstanding |
|
||||
| KEY-01 refusal on hardware | unproven | **still unproven** — blocked on deploying 10-01 |
|
||||
| Fresh-node onboarding non-regression | unproven | **still unproven** — blocked on the shape-A harness |
|
||||
|
||||
**Nothing moved from UNVERIFIED to VERIFIED in this plan.** What changed is that the remaining
|
||||
work is now mechanised (one command per transport), correctly specified (the method correction),
|
||||
and provably blocked on deployment rather than on anything doable in the repository.
|
||||
|
||||
## What Is Still Required
|
||||
|
||||
1. LAN: `rpc-exposure-probe.sh --target <node-lan-ip> --scheme http --port 80 --label lan` **from a second machine**.
|
||||
2. Tor: `torsocks rpc-exposure-probe.sh --target <onion> --label tor`.
|
||||
3. Mesh: `rpc-exposure-probe.sh --target <fips-ula> --label mesh` from a peer node.
|
||||
4. Deploy `879de59e`+ to a **disposable** node, capture
|
||||
`sudo sha256sum /var/lib/archipelago/identity/{node_key,nostr_secret}`, run
|
||||
`--destructive --label refusal` from a second machine, re-capture the digests. Response must
|
||||
carry `Not supported:` and the digests must match character for character.
|
||||
5. Build shape (A), walk the wizard end to end on a 10-01 binary (reloading once on the seed
|
||||
screen to confirm the same 24 words return, and seeing no `Not supported:` /
|
||||
`Rate limit exceeded`), then re-run step 4 against it.
|
||||
6. Sweep the remaining fleet for `onboarding.json` complete-true without `user.json`.
|
||||
|
||||
## Threat Flags
|
||||
|
||||
None. No new endpoint, no dependency (T-10-SC did not fire — `curl`, `grep` and `mktemp` are
|
||||
pre-existing host tools), no schema change. The one new surface is the `--destructive` branch,
|
||||
which is the plan's own T-10-11 and is mitigated as specified.
|
||||
|
||||
## Commits
|
||||
|
||||
- `527f6023` — `feat(10-02): add read-only-by-default RPC exposure probe (C-6 / KEY-01)` —
|
||||
`scripts/security/rpc-exposure-probe.sh` only.
|
||||
- `f2f89b5f` — `docs(10-02): record C-6 evidence so far — probe-method correction, 3 transports still open` —
|
||||
`docs/security/KEY-01-ON-NODE-VERIFICATION.md` only.
|
||||
|
||||
Not pushed — the orchestrator pushes.
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
Both files exist on disk (`scripts/security/rpc-exposure-probe.sh` mode 755, 296 lines;
|
||||
`docs/security/KEY-01-ON-NODE-VERIFICATION.md` 224 lines, contains `C-6` and
|
||||
`rpc-exposure-probe`); both commits `527f6023` and `f2f89b5f` are present in `git log`; the probe
|
||||
contains `DESTRUCTIVE` (5) and `READONLY_METHODS` (22); neither file contains a node address,
|
||||
onion address or credential.
|
||||
|
||||
**Not done by this agent, deliberately:** `STATE.md` / `ROADMAP.md` / `REQUIREMENTS.md` updates.
|
||||
Three other agents are executing concurrently in this shared tree and `.planning/STATE.md`
|
||||
already carries an uncommitted edit that is not mine; mutating shared planning state here would
|
||||
entangle their work. Requirements KEY-01 and KEY-04 must **not** be marked complete — this plan
|
||||
did not close them.
|
||||
Reference in New Issue
Block a user