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.

256-bit entropy BIP-39 · 24 words HKDF-SHA256 derivation Kernel CSPRNG only KEY-05 hardened

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.

The tree and the acorn Think of the seed as an acorn. Every branch of the tree — your identity, your wallet, your mesh radio's name — grows from it in a fixed, repeatable pattern. Plant the same acorn on new hardware (by typing your 24 words) and the same tree grows back, branch for branch. That's why those words are the single most valuable thing your node ever shows you — and why anyone who copies them owns your tree.

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.

Linux kernel CSPRNG (hardware noise: interrupts, timing jitter, CPU RNG) │ │ getrandom(2) — via an explicitly named OsRng, nothing else allowed ▼ 32 bytes of raw entropy ──▶ degenerate-draw check ──▶ refuse & wipe if suspicious │ │ BIP-39 encoding ▼ 24-word recovery phrase ←── the only form you ever see or back up │ │ PBKDF2-HMAC-SHA512 × 2048 (empty passphrase) ▼ 64-byte master seed ←── lives only in RAM, never written to disk │ ├── HKDF "archipelago/node/ed25519/v1" ──▶ Node identity key + DID ├── HKDF "archipelago/nostr-node/…/v1" ──▶ Node Nostr key (npub) ├── HKDF "archipelago/fips/secp256k1/v1" ──▶ FIPS mesh transport key ├── HKDF "archipelago/identity/{i}/…/v1" ──▶ Personal identities (Ed25519) ├── BIP-32 m/44'/1237'/0'/0/{i} (NIP-06) ──▶ Personal Nostr keys ├── HKDF "archipelago/lnd/entropy/v1" ──▶ Lightning wallet entropy → aezeed └── BIP-32 m/84'/0'/0' ──▶ Bitcoin xprv (dormant, reserved) │ │ and from the node identity key, second-order: ▼ ├── Reticulum / RNS mesh identity (HKDF, salt "archipelago-reticulum-identity-v1") ├── Message-store encryption key (SHA-256 domain-separated) ├── Mesh-contacts encryption key (SHA-256 domain-separated) └── Credential-store key (SHA-256 domain-separated)
One rule to remember If it's on the diagram above, your 24 words can rebuild it from scratch, on any hardware, forever. If it's not on the diagram (session tokens, app passwords, WireGuard keys, the Lightning channel state…), it's independent randomness — protected by other backups, not by the words.

Where Randomness Comes From

One named source. No mixing. No silent defaults.

Dice you can audit Computers can't invent randomness — they collect it. The Linux kernel constantly harvests unpredictable physical noise (the exact nanosecond your network card interrupts, timing jitter between CPU cores, the CPU's built-in hardware random generator) and distils it into a cryptographic random pool. Archipelago rolls its dice by asking that pool directly — and only that pool. There is deliberately no "mixing" of other sources, because a single, named, well-studied source is auditable; a blend of sources is a place for bugs to hide.

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).

// 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.

Refresh-proof by design If the onboarding page is retried within 10 minutes (browser refresh, flaky connection), the node returns the same words instead of minting a second seed. Generation is serialised behind a lock — there is no window where two competing seeds can exist.

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.

Why so paranoid about one function? In 2026 a well-known hardware wallet shipped a bug where a code refactor quietly switched seed generation to a predictable random source — with no error, no warning, and seeds that looked perfectly normal. Predictable randomness is invisible: the words look random, the wallet works, and months later someone who can predict the generator drains it. Archipelago's answer is to make that entire class of bug impossible to compile, and to log the health of the random pool at the exact moment your seed was created.

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.

DateChange
Jul 30Kernel CSPRNG readiness probe added; RNG non-determinism regression test (64 consecutive mnemonics must all be unique).
Jul 31Full entropy audit published (findings F-01…F-13).
Aug 1The 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 2Audit widened: 43 defaulted-RNG call sites across 15 files found and migrated to explicit OsRng — including AEAD nonces and ecash key material.
Aug 2Onboarding 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 2KEY-05 enforcement layer landed: sealed KeyGenRng allowlist, guarded draws, durable readiness ledger, clippy bans, supply-chain version pinning on the rand crate.
Aug 2Legacy 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.

FileContentsProtection
identity/master_seed.encYour 24 words (as text), encryptedArgon2(login password) + ChaCha20-Poly1305, mode 0600
identity/node_key / .pubNode Ed25519 identity key0600, seed-derived (recoverable from words)
identity/nostr_secret / nostr_pubkeyNode Nostr keypair0600, seed-derived
identity/fips_key / .pubFIPS mesh transport key (bech32 nsec)0600, seed-derived
identity/identity_indexNext unused identity derivation indexPlain integer (not secret)
identities/<uuid>.jsonPersonal identity records (keys + metadata)0600; keys seed-derived, metadata is not
identity/lnd_aezeed.encLightning wallet's own seed (see below)Encrypted under the LND wallet password
security/csprng-readiness.jsonlAppend-only entropy audit trail0600; deliberately outside identity/ so restores never touch it
The raw seed never touches disk What's stored is the encrypted words and the derived keys. The 64-byte master seed itself is recomputed in RAM from the words when needed (e.g. during restore or identity creation) and wiped afterwards.

