nix-bitcoin/docs/security-model.md
padreug b2f2e14e2e
Some checks are pending
nix-bitcoin tests / build_test_drivers (push) Waiting to run
nix-bitcoin tests / test_scenario (default) (push) Blocked by required conditions
nix-bitcoin tests / test_scenario (joinmarket-bitcoind-29) (push) Blocked by required conditions
nix-bitcoin tests / test_scenario (netns) (push) Blocked by required conditions
nix-bitcoin tests / test_scenario (netnsRegtest) (push) Blocked by required conditions
nix-bitcoin tests / check_flake (push) Waiting to run
docs: add security model documentation
2026-01-27 17:52:55 +00:00

16 KiB

Security Model

This document explains how nix-bitcoin protects your node and funds. It is intended for operators who may not be familiar with NixOS security primitives.

For vulnerability reporting and the security fund, see SECURITY.md.


Table of Contents


Secrets Management

nix-bitcoin has its own secrets system for managing service credentials. Secrets are files stored in a dedicated directory on the node, protected by unix permissions and never written to the Nix store (which is world-readable).

The secrets directory location depends on the deployment method:

  • Krops / NixOps: /var/src/secrets
  • Flakes / Containers: /etc/nix-bitcoin-secrets

What is stored

Each enabled service registers the secret files it needs. The secrets fall into these categories:

Passwords (random 20-character strings):

  • bitcoin-rpcpassword-privileged, bitcoin-rpcpassword-public — bitcoind RPC authentication
  • lnd-wallet-password — encrypts LND's wallet.db on disk
  • rtl-password, joinmarket-password, btcpayserver-password, lightning-loop-password — web UI and service passwords
  • backup-encryption-env — backup encryption passphrase

Derived credentials (computed from passwords):

  • bitcoin-HMAC-privileged, bitcoin-HMAC-public — HMAC hashes used in bitcoind's rpcauth= config
  • Same pattern for Liquid (liquid-rpcpassword-*, liquid-HMAC-*)

TLS keys and certificates:

  • lnd-key — EC private key (prime256v1) for LND's gRPC/REST API
  • lnd-cert — self-signed x509 certificate (10-year validity)

Other keys:

  • clightning-replication-ssh-key — for database replication
  • WireGuard server/peer private and public keys (when the WireGuard preset is enabled)

When secrets are generated

Secrets generation depends on the deployment method:

Method Where generated Mechanism
Krops Locally (your machine) generate-secrets shell command runs before rsync to target
NixOps Locally (your machine) generate-secrets shell command, transferred via NixOps keys
Flakes On the target node setup-secrets.service at boot with nix-bitcoin.generateSecrets = true
Containers Inside the container Same as Flakes
Manual You create them yourself Set nix-bitcoin.secretsSetupMethod = "manual"

For Krops and NixOps, secrets are generated once locally and then synced to the target. They are idempotent: existing files are never overwritten. If you delete a secret file and redeploy, it will be regenerated.

File permissions

The setup-secrets systemd service enforces permissions on every boot or deploy:

  1. The secrets directory is set to root:root 0700 during setup
  2. Each secret file is assigned ownership and permissions as declared by its module (e.g. lnd-wallet-password is owned by lnd:lnd 0440)
  3. Any file in the directory not claimed by a module is locked to root:root 0440
  4. The directory is opened to 0751 after setup completes, allowing services to access their specific files

All nix-bitcoin services declare a systemd dependency on nix-bitcoin-secrets.target, which is only reached after setup-secrets completes successfully.

What is NOT in the secrets directory

