Your Node's Seed & Entropy
How 32 random bytes become every key your node owns — where that randomness comes from, what protects it, and exactly what your 24 words can (and cannot) bring back.
Introduction
One master secret, many keys — by design.
Almost everything cryptographic on this node — its identity, its Nostr keys, its mesh transport keys, its Lightning wallet — grows from a single master seed: 32 bytes (256 bits) of randomness drawn once, shown to you once as a 24-word recovery phrase, and never stored in raw form anywhere.
This page explains, in both plain and technical language: where those 32 bytes come from, the guardrails that make sure they're genuinely random, how they're protected on disk, everything that is derived from them, everything that deliberately isn't, and what happens in every failure and recovery scenario.
The Big Picture
From kernel randomness to every key on the node.
Where Randomness Comes From
One named source. No mixing. No silent defaults.
Technically
The master seed is generated by MasterSeed::generate() in
core/archipelago/src/seed.rs. It fills a 32-byte buffer using
rand::rngs::OsRng — a thin wrapper around the
getrandom(2) system call, which reads the kernel's CSPRNG
(the same source as /dev/urandom, but immune to file-descriptor
exhaustion and chroot tricks).
- Exactly 32 bytes / 256 bits — the maximum BIP-39 strength, encoding to 24 words.
- The RNG is named at the call site. No function anywhere in the codebase generates key material with a "default" or implicit RNG anymore (see Recent Hardening for why this is stated so emphatically).
- The RNG type is compiler-enforced. Key-generation functions only accept RNGs on a sealed allowlist (
KeyGenRngincore/archipelago/src/entropy.rs). The list has exactly one production member:OsRng. No other module — not even a future refactor — can add a weaker RNG without editing the allowlist file itself. - The buffer is zeroized (securely wiped from memory) on every path, success or failure.
// core/archipelago/src/seed.rs — the actual draw
let mut entropy = [0u8; 32];
crate::entropy::draw_key_bytes(&mut rand::rngs::OsRng, &mut entropy)?; // guarded draw
let mnemonic = bip39::Mnemonic::from_entropy(&entropy)?; // → 24 words
entropy.zeroize(); // wipe the raw bytes
The 24 words are then stretched into the 64-byte master seed using standard BIP-39:
PBKDF2-HMAC-SHA512, 2048 rounds, empty passphrase. (There is no "25th word"
passphrase — your login password protects the stored backup instead, see
The Encrypted Envelope.) That 64-byte seed exists
only in memory, is re-computed from the words whenever needed, and is
never written to disk in any form.
When the Seed Is Born
At onboarding — not at first boot.
First boot: a placeholder key
A freshly-flashed node boots with a random temporary identity key so services can start. This key is NOT seed-derived and is about to be thrown away.
Onboarding: the real draw
When you reach the "Recovery phrase" step of setup, the seed.generate RPC performs the guarded 32-byte draw described above and shows you the 24 words — the only time they're ever displayed unprompted.
Derivation: keys are materialised
The node identity key, DID, Nostr key, FIPS mesh key, and your first personal identity are all derived from the seed and written to /var/lib/archipelago/identity/ (each file mode 0600). The placeholder key from step 1 is overwritten.
Password setup: the backup is sealed
When you set your login password, the 24 words are encrypted under it (Argon2 + ChaCha20-Poly1305) and stored as master_seed.enc — so you can re-reveal them later from Settings → Backup.
The Five Guardrails
Defence in depth around a single random draw.
1 Sealed RNG allowlist
Key draws only compile against RNG types on a closed, private allowlist. Production allowlist: OsRng. Full stop. A refactor that swaps in a weak or deterministic RNG becomes a compile error, not a silent disaster.
2 Degenerate-draw refusal
Every draw is inspected for three tell-tale broken-RNG shapes: all zeros, all bytes identical, or a counting pattern. A match is refused and wiped — never retried, because retrying would mask a broken RNG instead of exposing it.
3 CSPRNG readiness ledger
Before generating the seed, the node probes whether the kernel's random pool is fully initialised, and appends the verdict to a tamper-evident, append-only log at security/csprng-readiness.jsonl (0600). You can audit, forever, the entropy conditions your seed was born under.
4 Build-time lint bans
The CI lint config bans rand::random() and rand::thread_rng() outright across the workspace — the two "convenient" RNG entry points that caused real-world wallet disasters elsewhere. Using either fails the build.
5 Zeroization everywhere
Raw entropy, mnemonics, and derived secrets are wiped from memory on every code path — including error paths — so key material doesn't linger in freed RAM or end up in crash dumps.
What the degenerate check does — and doesn't — do
The check (is_degenerate in entropy.rs) is deliberately
closed-form: it recognises exactly three catastrophic failure shapes
(all-zero, all-identical, ±1 counter). It is not a statistical entropy
estimator — those can't actually distinguish good randomness from a cleverly broken RNG,
and they introduce false positives. The real security load is carried by guardrails 1,
3 and 4; the degenerate check is a tripwire for total RNG failure (e.g. a zeroed buffer
that was never filled).
Recent Hardening (the KEY-05 work)
This system was audited and rebuilt in early August 2026.
Triggered by the COLDCARD-class of entropy defects, a full entropy & seed-generation
audit (docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md) reviewed every random
draw in the codebase. The headline finding: the library used for mnemonic generation was
silently choosing its own RNG via a transitive default. It happened to be a secure one —
but nothing guaranteed that, and a dependency update could have changed it
without any diff in Archipelago's own code.
| Date | Change |
|---|---|
| Jul 30 | Kernel CSPRNG readiness probe added; RNG non-determinism regression test (64 consecutive mnemonics must all be unique). |
| Jul 31 | Full entropy audit published (findings F-01…F-13). |
| Aug 1 | The pivotal fix: master-seed RNG made explicit — OsRng named at the call site, injected through a testable seam, pinned by a known-answer test. |
| Aug 2 | Audit widened: 43 defaulted-RNG call sites across 15 files found and migrated to explicit OsRng — including AEAD nonces and ecash key material. |
| Aug 2 | Onboarding RPCs gated: seed.restore and friends now refuse on an already-provisioned node (previously an unauthenticated restore call could hijack a live node — fixed before any release shipped it). |
| Aug 2 | KEY-05 enforcement layer landed: sealed KeyGenRng allowlist, guarded draws, durable readiness ledger, clippy bans, supply-chain version pinning on the rand crate. |
| Aug 2 | Legacy Bitcoin Core wallet-import path deleted — the master xprv is no longer handed to any external wallet process. |
What's Kept on Disk
The words, encrypted — and the keys, materialised. Never the raw seed.
| File | Contents | Protection |
|---|---|---|
identity/master_seed.enc | Your 24 words (as text), encrypted | Argon2(login password) + ChaCha20-Poly1305, mode 0600 |
identity/node_key / .pub | Node Ed25519 identity key | 0600, seed-derived (recoverable from words) |
identity/nostr_secret / nostr_pubkey | Node Nostr keypair | 0600, seed-derived |
identity/fips_key / .pub | FIPS mesh transport key (bech32 nsec) | 0600, seed-derived |
identity/identity_index | Next unused identity derivation index | Plain integer (not secret) |
identities/<uuid>.json | Personal identity records (keys + metadata) | 0600; keys seed-derived, metadata is not |
identity/lnd_aezeed.enc | Lightning wallet's own seed (see below) | Encrypted under the LND wallet password |
security/csprng-readiness.jsonl | Append-only entropy audit trail | 0600; deliberately outside identity/ so restores never touch it |
The Encrypted Envelope
How master_seed.enc is built.
Revealing the words later (Settings → Backup → Reveal) requires an authenticated session plus re-entering your password plus your 2FA code if enabled, is rate-limited, and the words are returned only to your browser — never written to logs.
The Derivation Tree
Every key, its exact derivation, and where it lands.
| Key | Derivation | Label / path | Materialised at |
|---|---|---|---|
| Node identity (Ed25519) — signs everything, forms your DID | HKDF-SHA256 | archipelago/node/ed25519/v1 | identity/node_key |
| Node Nostr key (secp256k1) — the node's npub | HKDF-SHA256 | archipelago/nostr-node/secp256k1/v1 | identity/nostr_secret |
| FIPS mesh transport key — federation/mesh overlay identity | HKDF-SHA256 | archipelago/fips/secp256k1/v1 | identity/fips_key |
| Personal identity #i (Ed25519) | HKDF-SHA256 | archipelago/identity/{i}/ed25519/v1 | identities/<uuid>.json |
| Personal Nostr key #i — standard NIP-06, importable into other Nostr apps | BIP-32 | m/44'/1237'/0'/0/{i} | identities/<uuid>.json |
| Lightning (LND) wallet entropy — 16 bytes | HKDF-SHA256 | archipelago/lnd/entropy/v1 | fed into LND at wallet init (see below) |
| Bitcoin BIP-84 account xprv — native-segwit | BIP-32 | m/84'/0'/0' | nowhere — dormant, reserved for a future on-node cold vault |
| Release-root signing key — fleet update signing | HKDF-SHA256 | archipelago/release/root/ed25519/v1 | never on a node — derived offline by the publisher from a separate release mnemonic; nodes only pin the public key |
All HKDF derivations are HKDF-SHA256 with a distinct, versioned info-label (the
/v1 suffix means a future algorithm migration can introduce
/v2 labels without ambiguity). Personal Nostr keys intentionally use the
NIP-06 standard path instead of HKDF so that the same 24 words typed into any
NIP-06-compliant Nostr client reproduce the same npub — your social identity is
portable beyond Archipelago.
The Lightning Special Case
The one branch of the tree with a one-way gate in it.
LND (the Lightning node) uses its own seed format called aezeed, which is not BIP-39. Archipelago derives deterministic entropy from your master seed and hands it to LND at wallet-creation time — but LND then wraps it with its own internal random salt, so the resulting aezeed cannot be re-derived from your 24 words afterwards. The node therefore captures the aezeed exactly once, at init, and stores it encrypted alongside your other identity files. The Lightning seed backup screen shows it to you with the same tap-to-reveal flow as the main phrase.
Second-Order Keys
Derived from the node key — so still fully recoverable from the words.
Several subsystems derive their keys from the node identity key rather than the
master seed directly. Since the node key is itself seed-derived, these all regrow from
your 24 words too — the chain is words → master seed → node key → subsystem key.
| Subsystem | Derivation from node_key |
|---|---|
| Reticulum / RNS mesh identity (LoRa long-range mesh) | HKDF-SHA256, salt archipelago-reticulum-identity-v1, two labels for the X25519 + Ed25519 halves — yields a stable LXMF address that survives reinstalls |
| Message-store encryption (chats at rest) | SHA-256("archipelago-message-store-v1" ‖ node_key) |
| Mesh contacts encryption | SHA-256("archipelago-mesh-contacts-v1" ‖ node_key) |
| Credential store (saved app credentials) | Same domain-separated SHA-256 pattern |
What Is NOT Derived From the Seed
Independent randomness — deliberately outside the tree.
Plenty of secrets are freshly random rather than seed-derived. That's intentional: things that should die with a session, rotate freely, or belong to a third-party app must not be recoverable from your words (and mostly you wouldn't want them to be).
Ephemeral by design
Session tokens, device/companion pairing tokens, federation invites, TOTP/2FA secrets and backup codes, all encryption nonces, X3DH ephemeral mesh keys, anonymous marketplace/discovery Nostr keys.
App-owned secrets
Every manifest-declared generated_secret (app database passwords, API keys), Bitcoin RPC credentials, the LND wallet password (distinct from its seed), Home Assistant tokens.
Host-level material
WireGuard VPN keypairs (generated by wg genkey), SSH host keys and the TLS certificate (created by the installer image at first boot), the machine-id.
Opt-outs from derivability
Extra identities created with "new random key" instead of seed derivation, and a node key after an explicit rotate-key — rotation deliberately breaks the link to the words, and says so.
Failure Modes & Fallbacks
What happens when something goes wrong — at every stage.
| Scenario | Behaviour | Outcome |
|---|---|---|
| RNG returns a degenerate pattern (all-zero / repeated / counter) | Draw is refused and wiped, never retried; error logged; onboarding fails loudly | No seed created |
| Kernel random pool not yet initialised (exotic first-boot case) | getrandom(2) blocks until the pool is seeded — an unseeded pool physically cannot produce a seed. The advisory probe logs a warning and records the verdict in the readiness ledger |
Waits, then proceeds |
| Onboarding page refreshed / retried mid-generation | Same words returned for 10 minutes (mutex-serialised); no second seed can be minted | Idempotent |
master_seed.enc missing (e.g. backup write failed during setup) |
Node runs normally — every derived key is already materialised on disk. Only the Reveal feature and future re-derivation are unavailable; the UI says so explicitly | Degraded, functional |
master_seed.enc corrupt, or wrong password at reveal |
Authenticated decryption fails closed with an explicit error — no fallback, no partial output, no auto-regeneration | Fails loudly |
| Restore attempted on an already-provisioned node | The onboarding gate refuses identity-mutating RPCs once the node is set up — a live node cannot be hijacked or accidentally re-seeded | Refused |
| Legacy / corrupt on-disk FIPS key format | Self-heals: legacy raw-byte format is detected and migrated in place to the current bech32 format | Auto-migrated |
| Readiness-ledger write fails (disk full, permissions) | Warns and continues — the audit trail is best-effort and can never block key generation | Non-blocking |
The Restore Flow
Typing 24 words into a fresh node, step by step.
Gate check
Restore only proceeds on an un-onboarded node. A provisioned node refuses — this gate is load-bearing and runs before anything else.
Validation
Exactly 24 words, checked against the BIP-39 wordlist and its built-in checksum — a typo is caught here, before anything is written.
Identity regrowth
Node Ed25519 key + DID, node Nostr key, and FIPS mesh key are re-derived and written — byte-identical to the originals, because the HKDF labels are fixed.
Personal identity #0
The identity index resets to 0 and your default "Personal" identity (Ed25519 + NIP-06 Nostr key) is recreated. If you had created more seed-derived identities, their keys re-derive on demand as the index walks forward — but their names, avatars and profiles were metadata, not key material, and come from a node backup instead.
Mesh reactivation
FIPS federation auto-activation kicks off in the background; the Reticulum identity re-derives from the restored node key, so your LXMF mesh address comes back too.
Password & re-seal
When you set the (new) login password, the words are re-encrypted into a fresh master_seed.enc — the reveal feature works on the restored node just like the original.
What Comes Back — and What Doesn't
Restored by the words
Node identity & DID · node npub · FIPS mesh key · Reticulum/LXMF address · personal identity keys & npubs · message-store / contacts / credential encryption keys · the (dormant) Bitcoin xprv · the ability to reveal the phrase again.
Needs its own backup
Lightning wallet (aezeed — one-way gate, see above) and channel state · chat history & app data (node backup) · identity names/avatars (metadata) · app secrets (regenerate on reinstall).
Gone by design
Sessions & device pairings (re-login, re-pair) · 2FA secret (re-enrol) · WireGuard peers (re-pair) · rotated-away node keys · anonymous throwaway Nostr keys.
SeedQR
Your words as a scannable code — using the open SeedSigner standard.
Wherever the phrase is displayed, a QR code tab is offered alongside the words. For the main (BIP-39) phrase the default format is SeedQR: each word becomes its 4-digit position in the official wordlist (24 words → 96 digits), encoded as a compact numeric QR. Hardware wallets like Passport, SeedSigner and Keystone import this format directly — so you can move your on-chain identity into cold storage without ever typing the words. A plain-text QR fallback exists for wallets that read the phrase as text.
- The QR contains exactly the same secret as the words — treat a printout or screenshot of it with identical care.
- The Lightning aezeed is deliberately never SeedQR-encoded — it isn't BIP-39, hardware wallets can't import it, and pretending otherwise would be dishonest. It gets a plain-text QR with an explanation instead.
- Restore is by typed/pasted words; there's no camera-based SeedQR scanner on the restore path today.
Honest Edges
Known trade-offs and open hardening items, stated plainly.
The audit that produced this system also tracked what it didn't fix. None of these are secrets — honest security means naming the edges:
- The words cross the RPC boundary. During onboarding the phrase travels (over HTTPS/localhost) to your browser to be displayed, sits in the browser's session storage for the wizard's duration, and is held in server memory for the 10-minute idempotence window. This is the deliberate price of a refresh-proof, display-once flow.
- Argon2 parameters are the library defaults (≈19 MiB, 2 passes) rather than the heavier profile the design doc calls for. Still memory-hard and slow for attackers; scheduled for tightening.
- 2FA backup codes carry a slight statistical bias from a modulo operation — cosmetically imperfect, cryptographically irrelevant at their length, and queued for cleanup.
- The lint ban covers the main workspace, but one small helper crate outside the workspace isn't reached by it yet.
- Best-effort backup sealing: if writing
master_seed.encfails during setup, the node continues (keys exist; only reveal is lost) — a loud failure might arguably be better, and this trade-off is under review.
Verify It Yourself
Don't trust — recompute.
Because every derivation is deterministic and label-fixed, you can independently confirm that your node's keys really do come from your words:
- Independent re-derivation script:
scripts/verify-seed-derivation.pyin the Archipelago source — pure-standard-library Python, no Archipelago code. Paste your mnemonic (on a trusted, offline machine) and it recomputesnode_key,nostr_secretandfips_key, byte-comparing them against/var/lib/archipelago/identity/. - Known-answer tests: the test suite pins the exact expected keys for a fixed test mnemonic; any change to the derivation math, however subtle, turns the build red.
- Non-determinism regression test: 64 consecutive generated mnemonics are asserted unique — a canary against the "predictable RNG" failure class.
- Your entropy audit trail:
/var/lib/archipelago/security/csprng-readiness.jsonlrecords, append-only, the kernel randomness verdict at every key-generation event on this node — including the moment your seed was born.
Archipelago — Seed & Entropy Guide · reflects the KEY-05 hardened implementation (August 2026) · see also the LoRa & Mesh Guide