The Encrypted Envelope

How master_seed.enc is built.

your login password ──▶ Argon2id (memory-hard KDF) ──▶ 256-bit file key ▲ 16-byte random salt 24 words ──▶ ChaCha20-Poly1305 (authenticated encryption, 12-byte random nonce) │ ▼ ┌───────────┬────────────┬──────────────────────────────┐ │ salt (16) │ nonce (12) │ ciphertext + auth tag │ = master_seed.enc └───────────┴────────────┴──────────────────────────────┘
A safe inside a safe Your words are locked in a digital safe whose combination is your login password — but run through a deliberately slow, memory-hungry grinder (Argon2) so that guessing billions of passwords per second is physically impractical, even for someone who steals the file. The "auth tag" means the safe also notices if anyone has tampered with its contents: a corrupted or modified file fails loudly rather than yielding wrong words.

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.

One password, many doors — without reuse The node never uses the master seed directly as a key. Instead it uses HKDF — think of it as a locksmith who, given one master blank and a label ("node key", "mesh key", "Lightning entropy"…), cuts a completely different, unrelated key for each label. Knowing one cut key tells you nothing about the others or about the blank. The labels are fixed strings baked into the code, which is what makes regrowing the identical tree on new hardware possible.
KeyDerivationLabel / pathMaterialised at
Node identity (Ed25519) — signs everything, forms your DIDHKDF-SHA256archipelago/node/ed25519/v1identity/node_key
Node Nostr key (secp256k1) — the node's npubHKDF-SHA256archipelago/nostr-node/secp256k1/v1identity/nostr_secret
FIPS mesh transport key — federation/mesh overlay identityHKDF-SHA256archipelago/fips/secp256k1/v1identity/fips_key
Personal identity #i (Ed25519)HKDF-SHA256archipelago/identity/{i}/ed25519/v1identities/<uuid>.json
Personal Nostr key #i — standard NIP-06, importable into other Nostr appsBIP-32m/44'/1237'/0'/0/{i}identities/<uuid>.json
Lightning (LND) wallet entropy — 16 bytesHKDF-SHA256archipelago/lnd/entropy/v1fed into LND at wallet init (see below)
Bitcoin BIP-84 account xprv — native-segwitBIP-32m/84'/0'/0'nowhere — dormant, reserved for a future on-node cold vault
Release-root signing key — fleet update signingHKDF-SHA256archipelago/release/root/ed25519/v1never 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.

master seed ──HKDF──▶ 16 bytes of entropy ──▶ LND generates its own "aezeed" ──▶ Lightning wallet │ │ ⚠ one-way: the aezeed cannot be │ recomputed from your 24 words later ▼ captured ONCE at wallet init and stored as identity/lnd_aezeed.enc (encrypted)

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.

Back up the Lightning seed separately Your 24 words alone restore your node identity and on-chain derivations — but not an already-initialised Lightning wallet, and never the off-chain channel balances (those additionally need channel backups, which Lightning requires by design). Treat the LND aezeed shown in the Lightning app's backup screen as a second phrase worth writing down. Note it restores into LND-based wallets (Zeus, Blixt, another Archipelago node) — hardware wallets can't import it.

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.

SubsystemDerivation 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 encryptionSHA-256("archipelago-mesh-contacts-v1" ‖ node_key)
Credential store (saved app credentials)Same domain-separated SHA-256 pattern
Domain separation, in one sentence Each purpose prefixes a unique fixed string before hashing, so even though they start from the same node key, every subsystem ends up with an unrelated key — compromising one never exposes another.

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.

Consequence for backups A seed-only restore brings back everything in the derivation tree but none of the above. App data, chat history and app secrets travel via the separate node backup/restore feature; Lightning channels need channel backups; VPN peers re-pair. Your words are your identity's lifeboat, not a full-system image.

Failure Modes & Fallbacks

What happens when something goes wrong — at every stage.

ScenarioBehaviourOutcome
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 one true single point of failure is you Every software failure above fails safe. The only unrecoverable scenario is losing the 24 words and the node's disk together. Write the words down, store them offline, and never type them into anything except a node you're restoring. Anyone holding them can rebuild your entire identity tree — that's precisely what makes them a perfect backup, and a perfect target.

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.

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:

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:

The whole story in one paragraph Your node asked the Linux kernel for 32 bytes of hardware-grade randomness through a single, named, compiler-enforced channel; refused to proceed unless the bytes looked alive; wrote down the health of the random pool as evidence; turned the bytes into 24 words it showed you exactly once; locked an encrypted copy behind your password; and then grew every identity and key it owns from those words along fixed, versioned, independently-verifiable paths — so that the words in your drawer are, and will remain, a complete blueprint of who your node is.

Archipelago — Seed & Entropy Guide · reflects the KEY-05 hardened implementation (August 2026) · see also the LoRa & Mesh Guide