diff --git a/README.md b/README.md index dbac287..183ee5e 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ top left corner of the documents. * [Configuration and maintenance](docs/configuration.md) * [Using services](docs/services.md) * [FAQ](docs/faq.md) +* [Security model](docs/security-model.md) Features --- diff --git a/docs/security-model.md b/docs/security-model.md new file mode 100644 index 0000000..dd4a91b --- /dev/null +++ b/docs/security-model.md @@ -0,0 +1,358 @@ +# 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](../SECURITY.md). + +--- + +## Table of Contents + +- [Secrets Management](#secrets-management) + - [What is stored](#what-is-stored) + - [When secrets are generated](#when-secrets-are-generated) + - [File permissions](#file-permissions) + - [What is NOT in the secrets directory](#what-is-not-in-the-secrets-directory) +- [Wallet Key Material](#wallet-key-material) + - [LND](#lnd) + - [bitcoind](#bitcoind) +- [Service Isolation](#service-isolation) + - [Unix users and groups](#unix-users-and-groups) + - [systemd hardening](#systemd-hardening) + - [Network namespace isolation](#network-namespace-isolation) + - [RPC whitelisting](#rpc-whitelisting) + - [Operator user](#operator-user) +- [Network Security](#network-security) + - [RPC binding](#rpc-binding) + - [Tor](#tor) + - [WireGuard](#wireguard) + - [Firewall and D-Bus](#firewall-and-d-bus) +- [Hardening Presets](#hardening-presets) +- [Threat Model Summary](#threat-model-summary) +- [Operator Responsibilities](#operator-responsibilities) +- [Further Reading](#further-reading) + +--- + +## 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](https://nixos.org/manual/nix/stable/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](#wallet-key-material) +- **LND wallet database** (`/var/lib/lnd/chain/bitcoin//wallet.db`) +- **LND macaroons** (`/var/lib/lnd/chain/bitcoin//*.macaroon`) +- **LND channel backup** (`/var/lib/lnd/chain/bitcoin//channel.backup`) +- **bitcoind wallet** (`wallet.dat` or descriptors, if created by the operator) + +--- + +## Wallet Key Material + +### LND + +LND uses the [aezeed cipher seed](https://github.com/lightningnetwork/lnd/tree/master/aezeed) +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`](../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`](../modules/netns-isolation.nix) module places +each service in its own +[network namespace](https://man7.org/linux/man-pages/man7/network_namespaces.7.html). +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 +nix-bitcoin.netns-isolation.enable = true; +``` + +Note: This is not compatible with the WireGuard preset. + +### RPC whitelisting + +The [`bitcoind-rpc-public-whitelist`](../modules/bitcoind-rpc-public-whitelist.nix) +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`](../modules/operator.nix) 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`](../modules/presets/secure-node.nix) preset imports +[`enable-tor`](../modules/presets/enable-tor.nix), which routes all outbound +traffic from nix-bitcoin services through Tor. Services that support it can also +accept inbound connections via +[onion services](https://community.torproject.org/onion-services/overview/). + +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`](../modules/presets/wireguard.nix) 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`](../modules/security.nix) 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`](../modules/presets/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`](../modules/presets/hardened.nix)** — Imports the +[NixOS hardened kernel profile](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/profiles/hardened.nix). +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`](../modules/presets/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](https://madaidans-insecurities.github.io/guides/linux-hardening.html) +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](https://github.com/lightningnetwork/lnd/blob/master/docs/recovery.md). + 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 + +- [NixOS Hardened Profile](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/profiles/hardened.nix) — kernel and userspace hardening applied by `hardened.nix` +- [systemd.exec(5)](https://www.freedesktop.org/software/systemd/man/systemd.exec.html) — reference for the systemd sandboxing options used in `pkgs/lib.nix` +- [systemd.resource-control(5)](https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html) — resource limiting and IP address filtering +- [Linux network namespaces](https://man7.org/linux/man-pages/man7/network_namespaces.7.html) — the kernel feature behind `netns-isolation` +- [aezeed cipher seed](https://github.com/lightningnetwork/lnd/tree/master/aezeed) — LND's seed scheme and how it differs from BIP39 +- [LND fund recovery](https://github.com/lightningnetwork/lnd/blob/master/docs/recovery.md) — on-chain and off-chain recovery procedures +- [madaidan's Linux Hardening Guide](https://madaidans-insecurities.github.io/guides/linux-hardening.html) — background for `hardened-extended.nix` settings +- [Nix Store security](https://nixos.org/manual/nix/stable/store/) — why secrets must not be written to the store diff --git a/modules/lamassu-lnbits.nix b/modules/lamassu-lnbits.nix index b1c72bb..84b58a2 100644 --- a/modules/lamassu-lnbits.nix +++ b/modules/lamassu-lnbits.nix @@ -254,7 +254,7 @@ in if ${pkgs.postgresql}/bin/psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='${cfg.database.user}'" | grep -q 1; then password=$(cat ${secretsDir}/lamassu-db-password) # Escape single quotes by doubling them (SQL standard) - escaped_password=$(printf '%s' "$password" | sed "s/'/''/g") + escaped_password=$(printf '%s' "$password" | sed "s/'/''''/g") ${pkgs.postgresql}/bin/psql -c "ALTER USER \"${cfg.database.user}\" WITH PASSWORD '$escaped_password';" exit 0 fi