The secrets system manages service-to-service credentials. The following are explicitly outside its scope:

  • LND seed mnemonic (/var/lib/lnd/lnd-seed-mnemonic) — see Wallet Key Material
  • LND wallet database (/var/lib/lnd/chain/bitcoin/<network>/wallet.db)
  • LND macaroons (/var/lib/lnd/chain/bitcoin/<network>/*.macaroon)
  • LND channel backup (/var/lib/lnd/chain/bitcoin/<network>/channel.backup)
  • bitcoind wallet (wallet.dat or descriptors, if created by the operator)

Wallet Key Material

LND

LND uses the aezeed cipher seed scheme — a 24-word mnemonic that encodes the wallet's master entropy and a birthday timestamp.

How the wallet is created on first boot:

The LND module's preStart script checks if wallet.db exists. If not:

  1. lndinit gen-seed generates a fresh 24-word aezeed mnemonic and writes it to /var/lib/lnd/lnd-seed-mnemonic
  2. lndinit init-wallet creates wallet.db using the seed and the lnd-wallet-password from the secrets directory
  3. LND starts and auto-unlocks using the wallet password file

Important details:

  • No cipher seed passphrase: nix-bitcoin does not set an aezeed passphrase. The mnemonic is encrypted with the default passphrase "aezeed", which offers no real protection. Anyone with the 24 words can derive all keys.
  • Auto-unlock tradeoff: The wallet password is stored on disk in the secrets directory so LND can start unattended. This means root access to the node grants access to the wallet.
  • The seed file is only used once: After wallet.db is created, LND never reads the seed file again. It can and should be deleted from disk after backup.
  • Static Channel Backups (SCB): LND maintains channel.backup which is updated atomically every time a channel is opened or closed. It is encrypted with a key derived from the seed. If you re-seed, old SCBs become invalid.

bitcoind

nix-bitcoin does not create or manage a bitcoind wallet. The bitcoind.nix module configures the daemon and RPC authentication only. If you create a wallet via bitcoin-cli createwallet, its key material is entirely your responsibility to manage and back up.


Service Isolation

Unix users and groups

Each nix-bitcoin service runs as its own dedicated system user (e.g. bitcoind, lnd, clightning). Services cannot read each other's data directories unless explicitly granted access through group memberships.

systemd hardening

All nix-bitcoin services apply a strict systemd security profile (pkgs/lib.nix), which includes:

Setting Effect
ProtectSystem = "strict" Filesystem is read-only except for explicitly allowed paths
ProtectHome = true No access to home directories
PrivateTmp = true Isolated /tmp per service
PrivateDevices = true No access to physical devices
NoNewPrivileges = true Cannot gain new privileges via setuid/setgid
MemoryDenyWriteExecute = true Prevents writable+executable memory (JIT disabled)
ProtectKernelTunables = true Cannot modify kernel parameters
ProtectKernelModules = true Cannot load kernel modules
ProtectProc = "invisible" Other processes hidden in /proc
PrivateUsers = true Cannot see other users
IPAddressDeny = "any" All network denied by default (services opt in to what they need)
CapabilityBoundingSet = "" No Linux capabilities
SystemCallFilter Restricted to @system-service syscall set
RestrictAddressFamilies Only AF_UNIX, AF_INET, AF_INET6

For the full list, see man systemd.exec and man systemd.resource-control.

Network namespace isolation

The optional netns-isolation module places each service in its own network namespace. Services can only communicate with other services through explicitly allowed paths. For example, LND can reach bitcoind's RPC, but RTL cannot directly reach bitcoind.

Enable with:

nix-bitcoin.netns-isolation.enable = true;

Note: This is not compatible with the WireGuard preset.

RPC whitelisting

The bitcoind-rpc-public-whitelist module restricts which RPC methods the public RPC user can call. Services that only need read access (like electrs or mempool) use the public user, which cannot call wallet or administrative RPCs. Only the privileged user has full RPC access.

Operator user

The operator module creates a non-root user with group memberships for each enabled service. This lets you run bitcoin-cli, lncli, lightning-cli, etc. without being root. The operator user has read access to service data but does not have write access or the ability to modify service configuration.


Network Security

RPC binding

bitcoind and LND bind their RPC/API interfaces to 127.0.0.1 by default. They are not reachable from outside the machine unless you explicitly change the bind address.

Tor

The secure-node preset imports enable-tor, which routes all outbound traffic from nix-bitcoin services through Tor. Services that support it can also accept inbound connections via onion services.

When Tor enforcement is active (tor.enforce = true), a service's systemd IPAddressAllow is restricted to localhost and link-local addresses only, preventing any clearnet communication.

WireGuard

The wireguard preset creates an encrypted VPN tunnel for connecting a mobile wallet (e.g. Zeus) to your node. It sets up a single-peer WireGuard interface with:

  • The node as server (10.10.0.1)
  • Your device as peer (10.10.0.2)
  • Firewall rules restricting the peer to only reach the node's address (no routing to the broader network)
  • Helper commands (nix-bitcoin-wg-connect, lndconnect-wg) that generate QR codes for one-scan setup

This is an alternative to connecting over Tor, offering lower latency at the cost of requiring a reachable IP and port forwarding for NAT.

Firewall and D-Bus

The secure-node preset enables the NixOS firewall and the security module's D-Bus process information hiding. The D-Bus restriction prevents unprivileged services from discovering other services' process information (command lines, cgroup paths) via systemd's D-Bus interface.


Hardening Presets

nix-bitcoin provides three presets that can be combined:

secure-node.nix — Opinionated base configuration:

  • Enables firewall
  • Routes all traffic through Tor
  • Replaces sudo with doas
  • Enables D-Bus process information hiding
  • Creates an SSH onion service
  • Enables the operator user
  • Sets up daily backups
  • Enables nodeinfo command

hardened.nix — Imports the NixOS hardened kernel profile. This enables kernel-level hardening (address space layout randomization, kernel module restrictions, etc.) at a ~50% performance cost. Resets allowUserNamespaces to true (needed for Nix sandboxing) and uses the standard libc allocator.

hardened-extended.nix — Builds on hardened.nix with additional restrictions:

  • Disables kernel log leaks, SysRq, debugfs
  • Restricts ptrace, TTY line disciplines, core dumps
  • Disables TCP SACK and timestamps
  • Blacklists obscure network protocols, rare filesystems, Bluetooth, webcam, Thunderbolt/FireWire (DMA attack prevention)
  • Enables USBGuard
  • Enforces signed kernel modules and kernel lockdown

See madaidan's Linux Hardening Guide for background on these settings.

None of these presets affect secrets generation — that is controlled separately by the deployment method and nix-bitcoin.generateSecrets.


Threat Model Summary

Scenario Impact Notes
Attacker has root on the node Full compromise. All funds at risk. lnd-wallet-password is on disk, allowing wallet decryption. All secrets are readable.
Secrets directory leaked, no network access No direct fund theft. Secrets contain RPC passwords and TLS keys but not wallet key material. Without network access to the RPC port, passwords are not exploitable.
Secrets directory leaked, with RPC network access bitcoind wallet funds at risk (if a wallet exists). The privileged RPC password allows calling any bitcoind RPC, including spending. LND funds are safer: the attacker also needs a macaroon (not in secrets dir) to call LND RPCs.
LND seed mnemonic leaked All LND on-chain funds compromised. The attacker can derive all keys. Channel funds are at risk if the attacker broadcasts old commitment transactions. Immediate action required: sweep funds, close channels, re-seed.
Nix store accessed No secrets exposed. Secrets are never written to the Nix store. Configuration files reference secret file paths, not secret values.
Deploy machine compromised Full compromise. For Krops/NixOps, the deploy machine holds plaintext secrets and controls what code is deployed to the node.

Operator Responsibilities

These items are not automated by nix-bitcoin and require manual action:

  • Back up the LND seed mnemonic. After first boot, copy /var/lib/lnd/lnd-seed-mnemonic to secure offline storage. Then delete the file from the node. This is the only way to recover on-chain funds.

  • Back up channel state. Copy /var/lib/lnd/chain/bitcoin/mainnet/channel.backup after opening new channels. This allows off-chain fund recovery via the Data Loss Protection protocol. Alternatively, enable services.backups to automate this.

  • Secure the deploy machine. For Krops and NixOps deployments, your local secrets/ directory contains plaintext credentials. If your local machine is compromised, the node is compromised.

  • Review enabled presets. Consider enabling secure-node.nix (Tor, firewall, operator user) and netns-isolation (network namespace separation between services). These are not enabled by default.

  • Understand the auto-unlock tradeoff. LND's wallet password is stored on disk so the service can start unattended. This means anyone with root access to the node can unlock the wallet. There is no way to require manual unlock at boot within the current nix-bitcoin design.


Further Reading