diff --git a/docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md b/docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md new file mode 100644 index 00000000..6f677892 --- /dev/null +++ b/docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md @@ -0,0 +1,809 @@ +# Entropy & Seed-Generation Security Audit — 2026-07-31 + +**Trigger:** the Coinkite COLDCARD entropy incident, disclosed 2026-07-30 (see +`.planning/quick/260731-upz-research-coinkite-conkite-low-entropy-ha/260731-upz-RESEARCH.md`, +"T1"). That defect silently rebound seed generation to a non-cryptographic PRNG through a +build-time macro guard, reducing effective seed entropy to ≤2^32 and enabling a ~1,082 BTC +sweep. This audit asks the same question of Archipelago. + +**Why the stakes here are higher than a hardware wallet's.** Archipelago derives its *entire* +key hierarchy from one 24-word BIP-39 mnemonic (`core/archipelago/src/seed.rs:1-18`): the node +Ed25519 `did:key`, the node Nostr key, the FIPS mesh transport key, per-identity keys, the +BIP-84 Bitcoin wallet, the LND aezeed entropy — **and the fleet release-root signing key** +(`core/archipelago/src/seed.rs:143-146`). A Coldcard-class entropy defect here would not merely +drain wallets; it would let an attacker forge signed release manifests and catalogs for every +node in the fleet. + +**Headline verdict:** **no Coldcard-class entropy defect exists in this codebase.** Every +first-party key-generation call site draws from a genuine CSPRNG, and the code does several +things better than most implementations. The findings below are (a) one structural pattern +that is the *exact shape* of T1 and should be closed cheaply, (b) one **Critical** +access-control defect found while tracing the secret classes — unrelated to entropy but far +more immediately exploitable than anything entropy-related — and (c) a set of Medium/Low +hygiene items. + +--- + +## 1. Scope and method + +### Directories covered + +| Path | Coverage | +|---|---| +| `core/*/src/**/*.rs` | full RNG-API grep sweep + call-graph trace of every secret class | +| `neode-ui/src/**/*.{ts,vue}` | full browser-RNG grep sweep | +| `scripts/**/*.{sh,py}` | RNG / secret-material grep sweep | +| `image-recipe/**` | entropy, seed-file, machine-id, host-key and first-boot ordering evidence | +| `~/.cargo/registry/src/*/bip39-2.1.0/`, `argon2-0.5.3/` | vendored-dependency default-RNG / default-parameter reads | +| `docs/adr/005-chacha20-backup-encryption.md` | Argon2 parameter cross-check | + +### Explicitly excluded, and why + +- **`core/target/`** — build output, not source. Excluded from every grep (the pipeline used + `core/*/src`, which cannot reach it). +- **`image-recipe/_archived/` — NOT excluded, contrary to the original scoping assumption.** + This is a correction the next auditor should not have to re-derive: + `image-recipe/build-debian-iso.sh:19-40` is a thin wrapper that copies + `image-recipe/_archived/build-auto-installer-iso.sh` to a temp path, rewrites its relative + paths, and `exec`s it (`image-recipe/build-debian-iso.sh:40`). **The "archived" auto-installer + IS the live ISO build path.** Treating `_archived/` as dead code would have made [ARCHY-3] + unanswerable. It is therefore in scope and is the primary [ARCHY-3] evidence surface. +- `image-recipe/_archived/build/auto-installer/installer-iso/...` — a stale *build output* tree + under `_archived/`, superseded by the generator above. Its `/dev/urandom` hits + (`image-recipe/_archived/build/auto-installer/installer-iso/archipelago/scripts/first-boot-containers.sh:182`) + are duplicates of the live `scripts/first-boot-containers.sh` and are not separately assessed. + +### Greps run + +``` +grep -rnE 'SmallRng|seed_from_u64|::from_seed\(|rand::rngs::mock|StdRng' core/*/src --include=*.rs +grep -rnE 'OsRng|thread_rng|rand::random|getrandom|SystemRandom' core/*/src --include=*.rs +grep -rn -B3 -A3 -E 'SystemTime::now|as_nanos|Instant::now' core/*/src --include=*.rs \ + | grep -iE 'key|seed|nonce|salt|token|secret|password|mnemonic' +grep -rn -B2 -A2 -E 'Math\.random|getRandomValues|crypto\.subtle|jsbn|SecureRandom\(' \ + neode-ui/src --include=*.ts --include=*.vue +grep -rnE '\$RANDOM|/dev/urandom|/dev/random|openssl rand|uuidgen|random\.random|random\.randint|shuf ' \ + scripts/ image-recipe/ --include=*.sh --include=*.py +grep -rniE 'random-seed|urandom|jitterentropy|haveged|rng-tools|rngd|crng' image-recipe/ \ + --include=*.sh --include=*.service --include=*.conf +find image-recipe -name 'random-seed' -o -name '*.seed' +grep -rniE '(info|warn|error|debug|trace)!\(.*(mnemonic|seed|privkey|private_key|passphrase|aezeed)' \ + core/*/src --include=*.rs +grep -rn 'derive(Debug' core/archipelago/src/seed.rs core/archipelago/src/identity.rs \ + core/archipelago/src/credentials/store.rs +cargo tree -i rand@0.8.5 -p archipelago ; cargo tree -i rand@0.9.2 -p archipelago +``` + +Results of the two negative greps, stated so they count as findings rather than silence: + +- `find image-recipe -name 'random-seed' -o -name '*.seed'` returned **nothing**. No seed file + is checked into the image recipe. +- `grep -cE 'haveged|jitterentropy|rng-tools|rngd' image-recipe/_archived/build-auto-installer-iso.sh` + returned **0**. No userspace entropy daemon is installed by the image. +- `grep -rnE 'SmallRng|seed_from_u64|rand::rngs::mock|StdRng' core/*/src` returned **no RNG + hits at all** — the four matches are `NodeIdentity::from_seed(...)` calls + (`core/archipelago/src/api/rpc/seed_rpc.rs:122`, `:255`; + `core/archipelago/src/identity.rs:608`, `:634`), which is Archipelago's own + seed-to-identity function, not `rand`'s `from_seed`. **No non-cryptographic PRNG and no + deterministic seeding exists anywhere in the Rust workspace.** + +### Not performed + +- `cargo audit` — **`cargo-audit` is not installed on this host** (`command -v cargo-audit` + fails). No RustSec snapshot was taken. This is recorded as gap **F-07**; the research's + recommendation stands that `cargo audit`/`cargo deny` belongs in CI rather than in a + point-in-time audit. +- Anything requiring real hardware — see §6, the UNVERIFIED on-node checklist. + +### Concurrent-work caveat + +`core/archipelago/src/container/secrets.rs` and `neode-ui/src/views/OnboardingSeedGenerate.vue` +had **uncommitted third-party changes** on disk at audit time (another agent working in the +same tree). They were read as-is and not modified. Line numbers cited for those two files are +against the working-tree state of 2026-07-31, not against `HEAD`. + +--- + +## 2. Executive summary + +Archipelago's entropy path is structurally sound. Every first-party call site that produces key +material draws from `rand::rngs::OsRng` (a direct `getrandom(2)` wrapper) or from +`rand::random`/`rand::thread_rng` on `rand 0.8.5`, which is `ReseedingRng` +— a real CSPRNG that still carries fork protection in the 0.8 series. There is no Mersenne +Twister, no clock-seeded key, no `SmallRng`, no `seed_from_u64`, and no `Math.random()` in any +browser key path. The master-seed function is preceded by a genuinely good, non-blocking +CSPRNG-readiness probe (`core/archipelago/src/seed.rs:52-91`) that most implementations lack, +and the derivation is domain-separated, zeroized, and pinned by known-answer tests. + +Three things nonetheless warrant action, in this order: + +1. **The most urgent finding is not about entropy at all.** While tracing secret classes (3) + and (4), the audit found that `seed.generate` and `seed.restore` are in the + **unauthenticated** RPC allowlist (`core/archipelago/src/api/rpc/middleware.rs:25-27`), carry + **no onboarding-complete gate and no rate limit**, and unconditionally overwrite a live + node's Ed25519 identity, Nostr key and FIPS mesh key + (`core/archipelago/src/identity.rs:79-114`). The endpoint is proxied to the LAN over + plaintext HTTP (`image-recipe/configs/nginx-archipelago.conf:11`, `:165`, `:192`) and is + also reachable by mesh peers (`core/archipelago/src/server.rs:2080`). A guard function for + exactly this already exists and is simply never called + (`core/archipelago/src/identity.rs:117`). **Critical — F-01.** +2. **The T1-shaped structural risk is real but currently benign.** + `bip39::Mnemonic::generate(24)` at `core/archipelago/src/seed.rs:92` delegates its entropy + source to a transitive dependency default. Not a vulnerability today; exactly the pattern + that produced T1. **Medium — F-02**, and the one code change this audit applies. +3. **The one-ISO-many-nodes story is better than feared but has a fail-open hole.** No + `random-seed` file is baked, and per-device TLS/SSH regeneration exists — but the rootfs is + a cached container export shared by every node, the regeneration is fail-open, and its + completion marker is set even when regeneration failed, so a single failure leaves fleet-wide + shared SSH host keys and TLS private key permanently. **High — F-03.** + +Nothing in this audit suggests any existing Archipelago node has a weak master seed. No user +action of the "your seed may be predictable, migrate now" kind is warranted — a point §7 of +`docs/security/PSBT-SIGNING-ARCHITECTURE.md` depends on and must not overstate. + +--- + +## 3. Findings + +| ID | Severity | Title | Primary evidence | +|---|---|---|---| +| F-01 | **Critical** | Unauthenticated, unrated `seed.generate`/`seed.restore` overwrite a live node's identity keys | `core/archipelago/src/api/rpc/middleware.rs:25`, `core/archipelago/src/identity.rs:79` | +| F-02 | **Medium** | Master mnemonic's entropy source is a transitive-dependency default, not a call-site argument (T1 shape) | `core/archipelago/src/seed.rs:92` | +| F-03 | **High** | First-boot per-device secret regeneration is fail-open and never retried, over a fleet-shared cached rootfs | `image-recipe/_archived/build-auto-installer-iso.sh:1647`, `:1659`, `:1663` | +| F-04 | **Medium** | Master mnemonic crosses the RPC boundary and is held in memory for 10 min, deliberately un-cleared, over plaintext-capable HTTP | `core/archipelago/src/api/rpc/seed_rpc.rs:147`, `:205-211` | +| F-05 | **Medium** | `Argon2::default()` is 19 MiB / t=2, not ADR-005's stated 64 MB / 3 iterations | `core/archipelago/src/seed.rs:249`, `docs/adr/005-chacha20-backup-encryption.md:31` | +| F-06 | **Medium** | Release master mnemonic is passed via env var / stdout in the signing ceremony | `core/archipelago/src/ceremony.rs:71-77`, `:149-160` | +| F-07 | **Medium** | No `cargo audit`/`cargo deny` in CI; two `rand` majors coexist in the graph | `core/archipelago/Cargo.toml:68` | +| F-08 | **Low** | 24-word master mnemonic persisted in browser `sessionStorage` during onboarding | `neode-ui/src/views/OnboardingSeedGenerate.vue:330` | +| F-09 | **Low** | Modulo bias in TOTP backup-code generation | `core/archipelago/src/totp.rs:305` | +| F-10 | **Low** | Container `generated_secrets` use `thread_rng()` rather than an explicit `OsRng` (same T1 shape as F-02, smaller blast radius) | `core/archipelago/src/container/secrets.rs:92`, `:101` | +| F-11 | **Informational** | `Math.random()` inside a seed-handling view (benign — UX challenge selection only) | `neode-ui/src/views/OnboardingSeedVerify.vue:159` | +| F-12 | **Informational** | Identical default OS credentials on every flashed node | `image-recipe/archipelago-scripts/install-to-disk.sh:205` | + +--- + +### F-01 — Unauthenticated `seed.generate` / `seed.restore` overwrite a live node's identity keys — **Critical** + +**Evidence.** +- `core/archipelago/src/api/rpc/middleware.rs:24-28` places `seed.generate`, `seed.verify`, + `seed.restore` and `seed.save-encrypted` in `UNAUTHENTICATED_METHODS`, under the comment + "Onboarding flow (before user has a session)". +- `core/archipelago/src/api/rpc/mod.rs:263-265` — membership in that list skips the entire + session check; `:295` skips RBAC; `:326` skips CSRF. +- `core/archipelago/src/api/rpc/seed_rpc.rs:93-159` (`handle_seed_generate`) and `:226-305` + (`handle_seed_restore`) contain **no** check that onboarding is already complete or that a + node key already exists. +- `core/archipelago/src/identity.rs:79-114` (`NodeIdentity::from_seed`) writes `node_key`, + `node_key.pub` and, via `write_fips_key_from_seed` (`:108`), the FIPS mesh key — + **unconditionally, with no existence check.** `seed_rpc.rs:130-131` and `:261-266` likewise + overwrite `nostr_secret` / `nostr_pubkey`. +- The guard already exists and is never called on this path: + `core/archipelago/src/identity.rs:117-119` (`NodeIdentity::key_exists`). Its only callers are + `core/archipelago/src/server.rs:63` and `core/archipelago/src/api/rpc/seed_rpc.rs:343` + (read-only status). +- No rate limit: `core/archipelago/src/rate_limit.rs:60-97` enumerates per-method limits and + contains **no `seed.*` entry**, while explicitly acknowledging at `:96` that + "Inter-node federation RPCs (unauthenticated, need stricter limits)". +- Reachability: `image-recipe/configs/nginx-archipelago.conf:11` and `:15` bind port 80 as + `default_server` (plaintext, LAN); `:165-175` proxies `/rpc/v1` and `:192-195` proxies + `/rpc/` to `127.0.0.1:5678`. The FIPS mesh peer listener applies a path filter + (`core/archipelago/src/server.rs:1375`, `:1270`) but that filter **allows** `/rpc/v1` — + asserted at `core/archipelago/src/server.rs:2080`. + +**Exploitability.** No credentials, no session, no CSRF token, no rate limit. A single +unauthenticated JSON-RPC POST from anywhere on the LAN — or from any peer that can reach the +mesh listener — is sufficient. `seed.restore` is the worse of the two because the attacker +supplies the mnemonic: they then hold the node's Ed25519 signing key, its Nostr node key and +its FIPS transport key. `seed.generate` is a pure destructive primitive: it mints a mnemonic +nobody ever sees and overwrites the node's identity with it. + +**Blast radius.** Node identity takeover or permanent identity destruction. Downstream: the +node's `did:key` changes, so every federation trust relationship keyed on that DID breaks; the +FIPS mesh key changes, so mesh peering breaks; the Nostr node key changes, so discovery +announcements are signed by a key the fleet does not recognise. This does **not** by itself +expose the user's Bitcoin funds (the on-disk `master_seed.enc` envelope is not overwritten by +these handlers) — but do not read that as reassurance: an attacker who controls the node's +identity keys controls how that node presents itself to the federation. + +**This is not an entropy defect.** It surfaced because Step B of this audit required tracing +secret classes (3) and (4) end-to-end rather than only checking where their bits come from. +It is reported here because it is the most serious thing found and suppressing it until a +"more appropriate" document would be indefensible. + +**Remediation (concrete).** In `handle_seed_generate` and `handle_seed_restore`, bail early +when `NodeIdentity::key_exists(&identity_dir)` is true *and* the in-memory onboarding mnemonic +is absent — i.e. this is a booted, already-provisioned node rather than an onboarding retry. +Prefer additionally gating on `auth_manager.is_onboarding_complete()` +(`core/archipelago/src/auth.rs:182`). Add `seed.generate` / `seed.restore` to +`rate_limit.rs`'s table at the strictness of `auth.changePassword` (3 per 300s). Consider +removing `/rpc/v1` from `is_peer_allowed_path` for seed methods specifically, or filtering by +method rather than path. Needs its own plan — see Backlog R-01. + +--- + +### F-02 — Mnemonic entropy source is a transitive-dependency default — **Medium** — [ARCHY-1], **FIXED IN THIS AUDIT** + +**Evidence.** `core/archipelago/src/seed.rs:92`: + +```rust +let mnemonic = bip39::Mnemonic::generate(24) +``` + +Resolved against the vendored crate: +`~/.cargo/registry/src/index.crates.io-.../bip39-2.1.0/src/lib.rs:311-313` → +`generate_in` at `:296-298`, whose body is +`Mnemonic::generate_in_with(&mut rand::thread_rng(), language, word_count)` → +`generate_in_with` at `:267-283`, which is generic over `R: RngCore + CryptoRng` and fills the +entropy buffer at `:281`. + +So the entropy backend for Archipelago's whole key hierarchy — including the release-root +signing key — was selected by `bip39`'s default, not stated at Archipelago's call site. + +**Exploitability.** **None today.** `rand::thread_rng()` on `rand 0.8.5` +(`core/archipelago/Cargo.toml:68`) is `ReseedingRng`: seeded from +`getrandom(2)`, reseeded every 64 KiB, `CryptoRng`, and still fork-protected in the 0.8 series. +The mnemonic is genuinely 256-bit. This finding is about *future* exploitability, not present. + +**Blast radius (if it ever rebinds).** Total. Every key in `seed.rs:1-18`, including the fleet +release-root signing key at `:143-146`. That is strictly larger than a hardware wallet's, +because it includes the ability to forge signed release manifests. + +**Why it is worth fixing anyway.** This is the precise structural shape of T1: a call whose +entropy backend is fixed by dependency/build configuration rather than by the calling code, +with no compile error if it changes. `bip39` is pinned `=2.1.0` +(`core/archipelago/Cargo.toml:74`) which contains the exposure today, and a future `rand` bump +to 0.9+ removes fork protection (upstream changelog, 2025-01-27) without touching a line of +Archipelago source. + +**Remediation — applied.** `seed.rs` now routes generation through an internal helper that +takes `&mut (impl CryptoRng + RngCore)` and calls `bip39::Mnemonic::generate_in_with` +explicitly, with the production caller passing `OsRng`, plus a known-answer test that drives +generation from a deterministic RNG and asserts the resulting words. That test is impossible +to write against the pre-change code, because there was no seam to inject through. See §7. + +--- + +### F-03 — Fail-open, never-retried first-boot secret regeneration over a fleet-shared rootfs — **High** — part of [ARCHY-3] + +**Evidence.** +- The installed root filesystem is a **container image exported to a tar** + (`image-recipe/_archived/build-auto-installer-iso.sh:717-726`), cached across builds + (`:267`), shipped on the ISO (`:1094`) and extracted verbatim onto every target disk + (`:2303`, `tar -xf "$ROOTFS_TAR" -C /mnt/target`). Every node flashed from one ISO therefore + starts from a byte-identical filesystem. +- That rootfs installs `openssh-server` (`:345`). Debian's `openssh-server` postinst generates + host keys at install time — i.e. **inside the container build** — so SSH host keys are baked + into the shared tar. +- It also bakes a self-signed RSA-2048 TLS keypair at `:463-469` + (`openssl req -x509 -nodes -days 3650 -newkey rsa:2048 ... /etc/archipelago/ssl/archipelago.key`). +- The mitigation exists and is correct in intent: `archipelago-first-boot-secrets.service` + (`:1599-1614`) runs `first-boot-secrets.sh` (`:1616-1665`), which regenerates the TLS keypair + (`:1635-1648`) and the full SSH host-key set via `ssh-keygen -A` into a staging dir and swaps + on success (`:1651-1662`). It is installed at `:2587-2593` and enabled at `:3336`. +- **The hole:** both branches are fail-open — `:1647` "WARNING: TLS regeneration failed, + keeping baked key" and `:1659` "WARNING: ssh-keygen -A failed, keeping baked host keys" — and + `touch "$MARKER"` at `:1663` runs **unconditionally, outside both `if` blocks**. The unit's + `ConditionPathExists=!/var/lib/archipelago/.secrets-regenerated` (`:1605`) and the script's + own `[ -f "$MARKER" ] && exit 0` (`:1625`) then guarantee it **never runs again**. +- Timing: the unit declares `DefaultDependencies=no` and only `After=local-fs.target` + (`:1603-1604`), so it runs very early — precisely when a freshly-flashed headless machine has + the least accumulated entropy, and it is the first consumer of the pool. + +**Exploitability.** One transient failure at first boot (a full disk, a slow-to-seed pool +causing a timeout, an `openssl`/`ssh-keygen` hiccup) permanently leaves that node running the +**image-wide shared** SSH host key and TLS private key. An attacker who obtains one copy of the +ISO — which is a published artifact — holds the SSH host key and TLS private key of every node +that hit that failure path, enabling transparent MITM of the web UI and undetectable SSH host +impersonation. The failure is logged only to `/var/log/archipelago-first-boot-secrets.log` and +surfaces nowhere in the UI. + +**Blast radius.** Per-node, but silently and permanently, and correlated fleet-wide by ISO +build. + +**Remediation.** Move `touch "$MARKER"` inside a success branch that requires *both* +regenerations to have succeeded; on failure, leave the marker absent so the oneshot retries on +the next boot, and surface the condition (a `system.stats`/doctor field, not just a log file). +Additionally add `After=systemd-random-seed.service` — harmless today (no seed file is baked, +see [ARCHY-3]) and correct if one is ever introduced. Independently, strip the baked SSH host +keys and TLS key from the rootfs tar at build time so a regeneration failure degrades to "no +key / service refuses to start" rather than "shared key, silently". + +--- + +### F-04 — Master mnemonic crosses the RPC boundary and lingers in memory — **Medium** — [ARCHY-4] + +**Evidence.** +- `core/archipelago/src/api/rpc/seed_rpc.rs:147` builds `words: Vec` from the mnemonic + and `:156-158` returns it as the JSON-RPC result. +- Held server-side in a process-global `LazyLock>>>` + (`:13-19`) under a 10-minute TTL (`:27`). +- **Deliberately not cleared at verify time** — `:205-211` documents the reasoning (the web + client aborts at 15s and retries; clearing would make a retried verify fail). The rationale is + sound; the residual risk is real and should be named rather than assumed away. +- `save_pending_seed_encrypted` (`:42-57`) deliberately ignores the TTL, documented at `:35-39`. +- Plaintext HTTP is a supported deployment: `core/archipelago/src/api/rpc/mod.rs:227-241` + sets the session cookie's `Secure` flag **only** when `X-Forwarded-Proto: https` is present, + with the comment "On LAN HTTP, Secure flag prevents browsers from sending cookies back" — + i.e. plaintext LAN is an expected mode, corroborated by + `image-recipe/configs/nginx-archipelago.conf:11` binding `:80` as `default_server`. + +**Exploitability.** Passive: anyone with LAN traffic visibility during the ~1-2 minutes of +onboarding reads the 24 words in cleartext. This unlocks the Bitcoin wallet, the node identity, +and — if the same mnemonic is ever used as a release master seed — the fleet signing key. +Requires being on-path during onboarding, which bounds it. + +**Blast radius.** Total for that node's key hierarchy. + +**Mitigating factors (real, and worth stating).** `OnboardingMnemonicState` implements `Drop` +with `zeroize` (`:21-25`); the words are never logged; and `seed.reveal` — the *post*-onboarding +path — is properly gated (see §5). The exposure is confined to the onboarding window. + +**Remediation.** Confine seed-bearing methods to loopback or require TLS for them specifically; +shrink `MNEMONIC_TTL`; clear on a *successful, acknowledged* verify with a short grace window +rather than never. Deferred to a plan — Backlog R-04. + +--- + +### F-05 — `Argon2::default()` does not match ADR-005 — **Medium** + +**Evidence.** `docs/adr/005-chacha20-backup-encryption.md:31` specifies "Argon2id with high +memory cost (64MB) and iterations (3)". The code uses `Argon2::default()` at +`core/archipelago/src/seed.rs:249` and `:285` (the master-seed and aezeed envelope), +`core/archipelago/src/backup/identity.rs:38` and `:93`, and +`core/archipelago/src/backup/full.rs:618` and `:650`. + +From the vendored crate `argon2-0.5.3`: `impl Default for Argon2` (`src/lib.rs:176-180`) uses +`Params::default()`, whose constants are `DEFAULT_M_COST = 19 * 1024` KiB = **19 MiB** +(`src/params.rs:42`), `DEFAULT_T_COST = 2` (`:52`), `DEFAULT_P_COST = 1` (`:61`). + +**Actual: Argon2id, v0x13, m=19456 KiB, t=2, p=1. ADR-005 states: 64 MB, 3 iterations.** The +algorithm choice (Argon2id) is correct; the cost parameters are roughly 3.4× weaker in memory +and 1.5× weaker in time than the ADR claims. The defaults are the current OWASP minimum, so +this is a documentation-vs-code divergence and a modest hardening gap, not a break. + +**Exploitability.** Offline brute force of `master_seed.enc` / backup blobs by an attacker who +already has file read access, at a lower cost than the ADR promises. + +**Remediation.** Either construct `Argon2::new(Algorithm::Argon2id, Version::V0x13, +Params::new(65536, 3, 1, None)?)` in one shared helper and use it everywhere, **or** amend +ADR-005 to state the real parameters. Do **not** silently change the parameters on the +master-seed envelope without a migration path: an existing `master_seed.enc` was encrypted +under the old parameters and would fail to decrypt. That constraint is what makes this a +backlog item rather than a quick fix. + +--- + +### F-06 — Release master mnemonic passed by env var / printed to stdout — **Medium** + +**Evidence.** `core/archipelago/src/ceremony.rs:70-78` (`cmd_gen`) prints +`RELEASE_MASTER_MNEMONIC="<24 words>"` to **stdout** via `println!`. `:149-153` +(`load_release_root_key`) reads the phrase via `read_mnemonic()`, which at `:157-160` prefers +the `RELEASE_MASTER_MNEMONIC` environment variable and falls back to stdin. + +**Exploitability.** An environment variable is readable from `/proc//environ` by the same +user and lands in shell history if set inline; stdout lands in terminal scrollback, tmux +buffers, CI logs and `script`/asciinema captures. This is the seed that derives the **fleet +release-root signing key** (`core/archipelago/src/seed.rs:143-146`) — compromise means forging +signed manifests for every node. + +**Mitigating factors.** The ceremony is a deliberate, human-operated, offline procedure, the +tool prints a prominent warning at `ceremony.rs:73-75`, and the stdin path exists and is the +documented practice (project memory: "sign via user TTY"). The env-var path is a convenience +affordance, not the intended default. + +**Remediation.** Make stdin/TTY the only supported input for `sign`/`pubkey` and remove or +feature-gate the env-var branch; for `gen`, write the mnemonic to a `0600` file on explicitly +named removable media rather than stdout, or require an interactive confirmation. Low effort, +but it touches the signing ceremony — schedule it deliberately, not opportunistically. + +--- + +### F-07 — No dependency-advisory gate in CI; two `rand` majors in the graph — **Medium** + +**Evidence.** `cargo-audit` is not installed on this host, so no RustSec check was run. +`cargo tree -i rand@0.8.5 -p archipelago` and `-i rand@0.9.2 -p archipelago` show **both** +majors resolved into the same binary: + +- `rand 0.8.5` — direct (`core/archipelago/Cargo.toml:68`), plus `archipelago-security`, + `bip39 2.1.0`, `mainline 2.0.1`, `secp256k1 0.29.1`, `tungstenite 0.20.1`. +- `rand 0.9.2` — transitively via `totp-rs 5.7.0` and `tungstenite 0.26.2` (through + `tokio-tungstenite` → `async-wsocket` → `nostr-relay-pool` → `nostr-sdk 0.44.1`). + +**No Archipelago-authored key-generation call site uses `rand 0.9.x`** — the direct dependency +is pinned to `0.8.5` and every first-party `OsRng`/`thread_rng`/`rand::random` call resolves +against it. But `rand 0.9.0` removed fork protection from `ThreadRng`, and the orchestrator +forks and spawns constantly, so the day a `rand` bump lands the T1 shape in F-02 and F-10 +becomes materially worse. `getrandom` is likewise split across `0.2.17` and `0.3.4`. + +**Remediation.** Add `cargo audit` (or `cargo deny check advisories bans`) to CI, with a `bans` +rule that fails on duplicate `rand` majors so the split is visible rather than silent. Before +any `rand` 0.9+ bump, convert every key-generation site to explicit `OsRng` (F-02, F-10) — after +which the fork-protection removal is irrelevant to Archipelago. + +--- + +### F-08 — 24-word master mnemonic persisted in browser `sessionStorage` — **Low** + +**Evidence.** `neode-ui/src/views/OnboardingSeedGenerate.vue:330` writes the full word list: +`sessionStorage.setItem('_seed_words', JSON.stringify(words.value))`; it is re-read at `:297` +and at `neode-ui/src/views/OnboardingSeedVerify.vue:165`. The mnemonic itself arrives from +`seed.generate` at `OnboardingSeedGenerate.vue:256-258`. + +**Mitigating factors.** It **is** removed on successful verify +(`neode-ui/src/views/OnboardingSeedVerify.vue:251`), and its exclusion from the logout +cache-purge is a deliberate, test-pinned decision +(`neode-ui/src/stores/__tests__/resourcesClear.test.ts:213`, `:231`) — onboarding must survive a +reload. So this is a considered trade-off, not an oversight. + +**Residual risk.** A user who abandons onboarding mid-flow leaves the master mnemonic in +plaintext `sessionStorage` for the lifetime of the tab. On the node's own kiosk browser, that +tab may stay open indefinitely. Any XSS in the UI during that window reads it directly. + +**Remediation.** Clear `_seed_words` on route-leave from the onboarding flow as well as on +verify, and add a wall-clock expiry to the stored blob mirroring the server's `MNEMONIC_TTL`. + +--- + +### F-09 — Modulo bias in TOTP backup-code generation — **Low** — [ARCHY-5] + +**Evidence.** `core/archipelago/src/totp.rs:305`: + +```rust +let idx = (rand::random::() as usize) % charset.len(); +``` + +with `charset` = 32 characters (`:298`). **32 divides 256 exactly**, so in the *current* code +the bias is **zero** — the research's [ARCHY-5] framing of "classic modulo bias" is correct as a +pattern but the concrete instance is presently unbiased. The defect is latent: any future edit +to the charset (adding a symbol, removing an ambiguous letter) silently introduces bias with no +test to catch it. Reported as Low on that basis, not on present harm. + +**Remediation.** Replace with `rand::seq::SliceRandom::choose(&mut OsRng)`, which is +unbiased for any charset length, and add an assertion or test that pins the property. Left to +the backlog rather than applied here: the entropy source is already correct and the present +bias is nil, so it does not meet this plan's bar for a code change. + +--- + +### F-10 — Container `generated_secrets` use `thread_rng()` — **Low** + +**Evidence.** `core/archipelago/src/container/secrets.rs:90-93` (`random_hex`) and `:98-102` +(`random_base64`) both use `rand::thread_rng().fill_bytes(&mut buf)`. These materialise +manifest-declared `generated_secrets` for every app (Bitcoin RPC password, DB passwords, +netbird store encryption key, the Fedimint gateway credential at `:135-...`). + +**Assessment.** Cryptographically fine on `rand 0.8.5` for the same reason as F-02, and the same +T1-shaped structural objection applies with a smaller blast radius (per-app credentials rather +than the master key hierarchy). File permissions were verified rather than assumed: +`core/archipelago/src/container/secrets.rs:207` sets `.mode(0o600)` on creation, and `:269` and +`:307` are tests asserting `mode == 0o600` for the written files. **CLAUDE.md's "0600/rootless" +invariant holds and is test-enforced.** + +*(This file carried uncommitted third-party changes at audit time — line numbers are against the +2026-07-31 working tree.)* + +**Remediation.** Swap both helpers to `rand::rngs::OsRng` when F-02's pattern is generalised. +One-line change each; batched into the same backlog item. + +--- + +### F-11 — `Math.random()` inside a seed-handling view — **Informational (benign)** + +**Evidence.** `neode-ui/src/views/OnboardingSeedVerify.vue:157-163`, `pickRandomIndices` uses +`Math.floor(Math.random() * max)` to choose which of the 24 words the user is quizzed on. + +**Assessment: benign, and annotated here so the next auditor does not re-derive it.** The +indices select a UX challenge only. They are not key material, not a nonce, not a salt, and not +a secret: an attacker who predicts perfectly which words will be quizzed learns nothing — the +words themselves are what they would need, and those are already on the user's screen. The +verification is a *user*-facing "did you write it down" check, not an authentication boundary +(the server compares against its own held copy at +`core/archipelago/src/api/rpc/seed_rpc.rs:190-194`). + +Other `Math.random()` sites, all confirmed non-security: +`neode-ui/src/api/rpc-client.ts:183`, `:206`, `:215` (retry jitter); +`neode-ui/src/views/Login.vue:317` (progress bar); +`neode-ui/src/components/BootScreen.vue:112`, `:123` (starfield animation). + +**No remediation required.** Optionally add a one-line comment at the call site so this stays +annotated in the code rather than only in this document. + +--- + +### F-12 — Identical default OS credentials on every flashed node — **Informational** + +**Evidence.** `image-recipe/archipelago-scripts/install-to-disk.sh:205` sets +`archipelago:archipelago` via `chpasswd`, and `:367-371` prints the credentials with a +"Please change the password after first login!" warning. + +**Assessment.** Not an entropy defect and a known, documented alpha-stage default. Recorded here +only because it belongs to the same one-image-many-nodes correlation theme as [ARCHY-3]: it is +the one identity artefact that is *deliberately* identical across the fleet, and unlike the SSH +host key and TLS key (F-03) there is no first-boot regeneration for it. Out of scope to fix; +in scope to name. + +--- + +## 4. [ARCHY-1] … [ARCHY-4] adjudication + +### [ARCHY-1] — **CONFIRMED** + +The research's claim that `bip39::Mnemonic::generate(24)` at `core/archipelago/src/seed.rs:92` +resolves its entropy source through a transitive default is **exactly right**, and the citation +is accurate: `bip39-2.1.0/src/lib.rs:296-298` is `generate_in`, whose body is +`Mnemonic::generate_in_with(&mut rand::thread_rng(), language, word_count)`. The full chain is +`generate` (`:311-313`) → `generate_in` (`:296-298`) → `generate_in_with` (`:267-283`). + +**The entropy source is chosen by the dependency, not at the call site.** The injectable seam +exists and is public — `generate_in_with` — so closing this costs +almost nothing. It is **not** a vulnerability today (`rand 0.8.5`'s `thread_rng` is a +fork-protected ChaCha12 CSPRNG seeded from `getrandom(2)`), but it is the structural shape of +T1. **Fixed in this audit — see §7 and F-02.** + +### [ARCHY-2] — **CONFIRMED (as a positive finding)** + +`kernel_csprng_ready()` at `core/archipelago/src/seed.rs:58-75` calls +`libc::getrandom(..., libc::GRND_NONBLOCK)` (`:62-67`), maps a 1-byte success to `Some(true)` +(`:68-69`), `EAGAIN` to `Some(false)` (`:70-71`), and anything else to `None` (`:73`). The +single byte it draws is **discarded** — `byte` is never read again. It is used only by +`MasterSeed::generate` at `:85-91` to emit `info!` or `warn!`. + +**No key material is drawn from the non-blocking path.** The actual mnemonic entropy comes from +`bip39::Mnemonic::generate(24)` at `:92`, i.e. `getrandom(2)` **without** `GRND_NONBLOCK`, which +blocks until the pool is initialised. The doc comment at `:52-57` states this reasoning +correctly. The research's assessment — "exactly right and better than most implementations" — +holds. The two hardening notes it raised also hold and are carried to the backlog: the +invariant depends on the `getrandom` crate using the blocking syscall (worth a test, not just a +comment), and the `warn!` should be persisted as a structured, durable event so a node can +answer post-hoc "was the pool ready when this seed was born?" — the question Coldcard owners +cannot answer today. + +### [ARCHY-3] — **PARTIALLY CONFIRMED; the tree answers three of four sub-questions, the fourth is UNVERIFIED** + +First, a scoping correction the research could not have known: `image-recipe/_archived/` is +**not** dead. `image-recipe/build-debian-iso.sh:19-40` execs +`image-recipe/_archived/build-auto-installer-iso.sh`. That file is the ISO builder. + +| Sub-question | Verdict | Evidence | +|---|---|---| +| Does the build bake a populated seed file into the image? | **NO** | `find image-recipe -name 'random-seed' -o -name '*.seed'` → empty. The rootfs is a container export (`build-auto-installer-iso.sh:717-726`); `systemd-random-seed.service` never runs inside a container build, so `/var/lib/systemd/random-seed` is never created. The installer extracts that tar (`:2303`) and adds no seed file. | +| Is there a first-boot regeneration unit? | **YES, for TLS + SSH host keys — but it is fail-open and never retried** | `archipelago-first-boot-secrets.service` at `:1599-1614`, script at `:1616-1665`, installed `:2587-2593`, enabled `:3336`. Hole documented as **F-03** (`:1647`, `:1659`, `:1663`). It does **not** touch `/etc/machine-id` or any random-seed file. | +| Does the image install `jitterentropy-rngd` / `haveged` / `rng-tools`? | **NO** | `grep -cE 'haveged\|jitterentropy\|rng-tools\|rngd' image-recipe/_archived/build-auto-installer-iso.sh` → `0`. The rootfs package list at `:330-352` and following contains no entropy daemon. Kernel ≥5.6's in-kernel jitter source is therefore the only supplemental source on headless hardware. | +| Can onboarding key generation run before the kernel CSPRNG is initialised? | **NO — it can be *delayed* by it, but never weakened** | `bip39` fills entropy via `rand`'s `OsRng`/`ThreadRng` seeding, i.e. blocking `getrandom(2)`. `core/archipelago/src/seed.rs:52-57` documents exactly this and the probe at `:85-91` makes the ordering visible in the logs. The failure mode is a hang, not a weak key — the correct trade. | + +**What remains genuinely UNVERIFIED.** Whether `/etc/machine-id` is empty (regenerated per node) +or populated (shared) in the exported rootfs tar; whether SSH host keys are in fact present in +that tar as the `openssh-server` install at `:345` implies; the real `crng init done` timestamp +relative to seed generation on freshly-flashed hardware; and whether N nodes flashed from one +ISO actually produce N distinct seeds. **None of these is answerable from this environment.** +They are the on-node checklist in §6 and must not be reported as verified. + +**Net assessment.** The most-feared version of [ARCHY-3] — a baked, credited `random-seed` +giving every node a correlated pool — **does not exist**. The real exposure is narrower and +different from what the research predicted: fleet-shared SSH host keys and a fleet-shared TLS +private key in the cached rootfs, protected by a regeneration step that fails open and never +retries (F-03). + +### [ARCHY-4] — **CONFIRMED, and worse than described** + +Every specific claim checks out: + +- The mnemonic is returned to the web client as `words: Vec` — + `core/archipelago/src/api/rpc/seed_rpc.rs:147`, returned at `:156-158`. (The research cited + "~line 147"; exact.) +- 10-minute in-memory TTL — `MNEMONIC_TTL` at `:27`, state struct at `:16-19`. +- Deliberately **not** cleared at verify time, with a documented rationale — `:205-211`. + (Research cited `:205-209`; the comment block runs `:205-211`.) +- Plaintext HTTP is a live mode — `core/archipelago/src/api/rpc/mod.rs:227-241` conditions the + cookie `Secure` flag on `X-Forwarded-Proto: https` and comments explicitly on "LAN HTTP"; + `image-recipe/configs/nginx-archipelago.conf:11`, `:15` bind `:80` as `default_server` and + `:165-195` proxy `/rpc/v1` and `/rpc/` to the daemon. + +**Worse than described:** the research treated this as a confidentiality exposure. It is also an +**integrity and availability** exposure, because the same four seed methods are in +`UNAUTHENTICATED_METHODS` (`core/archipelago/src/api/rpc/middleware.rs:24-28`) with no +onboarding gate and no rate limit, and the handlers overwrite live identity keys +unconditionally. That is **F-01**, severity Critical. + +### [ARCHY-5] — **CONFIRMED as a pattern, REFUTED as a present defect** + +The line is exactly as cited (`core/archipelago/src/totp.rs:305`) but the charset at `:298` is +32 characters, and 32 divides 256 exactly, so the current distribution is **uniform — there is +no bias today**. The research's characterisation ("classic modulo bias whenever +`charset.len()` does not divide 256") is technically precise; its implied conclusion that this +instance is biased is not. Recorded as **F-09**, Low, on latent-defect grounds only. Stated +plainly rather than quietly dropped, per this audit's honesty rule. + +### Open question 9 (Argon2 parameters) — **DIVERGENCE CONFIRMED** + +`Argon2::default()` = Argon2id, v0x13, **m=19456 KiB (19 MiB), t=2, p=1** +(`argon2-0.5.3/src/params.rs:42`, `:52`, `:61`; `src/lib.rs:176-180`). +`docs/adr/005-chacha20-backup-encryption.md:31` states **64 MB and 3 iterations**. The code does +not match the ADR. Full detail and the migration constraint are in **F-05**. + +### Also noted from the research, confirmed benign + +`core/archipelago/src/storage_crypto.rs:39` and `core/archipelago/src/credentials/store.rs:69` +draw 96-bit ChaCha20-Poly1305 nonces via `rand::random()`. CSPRNG-backed; fine. The +random-nonce birthday bound (~2^32 messages per key) is not approached by either use. Same for +`core/archipelago/src/mesh/crypto.rs:70` (explicit `OsRng`, with a correct explanatory comment +at `:64`), `core/archipelago/src/fips/dial.rs:75` (a 16-bit dial ID, not a secret), and +`core/archipelago/src/wallet/bdhke.rs:133`, `:139`. + +--- + +## 5. What we do right + +Credit where the code is correct — each with evidence, so a future refactor that removes any of +these is visibly a regression. + +1. **The CSPRNG-readiness probe.** `core/archipelago/src/seed.rs:52-91`. Uses `GRND_NONBLOCK` + *as a probe only*, discards the byte, and logs the pool state immediately before generating + the master seed. The doc comment reasons correctly about why blocking `getrandom(2)` makes a + weak seed impossible. This is better than most wallet implementations and is precisely the + audit trail Coldcard owners now wish they had. +2. **Zeroization is real, not decorative.** `MasterSeed` is `#[derive(Zeroize, ZeroizeOnDrop)]` + (`core/archipelago/src/seed.rs:47-50`); the Argon2-derived key is explicitly zeroized on both + the encrypt and decrypt paths (`:262`, `:292`); the aezeed plaintext join is zeroized after + use (`:384`, `:401`); the in-memory onboarding mnemonic zeroizes on `Drop` + (`core/archipelago/src/api/rpc/seed_rpc.rs:21-25`); the reveal path zeroizes the password on + every exit (`:396`, `:430`, `:441`, `:465`). +3. **No `#[derive(Debug)]` on any secret-bearing type.** + `grep -rn 'derive(Debug' core/archipelago/src/seed.rs core/archipelago/src/identity.rs + core/archipelago/src/credentials/store.rs` returns **nothing** — the classic accidental-log + escape is closed by construction. +4. **No secret is logged.** The secret-logging grep across `core/*/src` returned only + non-secret status lines. The most sensitive one, + `core/archipelago/src/seed.rs:86` ("kernel CSPRNG initialized; generating master seed"), + contains no material. `core/archipelago/src/identity.rs:103-106` logs only the first 16 hex + chars of a **public** key. The file-level invariant at `core/archipelago/src/seed.rs:18` + ("Never log mnemonic or seed material at any level") is actually honoured. +5. **Encrypted-at-rest envelope with per-blob salt and nonce from `OsRng`.** + `core/archipelago/src/seed.rs:243-246`, AEAD at `:253-260`, and every identity blob written + `0600` via a single shared helper (`:318-324`). One implementation, not five. +6. **24-word enforcement on restore.** `core/archipelago/src/seed.rs:111-114` rejects any word + count other than 24, so a 12-word (128-bit) mnemonic cannot be smuggled into a hierarchy that + assumes 256 bits. +7. **Domain-separated derivation, pinned by known-answer tests.** Distinct HKDF info strings + per key class (`core/archipelago/src/seed.rs:37-41`), with KATs that pin the exact bytes: + `:764-779` (node key, cross-checked against `scripts/verify-seed-derivation.py`) and + `:800-816` (release-root private *and* public key). A derivation change cannot land silently. +8. **An existing non-determinism regression guard.** `core/archipelago/src/seed.rs:597-622` + generates 64 mnemonics and asserts both uniqueness and word-distribution spread, with a + comment naming exactly the failure it guards against. This is a genuinely good instinct that + predates the Coldcard incident — it would have caught a Yasmarang-class collapse. +9. **`seed.reveal` is properly gated.** `core/archipelago/src/api/rpc/seed_rpc.rs:360-369`: + authenticated session required (it is deliberately *not* in the unauthenticated allowlist), + password re-verification, replay-protected TOTP when 2FA is on, and separate backup-passphrase + decryption. The contrast with F-01's ungated `seed.generate`/`seed.restore` is what makes + F-01 look like an oversight rather than a design position. +10. **Correct browser RNG at the call sites that matter.** + `neode-ui/src/views/OnboardingVerify.vue:105-109` uses `crypto.getRandomValues` for the + 32-byte signing challenge; `neode-ui/src/views/web5/Web5.vue:183-185` does the same, and + guards on `crypto.subtle` being absent — which is exactly right, because `subtle` is + undefined in an insecure context while `getRandomValues` keeps working over plain HTTP. +11. **Container secret file modes are test-enforced, not assumed.** + `core/archipelago/src/container/secrets.rs:207` sets `0o600`; `:269` and `:307` are tests + asserting it. CLAUDE.md's invariant is mechanically defended. +12. **The release-root key is derived, not stored, and nodes hold only the public half.** + `core/archipelago/src/seed.rs:133-146` documents the publisher-only derivation; + `core/archipelago/src/trust/anchor.rs:34` pins the public key. Fleet nodes never hold the + signing key. +13. **The FIPS mesh peer listener is path-filtered.** `core/archipelago/src/server.rs:1375`, + `:1270`. The mechanism is right even though its current allowlist is too permissive for + seed methods (F-01). + +--- + +## 6. On-node verification checklist — **UNVERIFIED** + +**Every item below is UNVERIFIED.** None was executed. Real hardware — a freshly-flashed node, +`.228`, or the dev-box — is not reachable from the environment this audit ran in. Do not treat +any of these as checked until an operator has run them and recorded the output. + +**Run on a *freshly flashed* node, before completing onboarding, unless noted.** + +### C-1 — Was the kernel CSPRNG ready when keys were generated? ([ARCHY-3]) + +```bash +journalctl -b | grep -iE 'crng init|random: ' +journalctl -b -u archipelago | grep -i 'kernel CSPRNG' +cat /proc/sys/kernel/random/entropy_avail +systemd-analyze blame | grep -iE 'random|archipelago-first-boot-secrets' +``` +**Pass:** `crng init done` timestamp strictly precedes the +`kernel CSPRNG initialized; generating master seed` line from +`core/archipelago/src/seed.rs:86`. A `not yet initialized` warn line from `:87-89` is the +signal to escalate. + +### C-2 — Is a seed file present, and is `machine-id` unique? ([ARCHY-3]) + +```bash +ls -l /var/lib/systemd/random-seed /var/lib/urandom/random-seed 2>&1 +cat /etc/machine-id +``` +**Pass:** either no seed file at first boot, or one created *after* first boot with a +current mtime. `machine-id` must differ between two nodes flashed from the same ISO — run on +both and compare. + +### C-3 — Are SSH host keys and the TLS key per-node? (**F-03**, the highest-value check here) + +On two nodes flashed from the same ISO: +```bash +for f in /etc/ssh/ssh_host_*_key.pub; do echo "$f: $(ssh-keygen -lf "$f")"; done +openssl x509 -in /etc/archipelago/ssl/archipelago.crt -noout -fingerprint -sha256 +cat /var/lib/archipelago/.secrets-regenerated 2>&1; ls -l /var/lib/archipelago/.secrets-regenerated +grep -i warning /var/log/archipelago-first-boot-secrets.log +``` +**Fail:** any fingerprint matching between the two nodes, or any `WARNING:` line in the log +alongside an existing `.secrets-regenerated` marker (that combination is exactly the fail-open +path at `image-recipe/_archived/build-auto-installer-iso.sh:1647`/`:1659`/`:1663`). + +### C-4 — Does the shipped rootfs tar contain identity artefacts? (**F-03**, run on the *build host*) + +```bash +tar -tvf /archipelago-rootfs.tar | grep -E 'etc/ssh/ssh_host|etc/machine-id|var/lib/systemd/random-seed|archipelago/ssl/archipelago.key' +``` +**Expected:** SSH host keys and the TLS key **present** (they are baked — see +`build-auto-installer-iso.sh:345`, `:463-469`), `random-seed` **absent**, `machine-id` absent +or zero-length. Anything else changes F-03's severity. + +### C-5 — Cross-node same-ISO seed collision test (the empirical proof that would have caught T1) + +Flash N ≥ 3 nodes from one ISO. On each, without user interaction: +```bash +curl -s -X POST http://127.0.0.1:5678/rpc/v1 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","id":1,"method":"seed.generate","params":null}' \ + | sha256sum +``` +**Pass:** N distinct digests. **Handle the output as key material** — these are real mnemonics; +compare digests only, never the words, and re-provision every node used for this test. +Do **not** run this against a node in real use — per F-01 it overwrites the node's identity. + +### C-6 — Is the RPC endpoint reachable unauthenticated from the LAN? (**F-01**) + +From a *different* machine on the same LAN, against a **disposable** node: +```bash +curl -s -o /dev/null -w '%{http_code}\n' -X POST http:///rpc/v1 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","id":1,"method":"seed.status","params":null}' +``` +**Fail:** `200`. Use `seed.status` (read-only), **never** `seed.generate`/`seed.restore`, +to probe this. Repeat over the Tor onion address and over the FIPS mesh ULA to establish the +full exposure surface. + +### C-7 — Is the daemon's memory swappable? + +```bash +systemctl cat archipelago.service | grep -E 'MemoryDenyWriteExecute|LimitMEMLOCK' +swapon --show +``` +Informational: the onboarding mnemonic lives in process memory for up to 10 minutes (F-04) and +`image-recipe/archipelago-scripts/install-to-disk.sh:226-236` creates a 2-8 GB swapfile on +every install. + +--- + +## 7. `ARCHY-1` remediation status + + + +Everything else in this document is queued in §8, not implemented. + +--- + +## 8. Remediation Backlog + + + +--- + +## 9. Related documents + +- `docs/security/PSBT-SIGNING-ARCHITECTURE.md` — the signing architecture this audit's + conclusions feed into (watch-only descriptors, PSBT, multisig, honest LND limits). +- `docs/hardware-signer-design.md` — exploratory TROPIC01 air-gapped signer. +- `docs/adr/005-chacha20-backup-encryption.md` — the ADR that F-05 diverges from. +- `.planning/quick/260731-upz-research-coinkite-conkite-low-entropy-ha/260731-upz-RESEARCH.md` + — the incident analysis, the T1-T7 catalogue, and the audit checklist this document executed.