From 278d480653cd933ef9ab71376454a25536e1d587 Mon Sep 17 00:00:00 2001 From: Sovran PR Bot Date: Mon, 10 Aug 2026 00:56:00 +0000 Subject: [PATCH] vendor: replace nix-bitcoin flake input with minimal vendored modules (nixpkgs-only) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove inputs.nix-bitcoin (fort-nix/nix-bitcoin/release) from flake.nix - Vendor only 6 services actually used by Sovran: bitcoind, electrs, lnd (+lndconnect), rtl, btcpayserver, mempool + supporting infra: secrets, onion-services/addresses, operator, nodeinfo, security, versioning - All packages now from nixpkgs directly (pkgs.*) — no pinned pkgs - Keep nix-bitcoin.* option namespace for compatibility - backups.nix removed: Sovran uses rsnapshot to Second_Drive (configuration.nix: hourly/daily to BTCEcoandBackup) — duplicity remote backup not needed - netns-isolation.nix replaced with stub (5 lines): original 365-line bridge/iptables/ip-netns broke Caddy/AlbyHub/RTL a year ago and is incompatible with nwc-wallets (requires enable=false). Stub keeps option valid but warns if enabled. - Add pkgs/sovran-overlay.nix for gaps only: lndinit + netns-exec stub --- flake.nix | 15 +- modules/bitcoin-core.nix | 6 +- modules/bitcoinecosystem.nix | 1 + modules/core/role-logic.nix | 6 +- modules/vendor/nix-bitcoin/bitcoind.nix | 479 ++++++++++++++++++ modules/vendor/nix-bitcoin/btcpayserver.nix | 282 +++++++++++ modules/vendor/nix-bitcoin/electrs.nix | 106 ++++ modules/vendor/nix-bitcoin/enable-tor.nix | 54 ++ modules/vendor/nix-bitcoin/lib.nix | 132 +++++ modules/vendor/nix-bitcoin/lnd.nix | 314 ++++++++++++ modules/vendor/nix-bitcoin/lndconnect.nix | 282 +++++++++++ modules/vendor/nix-bitcoin/mempool.nix | 349 +++++++++++++ modules/vendor/nix-bitcoin/modules.nix | 27 + .../vendor/nix-bitcoin/netns-isolation.nix | 16 + modules/vendor/nix-bitcoin/nix-bitcoin.nix | 60 +++ modules/vendor/nix-bitcoin/nodeinfo.nix | 167 ++++++ .../vendor/nix-bitcoin/onion-addresses.nix | 106 ++++ modules/vendor/nix-bitcoin/onion-services.nix | 122 +++++ modules/vendor/nix-bitcoin/operator.nix | 58 +++ modules/vendor/nix-bitcoin/rtl.nix | 239 +++++++++ .../vendor/nix-bitcoin/secrets/secrets.nix | 235 +++++++++ modules/vendor/nix-bitcoin/secure-node.nix | 62 +++ modules/vendor/nix-bitcoin/security.nix | 48 ++ modules/vendor/nix-bitcoin/versioning.nix | 12 + pkgs/README.md | 2 + pkgs/nbxplorer.nix | 18 + pkgs/sovran-overlay.nix | 37 ++ 27 files changed, 3221 insertions(+), 14 deletions(-) create mode 100644 modules/vendor/nix-bitcoin/bitcoind.nix create mode 100644 modules/vendor/nix-bitcoin/btcpayserver.nix create mode 100644 modules/vendor/nix-bitcoin/electrs.nix create mode 100644 modules/vendor/nix-bitcoin/enable-tor.nix create mode 100644 modules/vendor/nix-bitcoin/lib.nix create mode 100644 modules/vendor/nix-bitcoin/lnd.nix create mode 100644 modules/vendor/nix-bitcoin/lndconnect.nix create mode 100644 modules/vendor/nix-bitcoin/mempool.nix create mode 100644 modules/vendor/nix-bitcoin/modules.nix create mode 100644 modules/vendor/nix-bitcoin/netns-isolation.nix create mode 100644 modules/vendor/nix-bitcoin/nix-bitcoin.nix create mode 100644 modules/vendor/nix-bitcoin/nodeinfo.nix create mode 100644 modules/vendor/nix-bitcoin/onion-addresses.nix create mode 100644 modules/vendor/nix-bitcoin/onion-services.nix create mode 100644 modules/vendor/nix-bitcoin/operator.nix create mode 100644 modules/vendor/nix-bitcoin/rtl.nix create mode 100644 modules/vendor/nix-bitcoin/secrets/secrets.nix create mode 100644 modules/vendor/nix-bitcoin/secure-node.nix create mode 100644 modules/vendor/nix-bitcoin/security.nix create mode 100644 modules/vendor/nix-bitcoin/versioning.nix create mode 100644 pkgs/README.md create mode 100644 pkgs/nbxplorer.nix create mode 100644 pkgs/sovran-overlay.nix diff --git a/flake.nix b/flake.nix index 9024d45..d10e9c6 100755 --- a/flake.nix +++ b/flake.nix @@ -3,13 +3,12 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - nix-bitcoin.url = "github:fort-nix/nix-bitcoin/release"; nixvim.url = "github:nix-community/nixvim"; btc-clients.url = "github:emmanuelrosa/btc-clients-nix"; nixpkgs-stable.url = "github:nixos/nixpkgs/nixos-26.05"; }; - outputs = { self, nixpkgs, nix-bitcoin, nixvim, btc-clients, nixpkgs-stable, ... }: + outputs = { self, nixpkgs, nixvim, btc-clients, nixpkgs-stable, ... }: let overlay-stable = final: prev: { @@ -18,11 +17,12 @@ config.allowUnfree = true; }; }; + overlay-sovran = import ./pkgs/sovran-overlay.nix; in { nixosConfigurations.nixos = nixpkgs.lib.nixosSystem { modules = [ - { nixpkgs.hostPlatform = "x86_64-linux"; } + { nixpkgs.hostPlatform = "x86_64-linux"; nixpkgs.overlays = [ overlay-stable overlay-sovran ]; } self.nixosModules.Sovran_SystemsOS ./hardware-configuration.nix ./role-state.nix @@ -32,10 +32,9 @@ nixosConfigurations.sovran_systemsos-iso = nixpkgs.lib.nixosSystem { modules = [ - { nixpkgs.hostPlatform = "x86_64-linux"; } - ({ config, pkgs, ... }: { nixpkgs.overlays = [ overlay-stable ]; }) + { nixpkgs.hostPlatform = "x86_64-linux"; nixpkgs.overlays = [ overlay-stable overlay-sovran ]; } ./iso/common.nix - nix-bitcoin.nixosModules.default + ./modules/vendor/nix-bitcoin/modules.nix nixvim.nixosModules.nixvim ]; }; @@ -43,10 +42,10 @@ nixosModules.Sovran_SystemsOS = { pkgs, lib, config, ... }: { imports = [ ({ config, pkgs, ... }: { - nixpkgs.overlays = [ overlay-stable ]; + nixpkgs.overlays = [ overlay-stable overlay-sovran ]; }) ./configuration.nix - nix-bitcoin.nixosModules.default + ./modules/vendor/nix-bitcoin/modules.nix nixvim.nixosModules.nixvim ]; config = { diff --git a/modules/bitcoin-core.nix b/modules/bitcoin-core.nix index 609c8f3..2794d57 100755 --- a/modules/bitcoin-core.nix +++ b/modules/bitcoin-core.nix @@ -1,7 +1,7 @@ { config, pkgs, lib, ... }: lib.mkIf config.sovran_systemsOS.features.bitcoin-core { - - services.bitcoind.package = lib.mkForce config.nix-bitcoin.pkgs.bitcoind; - + # Vendored nix-bitcoin now uses nixpkgs directly; no nix-bitcoin.pkgs indirection. + # Use standard bitcoind from nixpkgs (override to knots if desired via pkgs.bitcoind-knots) + services.bitcoind.package = lib.mkForce pkgs.bitcoind; } diff --git a/modules/bitcoinecosystem.nix b/modules/bitcoinecosystem.nix index e5e9168..205057c 100755 --- a/modules/bitcoinecosystem.nix +++ b/modules/bitcoinecosystem.nix @@ -68,6 +68,7 @@ lib.mkIf config.sovran_systemsOS.services.bitcoin { name = "free"; }; + # vendored: now no-op (always uses nixpkgs) nix-bitcoin.useVersionLockedPkgs = false; systemd.services.bitcoind = { diff --git a/modules/core/role-logic.nix b/modules/core/role-logic.nix index 52d17da..9fa22f4 100755 --- a/modules/core/role-logic.nix +++ b/modules/core/role-logic.nix @@ -3,9 +3,9 @@ { config = lib.mkMerge [ - # nix-bitcoin is globally imported by the flake (nixosModules.Sovran_SystemsOS). - # This default satisfies nix-bitcoin's generateSecrets assertion so that Desktop - # Only systems can evaluate without enabling any Bitcoin services. + # Vendored nix-bitcoin is always imported via modules/vendor/nix-bitcoin/modules.nix. + # This default satisfies the secrets assertion so Desktop-Only systems evaluate + # without enabling any Bitcoin services. { nix-bitcoin.generateSecrets = lib.mkDefault true; } diff --git a/modules/vendor/nix-bitcoin/bitcoind.nix b/modules/vendor/nix-bitcoin/bitcoind.nix new file mode 100644 index 0000000..ba97135 --- /dev/null +++ b/modules/vendor/nix-bitcoin/bitcoind.nix @@ -0,0 +1,479 @@ +{ config, pkgs, lib, ... }: + +with lib; +let + options = { + services.bitcoind = { + enable = mkEnableOption "Bitcoin daemon"; + address = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "Address to listen for peer connections."; + }; + port = mkOption { + type = types.port; + default = if !cfg.regtest then 8333 else 18444; + defaultText = "if !cfg.regtest then 8333 else 18444"; + description = "Port to listen for peer connections."; + }; + onionPort = mkOption { + type = types.nullOr types.port; + # When the bitcoind onion service is enabled, add an onion-tagged socket + # to distinguish local connections from Tor connections + default = if (config.nix-bitcoin.onionServices.bitcoind.enable or false) then 8334 else null; + description = '' + Port to listen for Tor peer connections. + If set, inbound connections to this port are tagged as onion peers. + ''; + }; + listen = mkOption { + type = types.bool; + default = false; + description = '' + Listen for peer connections at `address:port` + and `address:onionPort` (if {option}`onionPort` is set). + ''; + }; + listenWhitelisted = mkOption { + type = types.bool; + default = false; + description = '' + Listen for peer connections at `address:whitelistedPort`. + Peers connected through this socket are automatically whitelisted. + ''; + }; + whitelistedPort = mkOption { + type = types.port; + default = 8335; + description = "See `listenWhitelisted`."; + }; + getPublicAddressCmd = mkOption { + type = types.str; + default = ""; + description = '' + Bash expression which outputs the public service address to announce to peers. + If left empty, no address is announced. + ''; + }; + package = mkOption { + type = types.package; + default = pkgs.bitcoind; + defaultText = "pkgs.bitcoind"; + description = '' + The package providing bitcoind binaries. + + You can use this option to select other bitcoind-compatible implementations. + Example: + ```nix + services.bitcoind.package = pkgs.bitcoind-knots; + ``` + ''; + }; + extraConfig = mkOption { + type = types.lines; + default = ""; + example = '' + par=16 + logips=1 + ''; + description = "Extra lines appended to {file}`bitcoin.conf`."; + }; + dataDir = mkOption { + type = types.path; + default = "/var/lib/bitcoind"; + description = "The data directory for bitcoind."; + }; + rpc = { + address = mkOption { + type = types.str; + default = "127.0.0.1"; + description = '' + Address to listen for JSON-RPC connections. + ''; + }; + port = mkOption { + type = types.port; + default = if !cfg.regtest then 8332 else 18443; + defaultText = "if !cfg.regtest then 8332 else 18443"; + description = "Port to listen for JSON-RPC connections."; + }; + threads = mkOption { + type = types.nullOr types.ints.u16; + default = null; + description = "The number of threads to service RPC calls."; + }; + allowip = mkOption { + type = types.listOf types.str; + default = [ "127.0.0.1" ]; + description = '' + Allow JSON-RPC connections from specified sources. + ''; + }; + users = mkOption { + default = {}; + description = '' + Allowed users for JSON-RPC connections. + ''; + example = { + alice = { + passwordHMAC = "f7efda5c189b999524f151318c0c86$d5b51b3beffbc02b724e5d095828e0bc8b2456e9ac8757ae3211a5d9b16a22ae"; + rpcwhitelist = [ "sendtoaddress" "getnewaddress" ]; + }; + }; + type = with types; attrsOf (submodule ({ name, ... }: { + options = { + name = mkOption { + type = types.str; + default = name; + example = "alice"; + description = '' + Username for JSON-RPC connections. + ''; + }; + passwordHMAC = mkOption { + type = types.str; + example = "f7efda5c189b999524f151318c0c86$d5b51b3beffbc02b724e5d095828e0bc8b2456e9ac8757ae3211a5d9b16a22ae"; + description = '' + Password HMAC-SHA-256 for JSON-RPC connections. Must be a string of the + format `$`. + ''; + }; + passwordHMACFromFile = mkOption { + type = lib.types.bool; + internal = true; + default = false; + }; + rpcwhitelist = mkOption { + type = types.listOf types.str; + default = []; + description = '' + List of allowed rpc calls for each user. + If empty list, rpcwhitelist is disabled for that user. + ''; + }; + }; + })); + }; + }; + regtest = mkOption { + type = types.bool; + default = false; + description = "Enable regtest mode."; + }; + network = mkOption { + readOnly = true; + default = if cfg.regtest then "regtest" else "mainnet"; + }; + makeNetworkName = mkOption { + readOnly = true; + default = mainnet: regtest: if cfg.regtest then regtest else mainnet; + }; + proxy = mkOption { + type = types.nullOr types.str; + default = if cfg.tor.proxy then config.nix-bitcoin.torClientAddressWithPort else null; + description = "Connect through SOCKS5 proxy"; + }; + i2p = mkOption { + type = types.enum [ false true "only-outgoing" ]; + default = false; + description = '' + Enable peer connections via i2p. + With `only-outgoing`, incoming i2p connections are disabled. + ''; + }; + dataDirReadableByGroup = mkOption { + type = types.bool; + default = false; + description = '' + If enabled, data dir content is readable by the bitcoind service group. + Warning: This disables bitcoind's wallet support. + ''; + }; + sysperms = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + Create new files with system default permissions, instead of umask 077 + (only effective with disabled wallet functionality) + ''; + }; + disablewallet = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + Do not load the wallet and disable wallet RPC calls + ''; + }; + dbCache = mkOption { + type = types.nullOr (intAtLeast 4); + default = null; + example = 4000; + description = "Override the default database cache size in MiB."; + }; + prune = mkOption { + type = types.ints.unsigned; + default = 0; + example = 10000; + description = '' + Automatically prune block files to stay under the specified target size in MiB. + Value 0 disables pruning. + ''; + }; + txindex = mkOption { + type = types.bool; + default = false; + description = "Enable the transaction index."; + }; + zmqpubrawblock = mkOption { + type = types.nullOr types.str; + default = null; + example = "tcp://127.0.0.1:28332"; + description = "ZMQ address for zmqpubrawblock notifications"; + }; + zmqpubrawtx = mkOption { + type = types.nullOr types.str; + default = null; + example = "tcp://127.0.0.1:28333"; + description = "ZMQ address for zmqpubrawtx notifications"; + }; + assumevalid = mkOption { + type = types.nullOr types.str; + default = null; + example = "00000000000000000000e5abc3a74fe27dc0ead9c70ea1deb456f11c15fd7bc6"; + description = '' + If this block is in the chain assume that it and its ancestors are + valid and potentially skip their script verification. + ''; + }; + addnodes = mkOption { + type = types.listOf types.str; + default = []; + example = [ "ecoc5q34tmbq54wl.onion" ]; + description = "Add nodes to connect to and attempt to keep the connections open"; + }; + discover = mkOption { + type = types.nullOr types.bool; + default = null; + description = "Discover own IP addresses"; + }; + addresstype = mkOption { + type = types.nullOr types.str; + default = null; + example = "bech32"; + description = "The type of addresses to use"; + }; + user = mkOption { + type = types.str; + default = "bitcoin"; + description = "The user as which to run bitcoind."; + }; + group = mkOption { + type = types.str; + default = cfg.user; + description = "The group as which to run bitcoind."; + }; + cli = mkOption { + readOnly = true; + type = types.package; + default = pkgs.writers.writeBashBin "bitcoin-cli" '' + exec ${cfg.package}/bin/bitcoin-cli -datadir='${cfg.dataDir}' "$@" + ''; + defaultText = "(See source)"; + description = "Binary to connect with the bitcoind instance."; + }; + tor = nbLib.tor; + }; + }; + + cfg = config.services.bitcoind; + nbLib = config.nix-bitcoin.lib; + secretsDir = config.nix-bitcoin.secretsDir; + + i2pSAM = config.services.i2pd.proto.sam; + + configFile = builtins.toFile "bitcoin.conf" '' + # We're already logging via journald + nodebuglogfile=1 + logtimestamps=0 + + startupnotify=/run/current-system/systemd/bin/systemd-notify --ready + + ${optionalString cfg.regtest '' + regtest=1 + [regtest] + ''} + ${optionalString (cfg.dbCache != null) "dbcache=${toString cfg.dbCache}"} + prune=${toString cfg.prune} + ${optionalString cfg.txindex "txindex=1"} + ${optionalString (cfg.sysperms != null) "sysperms=${if cfg.sysperms then "1" else "0"}"} + ${optionalString (cfg.disablewallet != null) "disablewallet=${if cfg.disablewallet then "1" else "0"}"} + ${optionalString (cfg.assumevalid != null) "assumevalid=${cfg.assumevalid}"} + + # Connection options + listen=${if (cfg.listen || cfg.listenWhitelisted) then "1" else "0"} + ${optionalString cfg.listen + "bind=${cfg.address}:${toString cfg.port}"} + ${optionalString (cfg.listen && cfg.onionPort != null) + "bind=${cfg.address}:${toString cfg.onionPort}=onion"} + ${optionalString cfg.listenWhitelisted + "whitebind=${cfg.address}:${toString cfg.whitelistedPort}"} + ${optionalString (cfg.proxy != null) "proxy=${cfg.proxy}"} + ${optionalString (cfg.i2p != false) "i2psam=${nbLib.addressWithPort i2pSAM.address i2pSAM.port}"} + ${optionalString (cfg.i2p == "only-outgoing") "i2pacceptincoming=0"} + + ${optionalString (cfg.discover != null) "discover=${if cfg.discover then "1" else "0"}"} + ${lib.concatMapStrings (node: "addnode=${node}\n") cfg.addnodes} + + # RPC server options + rpcbind=${cfg.rpc.address} + rpcport=${toString cfg.rpc.port} + rpcconnect=${cfg.rpc.address} + ${optionalString (cfg.rpc.threads != null) "rpcthreads=${toString cfg.rpc.threads}"} + rpcwhitelistdefault=0 + ${concatMapStrings (user: '' + ${optionalString (!user.passwordHMACFromFile) "rpcauth=${user.name}:${user.passwordHMAC}"} + ${optionalString (user.rpcwhitelist != []) + "rpcwhitelist=${user.name}:${lib.strings.concatStringsSep "," user.rpcwhitelist}"} + '') (builtins.attrValues cfg.rpc.users) + } + ${lib.concatMapStrings (rpcallowip: "rpcallowip=${rpcallowip}\n") cfg.rpc.allowip} + + # Wallet options + ${optionalString (cfg.addresstype != null) "addresstype=${cfg.addresstype}"} + + # ZMQ options + ${optionalString (cfg.zmqpubrawblock != null) "zmqpubrawblock=${cfg.zmqpubrawblock}"} + ${optionalString (cfg.zmqpubrawtx != null) "zmqpubrawtx=${cfg.zmqpubrawtx}"} + + # Extra options + ${cfg.extraConfig} + ''; + + zmqServerEnabled = (cfg.zmqpubrawblock != null) || (cfg.zmqpubrawtx != null); + + intAtLeast = n: types.addCheck types.int (x: x >= n) // { + name = "intAtLeast"; + description = "integer >= ${toString n}"; + }; +in { + inherit options; + + config = mkIf cfg.enable { + environment.systemPackages = [ cfg.package (hiPrio cfg.cli) ]; + + services.bitcoind = mkMerge [ + (mkIf cfg.dataDirReadableByGroup { + disablewallet = true; + sysperms = true; + }) + { + rpc.users.privileged = { + passwordHMACFromFile = true; + }; + rpc.users.public = { + passwordHMACFromFile = true; + rpcwhitelist = import ./bitcoind-rpc-public-whitelist.nix; + }; + } + ]; + + services.i2pd = mkIf (cfg.i2p != false) { + enable = true; + proto.sam.enable = true; + }; + + systemd.tmpfiles.rules = [ + "d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -" + ]; + + systemd.services.bitcoind = rec { + wants = [ + "network-online.target" + # Use `wants` instead of `requires` for `nix-bitcoin-secrets.target` + # so that bitcoind and all dependent services are not restarted when + # the secrets target restarts. + # The secrets target always restarts when deploying with one of the methods + # in ./deployment. + # + # TODO-EXTERNAL: Instead of `wants`, use a future systemd dependency type + # that propagates initial start failures but no restarts + "nix-bitcoin-secrets.target" + ]; + after = wants; + wantedBy = [ "multi-user.target" ]; + + preStart = let + extraRpcauth = concatMapStrings (name: let + user = cfg.rpc.users.${name}; + in optionalString user.passwordHMACFromFile '' + echo "rpcauth=${user.name}:$(cat ${secretsDir}/bitcoin-HMAC-${name})" + '' + ) (builtins.attrNames cfg.rpc.users); + in '' + ${optionalString cfg.dataDirReadableByGroup '' + if [[ -e '${cfg.dataDir}/blocks' ]]; then + chmod -R g+rX '${cfg.dataDir}/blocks' + fi + ''} + + cfg=$( + cat ${configFile} + ${extraRpcauth} + echo + ${optionalString (cfg.getPublicAddressCmd != "") '' + echo "externalip=$(${cfg.getPublicAddressCmd})" + ''} + ) + confFile='${cfg.dataDir}/bitcoin.conf' + if [[ ! -e $confFile || $cfg != $(cat $confFile) ]]; then + install -o '${cfg.user}' -g '${cfg.group}' -m 640 <(echo "$cfg") $confFile + fi + ''; + + # Enable RPC access for group + postStart = '' + chmod g=r '${cfg.dataDir}/${optionalString cfg.regtest "regtest/"}.cookie' + '' + (optionalString cfg.regtest) '' + chmod g=x '${cfg.dataDir}/regtest' + ''; + + serviceConfig = nbLib.defaultHardening // { + Type = "notify"; + NotifyAccess = "all"; + User = cfg.user; + Group = cfg.group; + TimeoutStartSec = "30min"; + TimeoutStopSec = "30min"; + ExecStart = "${cfg.package}/bin/bitcoind -datadir='${cfg.dataDir}'"; + Restart = "on-failure"; + UMask = mkIf cfg.dataDirReadableByGroup "0027"; + ReadWritePaths = [ cfg.dataDir ]; + } // nbLib.allowedIPAddresses cfg.tor.enforce + // optionalAttrs zmqServerEnabled nbLib.allowNetlink; + }; + + users.users.${cfg.user} = { + isSystemUser = true; + group = cfg.group; + }; + users.groups.${cfg.group} = {}; + users.groups.bitcoinrpc-public = {}; + + nix-bitcoin.operator.groups = [ cfg.group ]; + + nix-bitcoin.secrets = { + bitcoin-rpcpassword-privileged.user = cfg.user; + bitcoin-rpcpassword-public = { + user = cfg.user; + group = "bitcoinrpc-public"; + }; + + bitcoin-HMAC-privileged.user = cfg.user; + bitcoin-HMAC-public.user = cfg.user; + }; + nix-bitcoin.generateSecretsCmds.bitcoind = '' + makeBitcoinRPCPassword privileged + makeBitcoinRPCPassword public + ''; + }; +} diff --git a/modules/vendor/nix-bitcoin/btcpayserver.nix b/modules/vendor/nix-bitcoin/btcpayserver.nix new file mode 100644 index 0000000..9f4c4e3 --- /dev/null +++ b/modules/vendor/nix-bitcoin/btcpayserver.nix @@ -0,0 +1,282 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + options.services = { + btcpayserver = { + enable = mkEnableOption "btcpayserver, a self-hosted Bitcoin payment processor"; + address = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "Address to listen on."; + }; + port = mkOption { + type = types.port; + default = 23000; + description = "Port to listen on."; + }; + package = mkOption { + type = types.package; + default = if cfg.btcpayserver.lbtc then + pkgs.btcpayserver.override { altcoinSupport = true; } + else + pkgs.btcpayserver; + defaultText = "(See source)"; + description = "The package providing btcpayserver binaries."; + }; + dataDir = mkOption { + type = types.path; + default = "/var/lib/btcpayserver"; + description = "The data directory for btcpayserver."; + }; + lightningBackend = mkOption { + type = types.nullOr (types.enum [ "clightning" "lnd" ]); + default = null; + description = "The lightning node implementation to use."; + }; + lbtc = mkOption { + type = types.bool; + default = false; + description = "Enable liquid support in btcpayserver."; + }; + rootpath = mkOption { + type = types.nullOr types.str; + default = null; + example = "btcpayserver"; + description = "The prefix for root-relative btcpayserver URLs."; + }; + user = mkOption { + type = types.str; + default = "btcpayserver"; + description = "The user as which to run btcpayserver."; + }; + group = mkOption { + type = types.str; + default = cfg.btcpayserver.user; + description = "The group as which to run btcpayserver."; + }; + tor.enforce = nbLib.tor.enforce; + }; + + nbxplorer = { + enable = mkOption { + # This option is only used by netns-isolation + internal = true; + default = cfg.btcpayserver.enable; + description = '' + nbxplorer is always enabled when btcpayserver is enabled. + ''; + }; + package = mkOption { + type = types.package; + default = pkgs.nbxplorer; + defaultText = "pkgs.nbxplorer"; + description = "The package providing nbxplorer binaries."; + }; + address = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "Address to listen on."; + }; + port = mkOption { + type = types.port; + default = 24444; + description = "Port to listen on."; + }; + dataDir = mkOption { + type = types.path; + default = "/var/lib/nbxplorer"; + description = "The data directory for nbxplorer."; + }; + user = mkOption { + type = types.str; + default = "nbxplorer"; + description = "The user as which to run nbxplorer."; + }; + group = mkOption { + type = types.str; + default = cfg.nbxplorer.user; + description = "The group as which to run nbxplorer."; + }; + tor.enforce = nbLib.tor.enforce; + }; + }; + + cfg = config.services; + nbLib = config.nix-bitcoin.lib; + + inherit (config.services) bitcoind liquidd; +in { + inherit options; + + config = mkIf cfg.btcpayserver.enable { + services.bitcoind = { + enable = true; + rpc.users.btcpayserver = { + passwordHMACFromFile = true; + rpcwhitelist = cfg.bitcoind.rpc.users.public.rpcwhitelist ++ [ + "setban" + "generatetoaddress" + ]; + }; + listenWhitelisted = true; + }; + services.clightning.enable = mkIf (cfg.btcpayserver.lightningBackend == "clightning") true; + services.lnd = mkIf (cfg.btcpayserver.lightningBackend == "lnd") { + enable = true; + macaroons.btcpayserver = { + inherit (cfg.btcpayserver) user; + permissions = ''{"entity":"info","action":"read"},{"entity":"onchain","action":"read"},{"entity":"offchain","action":"read"},{"entity":"address","action":"read"},{"entity":"message","action":"read"},{"entity":"peers","action":"read"},{"entity":"signer","action":"read"},{"entity":"invoices","action":"read"},{"entity":"invoices","action":"write"},{"entity":"address","action":"write"}''; + }; + }; + services.liquidd = mkIf cfg.btcpayserver.lbtc { + enable = true; + listenWhitelisted = true; + }; + services.postgresql = { + enable = true; + ensureDatabases = [ + "btcpaydb" # This name is kept for backwards compatibility + "nbxplorer" + ]; + ensureUsers = [ + { name = cfg.btcpayserver.user; } + { name = cfg.nbxplorer.user; } + ]; + }; + systemd.services.postgresql-setup.postStart = '' + psql -tAc ' + ALTER DATABASE "btcpaydb" OWNER TO "${cfg.btcpayserver.user}"; + ALTER DATABASE "nbxplorer" OWNER TO "${cfg.nbxplorer.user}"; + ' + ''; + + systemd.tmpfiles.rules = [ + "d '${cfg.nbxplorer.dataDir}' 0770 ${cfg.nbxplorer.user} ${cfg.nbxplorer.group} - -" + "d '${cfg.btcpayserver.dataDir}' 0770 ${cfg.btcpayserver.user} ${cfg.btcpayserver.group} - -" + ]; + + systemd.services.nbxplorer = let + configFile = builtins.toFile "config" '' + network=${bitcoind.network} + btcrpcuser=${cfg.bitcoind.rpc.users.btcpayserver.name} + btcrpcurl=http://${nbLib.addressWithPort bitcoind.rpc.address cfg.bitcoind.rpc.port} + btcnodeendpoint=${nbLib.addressWithPort bitcoind.address bitcoind.whitelistedPort} + bind=${cfg.nbxplorer.address} + port=${toString cfg.nbxplorer.port} + ${optionalString cfg.btcpayserver.lbtc '' + chains=btc,lbtc + lbtcrpcuser=${liquidd.rpcuser} + lbtcrpcurl=http://${nbLib.addressWithPort liquidd.rpc.address liquidd.rpc.port} + lbtcnodeendpoint=${nbLib.addressWithPort liquidd.address liquidd.whitelistedPort} + ''} + postgres=User ID=${cfg.nbxplorer.user};Host=/run/postgresql;Database=nbxplorer + ''; + in rec { + wantedBy = [ "multi-user.target" ]; + requires = [ "postgresql.target" ]; + wants = [ "bitcoind.service" ] ++ optional cfg.btcpayserver.lbtc "liquidd.service"; + after = requires ++ wants ++ [ "nix-bitcoin-secrets.target" ]; + preStart = '' + install -m 600 ${configFile} '${cfg.nbxplorer.dataDir}/settings.config' + { + echo "btcrpcpassword=$(cat ${config.nix-bitcoin.secretsDir}/bitcoin-rpcpassword-btcpayserver)" + ${optionalString cfg.btcpayserver.lbtc '' + echo "lbtcrpcpassword=$(cat ${config.nix-bitcoin.secretsDir}/liquid-rpcpassword)" + ''} + } >> '${cfg.nbxplorer.dataDir}/settings.config' + ''; + serviceConfig = nbLib.defaultHardening // { + ExecStart = '' + ${cfg.nbxplorer.package}/bin/nbxplorer --conf=${cfg.nbxplorer.dataDir}/settings.config \ + --datadir=${cfg.nbxplorer.dataDir} + ''; + User = cfg.nbxplorer.user; + Restart = "on-failure"; + RestartSec = "10s"; + ReadWritePaths = [ cfg.nbxplorer.dataDir ]; + MemoryDenyWriteExecute = false; + } // nbLib.allowedIPAddresses cfg.nbxplorer.tor.enforce; + }; + + systemd.services.btcpayserver = let + nbExplorerUrl = "http://${nbLib.addressWithPort cfg.nbxplorer.address cfg.nbxplorer.port}/"; + nbExplorerCookie = "${cfg.nbxplorer.dataDir}/${bitcoind.makeNetworkName "Main" "RegTest"}/.cookie"; + configFile = builtins.toFile "btcpayserver-config" ('' + network=${bitcoind.network} + bind=${cfg.btcpayserver.address} + port=${toString cfg.btcpayserver.port} + socksendpoint=${config.nix-bitcoin.torClientAddressWithPort} + btcexplorerurl=${nbExplorerUrl} + btcexplorercookiefile=${nbExplorerCookie} + postgres=User ID=${cfg.btcpayserver.user};Host=/run/postgresql;Database=btcpaydb + '' + optionalString (cfg.btcpayserver.rootpath != null) '' + rootpath=${cfg.btcpayserver.rootpath} + '' + optionalString (cfg.btcpayserver.lightningBackend == "clightning") '' + btclightning=type=clightning;server=unix:///${cfg.clightning.dataDir}/${bitcoind.makeNetworkName "bitcoin" "regtest"}/lightning-rpc + '' + optionalString (cfg.btcpayserver.lightningBackend == "lnd") + ( + "btclightning=type=lnd-rest;" + + "server=https://${nbLib.address cfg.lnd.restAddress}:${toString cfg.lnd.restPort}/;" + + "macaroonfilepath=/run/lnd/btcpayserver.macaroon;" + + "certfilepath=${config.services.lnd.certPath}" + + "\n" + ) + + optionalString cfg.btcpayserver.lbtc '' + chains=btc,lbtc + lbtcexplorerurl=${nbExplorerUrl} + lbtcexplorercookiefile=${nbExplorerCookie} + ''); + in rec { + wantedBy = [ "multi-user.target" ]; + requires = [ "postgresql.target" ]; + wants = [ "nbxplorer.service" ] + ++ optional (cfg.btcpayserver.lightningBackend != null) "${cfg.btcpayserver.lightningBackend}.service"; + after = requires ++ wants; + serviceConfig = nbLib.defaultHardening // { + ExecStart = '' + ${cfg.btcpayserver.package}/bin/btcpayserver --conf=${configFile} \ + --datadir='${cfg.btcpayserver.dataDir}' + ''; + User = cfg.btcpayserver.user; + # Also restart after the program has exited successfully. + # This is required to support restarting from the web interface after + # interactive plugin installation. + # Restart rate limiting is implemented via the `startLimit*` options below. + Restart = "always"; + ReadWritePaths = [ cfg.btcpayserver.dataDir ]; + MemoryDenyWriteExecute = false; + } // nbLib.allowedIPAddresses cfg.btcpayserver.tor.enforce; + startLimitIntervalSec = 30; + startLimitBurst = 10; + }; + + users.users.${cfg.nbxplorer.user} = { + isSystemUser = true; + group = cfg.nbxplorer.group; + extraGroups = [ "bitcoinrpc-public" ] + ++ optional cfg.btcpayserver.lbtc liquidd.group; + home = cfg.nbxplorer.dataDir; + }; + users.groups.${cfg.nbxplorer.group} = {}; + users.users.${cfg.btcpayserver.user} = { + isSystemUser = true; + group = cfg.btcpayserver.group; + extraGroups = [ cfg.nbxplorer.group ] + ++ optional (cfg.btcpayserver.lightningBackend == "clightning") cfg.clightning.user; + home = cfg.btcpayserver.dataDir; + }; + users.groups.${cfg.btcpayserver.group} = {}; + + nix-bitcoin.secrets = { + bitcoin-rpcpassword-btcpayserver = { + user = cfg.bitcoind.user; + group = cfg.nbxplorer.group; + }; + bitcoin-HMAC-btcpayserver.user = cfg.bitcoind.user; + }; + nix-bitcoin.generateSecretsCmds.btcpayserver = '' + makeBitcoinRPCPassword btcpayserver + ''; + }; +} diff --git a/modules/vendor/nix-bitcoin/electrs.nix b/modules/vendor/nix-bitcoin/electrs.nix new file mode 100644 index 0000000..592d492 --- /dev/null +++ b/modules/vendor/nix-bitcoin/electrs.nix @@ -0,0 +1,106 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + options.services.electrs = { + enable = mkEnableOption "electrs, an Electrum server implemented in Rust"; + address = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "Address to listen for RPC connections."; + }; + port = mkOption { + type = types.port; + default = 50001; + description = "Port to listen for RPC connections."; + }; + dataDir = mkOption { + type = types.path; + default = "/var/lib/electrs"; + description = "The data directory for electrs."; + }; + monitoringPort = mkOption { + type = types.port; + default = 4224; + description = "Prometheus monitoring port."; + }; + extraArgs = mkOption { + type = types.separatedString " "; + default = ""; + description = "Extra command line arguments passed to electrs."; + }; + user = mkOption { + type = types.str; + default = "electrs"; + description = "The user as which to run electrs."; + }; + group = mkOption { + type = types.str; + default = cfg.user; + description = "The group as which to run electrs."; + }; + tor.enforce = nbLib.tor.enforce; + }; + + cfg = config.services.electrs; + nbLib = config.nix-bitcoin.lib; + secretsDir = config.nix-bitcoin.secretsDir; + bitcoind = config.services.bitcoind; +in { + inherit options; + + config = mkIf cfg.enable { + assertions = [ + { assertion = bitcoind.prune == 0; + message = "electrs does not support bitcoind pruning."; + } + ]; + + services.bitcoind = { + enable = true; + listenWhitelisted = true; + }; + + systemd.tmpfiles.rules = [ + "d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -" + ]; + + systemd.services.electrs = { + wantedBy = [ "multi-user.target" ]; + requires = [ "bitcoind.service" ]; + after = [ "bitcoind.service" "nix-bitcoin-secrets.target" ]; + preStart = '' + echo "auth = \"${bitcoind.rpc.users.public.name}:$(cat ${secretsDir}/bitcoin-rpcpassword-public)\"" \ + > electrs.toml + ''; + serviceConfig = nbLib.defaultHardening // { + # electrs only uses the working directory for reading electrs.toml + WorkingDirectory = cfg.dataDir; + ExecStart = '' + ${pkgs.electrs}/bin/electrs \ + --log-filters=INFO \ + --network=${bitcoind.makeNetworkName "bitcoin" "regtest"} \ + --db-dir='${cfg.dataDir}' \ + --daemon-dir='${bitcoind.dataDir}' \ + --electrum-rpc-addr=${cfg.address}:${toString cfg.port} \ + --monitoring-addr=${cfg.address}:${toString cfg.monitoringPort} \ + --daemon-rpc-addr=${nbLib.addressWithPort bitcoind.rpc.address bitcoind.rpc.port} \ + --daemon-p2p-addr=${nbLib.addressWithPort bitcoind.address bitcoind.whitelistedPort} \ + ${cfg.extraArgs} + ''; + User = cfg.user; + Group = cfg.group; + Restart = "on-failure"; + RestartSec = "10s"; + ReadWritePaths = [ cfg.dataDir ]; + } // nbLib.allowedIPAddresses cfg.tor.enforce; + }; + + users.users.${cfg.user} = { + isSystemUser = true; + group = cfg.group; + extraGroups = [ "bitcoinrpc-public" ]; + }; + users.groups.${cfg.group} = {}; + }; +} diff --git a/modules/vendor/nix-bitcoin/enable-tor.nix b/modules/vendor/nix-bitcoin/enable-tor.nix new file mode 100644 index 0000000..47766f2 --- /dev/null +++ b/modules/vendor/nix-bitcoin/enable-tor.nix @@ -0,0 +1,54 @@ +{ lib, config, ... }: +let + defaultTrue = lib.mkDefault true; + defaultEnableTorProxy = { + tor.proxy = defaultTrue; + tor.enforce = defaultTrue; + }; + defaultEnforceTor = { + tor.enforce = defaultTrue; + }; +in { + services.tor = { + enable = true; + client.enable = true; + }; + + services = { + # Use Tor as a proxy for outgoing connections + # and restrict all connections to Tor + # + bitcoind = defaultEnableTorProxy; + clightning = defaultEnableTorProxy; + lnd = defaultEnableTorProxy; + lightning-loop = defaultEnableTorProxy; + liquidd = defaultEnableTorProxy; + # TODO-EXTERNAL: + # disable Tor enforcement until btcpayserver can fetch rates over Tor + # btcpayserver = defaultEnableTorProxy; + lightning-pool = defaultEnableTorProxy; + mempool = defaultEnableTorProxy; + + # These services don't make outgoing connections + # (or use Tor by default in case of joinmarket) + # but we restrict them to Tor just to be safe. + # + electrs = defaultEnforceTor; + fulcrum = defaultEnforceTor; + nbxplorer = defaultEnforceTor; + rtl = defaultEnforceTor; + joinmarket = defaultEnforceTor; + joinmarket-ob-watcher = defaultEnforceTor; + clightning-rest = defaultEnforceTor; + }; + + # Add onion services for incoming connections + nix-bitcoin.onionServices = { + bitcoind.enable = defaultTrue; + liquidd.enable = defaultTrue; + electrs.enable = defaultTrue; + fulcrum.enable = defaultTrue; + joinmarket-ob-watcher.enable = defaultTrue; + rtl.enable = defaultTrue; + }; +} diff --git a/modules/vendor/nix-bitcoin/lib.nix b/modules/vendor/nix-bitcoin/lib.nix new file mode 100644 index 0000000..5bfb639 --- /dev/null +++ b/modules/vendor/nix-bitcoin/lib.nix @@ -0,0 +1,132 @@ +lib: pkgs: config: + +with lib; + +# See `man systemd.exec` and `man systemd.resource-control` for an explanation +# of the systemd-related options available through this file. +let self = { + # These settings roughly follow systemd's "strict" security profile + defaultHardening = { + PrivateTmp = true; + ProtectSystem = "strict"; + ProtectHome = true; + NoNewPrivileges = true; + PrivateDevices = true; + MemoryDenyWriteExecute = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectKernelLogs = true; + ProtectClock = true; + ProtectProc = "invisible"; + ProcSubset = "pid"; + ProtectControlGroups = true; + RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6"; + RestrictNamespaces = true; + LockPersonality = true; + IPAddressDeny = "any"; + PrivateUsers = true; + RestrictSUIDSGID = true; + RemoveIPC = true; + RestrictRealtime = true; + ProtectHostname = true; + CapabilityBoundingSet = ""; + # @system-service whitelist and docker seccomp blacklist (except for "clone" + # which is a core requirement for systemd services) + # @system-service is defined in src/shared/seccomp-util.c (systemd source) + SystemCallFilter = [ "@system-service" "~add_key kcmp keyctl mbind move_pages name_to_handle_at personality process_vm_readv process_vm_writev request_key setns unshare userfaultfd" ]; + SystemCallArchitectures = "native"; + }; + + allowNetlink = { + RestrictAddressFamilies = self.defaultHardening.RestrictAddressFamilies + " AF_NETLINK"; + }; + + nodejs = { + # Required for JIT compilation + MemoryDenyWriteExecute = false; + # Required by nodejs >= 18 + SystemCallFilter = self.defaultHardening.SystemCallFilter ++ [ "@pkey" ]; + }; + + # Allow takes precedence over Deny. + allowLocalIPAddresses = { + IPAddressAllow = [ + "127.0.0.1/32" + "::1/128" + "169.254.0.0/16" + ]; + }; + allowAllIPAddresses = { IPAddressAllow = "any"; }; + allowTor = self.allowLocalIPAddresses; + allowedIPAddresses = onlyLocal: + if onlyLocal + then self.allowLocalIPAddresses + else self.allowAllIPAddresses; + + tor = { + proxy = mkOption { + type = types.bool; + default = false; + description = "Whether to proxy outgoing connections with Tor."; + }; + enforce = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enforce Tor on this service by only allowing connections + from and to localhost and link-local addresses. + ''; + }; + }; + + script = name: src: pkgs.writers.writeBash name '' + set -eo pipefail + ${src} + ''; + + # Used for ExecStart* + rootScript = name: src: "+${self.script name src}"; + + cliExec = mkOption { + # Used by netns-isolation to execute the cli in the service's private netns + internal = true; + type = types.str; + default = "exec"; + }; + + mkOnionService = map: { + map = [ map ]; + version = 3; + }; + + # Convert a bind address, which may be a special INADDR_ANY address, + # to an actual IP address + address = addr: + if addr == "0.0.0.0" then + "127.0.0.1" + else if addr == "::" then + "::1" + else + addr; + + addressWithPort = addr: port: "${self.address addr}:${toString port}"; + + optionalAttr = cond: name: if cond then name else null; + + mkCertExtraAltNames = cert: + builtins.concatStringsSep "," ( + (map (domain: "DNS:${domain}") cert.extraDomains) ++ + (map (ip: "IP:${ip}") cert.extraIPs) + ); + + test = { + mkIfTest = test: mkIf (config.tests.${test} or false); + }; + + mkAlias = default: mkOption { + internal = true; + readOnly = true; + inherit default; + }; + +}; in self diff --git a/modules/vendor/nix-bitcoin/lnd.nix b/modules/vendor/nix-bitcoin/lnd.nix new file mode 100644 index 0000000..cfd77db --- /dev/null +++ b/modules/vendor/nix-bitcoin/lnd.nix @@ -0,0 +1,314 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + options.services.lnd = { + enable = mkEnableOption "Lightning Network daemon, a Lightning Network implementation in Go"; + address = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "Address to listen for peer connections"; + }; + port = mkOption { + type = types.port; + default = 9735; + description = "Port to listen for peer connections"; + }; + rpcAddress = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "Address to listen for RPC connections."; + }; + rpcPort = mkOption { + type = types.port; + default = 10009; + description = "Port to listen for gRPC connections."; + }; + restAddress = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "Address to listen for REST connections."; + }; + restPort = mkOption { + type = types.port; + default = 8080; + description = "Port to listen for REST connections."; + }; + dataDir = mkOption { + type = types.path; + default = "/var/lib/lnd"; + description = "The data directory for LND."; + }; + networkDir = mkOption { + readOnly = true; + default = "${cfg.dataDir}/chain/bitcoin/${bitcoind.network}"; + description = "The network data directory."; + }; + tor-socks = mkOption { + type = types.nullOr types.str; + default = if cfg.tor.proxy then config.nix-bitcoin.torClientAddressWithPort else null; + description = "Socks proxy for connecting to Tor nodes"; + }; + macaroons = mkOption { + default = {}; + type = with types; attrsOf (submodule { + options = { + user = mkOption { + type = types.str; + description = "User who owns the macaroon."; + }; + permissions = mkOption { + type = types.str; + example = '' + {"entity":"info","action":"read"},{"entity":"onchain","action":"read"} + ''; + description = "List of granted macaroon permissions."; + }; + }; + }); + description = '' + Extra macaroon definitions. + ''; + }; + certificate = { + extraIPs = mkOption { + type = with types; listOf str; + default = []; + example = [ "60.100.0.1" ]; + description = '' + Extra `subjectAltName` IPs added to the certificate. + This works the same as lnd option {option}`tlsextraip`. + ''; + }; + extraDomains = mkOption { + type = with types; listOf str; + default = []; + example = [ "example.com" ]; + description = '' + Extra `subjectAltName` domain names added to the certificate. + This works the same as lnd option {option}`tlsextradomain`. + ''; + }; + }; + extraConfig = mkOption { + type = types.lines; + default = ""; + example = '' + autopilot.active=1 + ''; + description = '' + Extra lines appended to {file}`lnd.conf`. + See here for all available options: + https://github.com/lightningnetwork/lnd/blob/master/sample-lnd.conf + ''; + }; + package = mkOption { + type = types.package; + default = pkgs.lnd; + defaultText = "pkgs.lnd"; + description = "The package providing lnd binaries."; + }; + cli = mkOption { + default = pkgs.writers.writeBashBin "lncli" + # Switch user because lnd makes datadir contents readable by user only + '' + ${runAsUser} ${cfg.user} ${cfg.package}/bin/lncli \ + --rpcserver ${cfg.rpcAddress}:${toString cfg.rpcPort} \ + --tlscertpath '${cfg.certPath}' \ + --macaroonpath '${networkDir}/admin.macaroon' "$@" + ''; + defaultText = "(See source)"; + description = "Binary to connect with the lnd instance."; + }; + getPublicAddressCmd = mkOption { + type = types.str; + default = ""; + description = '' + Bash expression which outputs the public service address to announce to peers. + If left empty, no address is announced. + ''; + }; + user = mkOption { + type = types.str; + default = "lnd"; + description = "The user as which to run LND."; + }; + group = mkOption { + type = types.str; + default = cfg.user; + description = "The group as which to run LND."; + }; + certPath = mkOption { + readOnly = true; + default = "${secretsDir}/lnd-cert"; + description = "LND TLS certificate path."; + }; + tor = nbLib.tor; + }; + + cfg = config.services.lnd; + nbLib = config.nix-bitcoin.lib; + secretsDir = config.nix-bitcoin.secretsDir; + runAsUser = config.nix-bitcoin.runAsUserCmd; + lndinit = "${pkgs.lndinit}/bin/lndinit"; + + bitcoind = config.services.bitcoind; + + bitcoindRpcAddress = nbLib.address bitcoind.rpc.address; + networkDir = cfg.networkDir; + configFile = pkgs.writeText "lnd.conf" '' + datadir=${cfg.dataDir} + tlscertpath=${cfg.certPath} + tlskeypath=${secretsDir}/lnd-key + + # We're logging via journald + logging.file.disable=1 + logging.console.no-timestamps=1 + + listen=${toString cfg.address}:${toString cfg.port} + rpclisten=${cfg.rpcAddress}:${toString cfg.rpcPort} + restlisten=${cfg.restAddress}:${toString cfg.restPort} + + bitcoin.${bitcoind.network}=1 + bitcoin.node=bitcoind + + ${optionalString (cfg.tor.proxy) "tor.active=true"} + ${optionalString (cfg.tor-socks != null) "tor.socks=${cfg.tor-socks}"} + + bitcoind.rpchost=${bitcoindRpcAddress}:${toString bitcoind.rpc.port} + bitcoind.rpcuser=${bitcoind.rpc.users.public.name} + bitcoind.zmqpubrawblock=${zmqHandleSpecialAddress bitcoind.zmqpubrawblock} + bitcoind.zmqpubrawtx=${zmqHandleSpecialAddress bitcoind.zmqpubrawtx} + + wallet-unlock-password-file=${secretsDir}/lnd-wallet-password + + ${cfg.extraConfig} + ''; + + zmqHandleSpecialAddress = builtins.replaceStrings [ "0.0.0.0" "[::]" ] [ "127.0.0.1" "[::1]" ]; +in { + + inherit options; + + config = mkIf cfg.enable { + assertions = [ + { assertion = + !(config.services ? clightning) + || !config.services.clightning.enable + || config.services.clightning.port != cfg.port; + message = '' + LND and clightning can't both bind to lightning port 9735. Either + disable LND/clightning or change services.clightning.port or + services.lnd.port to a port other than 9735. + ''; + } + ]; + + services.bitcoind = { + enable = true; + + # Increase rpc thread count due to reports that lightning implementations fail + # under high bitcoind rpc load + rpc.threads = 16; + + zmqpubrawblock = mkDefault "tcp://${bitcoindRpcAddress}:28332"; + zmqpubrawtx = mkDefault "tcp://${bitcoindRpcAddress}:28333"; + }; + + environment.systemPackages = [ cfg.package (hiPrio cfg.cli) ]; + + systemd.tmpfiles.rules = [ + "d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -" + ]; + + services.lnd.certificate.extraIPs = mkIf (cfg.rpcAddress != "127.0.0.1") [ "${cfg.rpcAddress}" ]; + + systemd.services.lnd = { + wantedBy = [ "multi-user.target" ]; + requires = [ "bitcoind.service" ]; + after = [ "bitcoind.service" "nix-bitcoin-secrets.target" ]; + preStart = '' + install -m600 ${configFile} '${cfg.dataDir}/lnd.conf' + { + echo "bitcoind.rpcpass=$(cat ${secretsDir}/bitcoin-rpcpassword-public)" + ${optionalString (cfg.getPublicAddressCmd != "") '' + echo "externalip=$(${cfg.getPublicAddressCmd})" + ''} + } >> '${cfg.dataDir}/lnd.conf' + + if [[ ! -f ${networkDir}/wallet.db ]]; then + seed='${cfg.dataDir}/lnd-seed-mnemonic' + + if [[ ! -f "$seed" ]]; then + echo "Create lnd seed" + (umask u=r,go=; ${lndinit} gen-seed > "$seed") + fi + + echo "Create lnd wallet" + ${lndinit} -v init-wallet \ + --file.seed="$seed" \ + --file.wallet-password='${secretsDir}/lnd-wallet-password' \ + --init-file.output-wallet-dir='${cfg.networkDir}' + fi + ''; + serviceConfig = nbLib.defaultHardening // { + Type = "notify"; + RuntimeDirectory = "lnd"; # Only used to store custom macaroons + RuntimeDirectoryMode = "711"; + ExecStart = "${cfg.package}/bin/lnd --configfile='${cfg.dataDir}/lnd.conf'"; + User = cfg.user; + TimeoutSec = "15min"; + Restart = "on-failure"; + RestartSec = "10s"; + ReadWritePaths = [ cfg.dataDir ]; + ExecStartPost = let + curl = "${pkgs.curl}/bin/curl -fsS --cacert ${cfg.certPath}"; + restUrl = "https://${nbLib.addressWithPort cfg.restAddress cfg.restPort}/v1"; + # Setting macaroon permissions for other users needs root permissions + script = nbLib.rootScript "lnd-create-macaroons" '' + umask ug=r,o= + ${lib.concatMapStrings (macaroon: '' + echo "Create custom macaroon ${macaroon}" + macaroonPath="$RUNTIME_DIRECTORY/${macaroon}.macaroon" + ${curl} \ + -H "Grpc-Metadata-macaroon: $(${pkgs.xxd}/bin/xxd -ps -u -c 99999 '${networkDir}/admin.macaroon')" \ + -X POST \ + -d '{"permissions":[${cfg.macaroons.${macaroon}.permissions}]}' \ + ${restUrl}/macaroon |\ + ${pkgs.jq}/bin/jq -c '.macaroon' | ${pkgs.xxd}/bin/xxd -p -r > "$macaroonPath" + chown ${cfg.macaroons.${macaroon}.user}: "$macaroonPath" + '') (attrNames cfg.macaroons)} + ''; + in [ + script + ]; + } // nbLib.allowedIPAddresses cfg.tor.enforce; + }; + + users.users.${cfg.user} = { + isSystemUser = true; + group = cfg.group; + extraGroups = [ "bitcoinrpc-public" ]; + home = cfg.dataDir; # lnd creates .lnd dir in HOME + }; + users.groups.${cfg.group} = {}; + nix-bitcoin.operator = { + groups = [ cfg.group ]; + allowRunAsUsers = [ cfg.user ]; + }; + + nix-bitcoin.secrets = { + lnd-wallet-password.user = cfg.user; + lnd-key.user = cfg.user; + lnd-cert.user = cfg.user; + lnd-cert.permissions = "444"; # world readable + }; + # Advantages of manually pre-generating certs: + # - Reduces dynamic state + # - Enables deployment of a mesh of server plus client nodes with predefined certs + nix-bitcoin.generateSecretsCmds.lnd = '' + makePasswordSecret lnd-wallet-password + makeCert lnd '${nbLib.mkCertExtraAltNames cfg.certificate}' + ''; + }; +} diff --git a/modules/vendor/nix-bitcoin/lndconnect.nix b/modules/vendor/nix-bitcoin/lndconnect.nix new file mode 100644 index 0000000..234fd98 --- /dev/null +++ b/modules/vendor/nix-bitcoin/lndconnect.nix @@ -0,0 +1,282 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + options = { + services.lnd.lndconnect = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Add a `lndconnect` binary to the system environment which prints + connection info for lnd clients. + See: https://github.com/LN-Zap/lndconnect + + Usage: + ```bash + # Print QR code + lndconnect + + # Print URL + lndconnect --url + ``` + ''; + }; + onion = mkOption { + type = types.bool; + default = false; + description = '' + Create an onion service for the lnd REST server, + which is used by lndconnect. + ''; + }; + }; + + services.clightning.plugins.clnrest.lnconnect = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Add a `lnconnect-clnrest` binary to the system environment which prints + connection info for clightning clients. + See: https://github.com/LN-Zap/lndconnect + + Usage: + ```bash + # Print QR code + lnconnect-clnrest + + # Print URL + lnconnect-clnrest --url + ``` + ''; + }; + onion = mkOption { + type = types.bool; + default = false; + description = '' + Create an onion service for the clnrest server, + which is used by lnconnect. + ''; + }; + }; + + services.clightning-rest.lndconnect = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Add a `lndconnect-clightning` binary to the system environment which prints + connection info for clightning clients. + See: https://github.com/LN-Zap/lndconnect + + Usage: + ```bash + # Print QR code + lndconnect-clightning + + # Print URL + lndconnect-clightning --url + ``` + ''; + }; + onion = mkOption { + type = types.bool; + default = false; + description = '' + Create an onion service for the clightning REST server, + which is used by lndconnect. + ''; + }; + }; + + nix-bitcoin.mkLndconnect = mkOption { + readOnly = true; + default = mkLndconnect; + description = '' + A function to create a lndconnect binary. + See the source for further details. + ''; + }; + }; + + nbLib = config.nix-bitcoin.lib; + runAsUser = config.nix-bitcoin.runAsUserCmd; + + inherit (config.services) + lnd + clightning + clightning-rest; + + inherit (clightning.plugins) clnrest; + + mkLndconnect = { + name, + shebang ? "#!${pkgs.stdenv.shell} -e", + isClightning ? false, + isClnrest ? false, + port, + authSecretPath, + enableOnion, + onionService ? null, + certPath ? null + }: + # TODO-EXTERNAL: + # lndconnect requires a --configfile argument, although it's unused + # https://github.com/LN-Zap/lndconnect/issues/25 + lib.hiPrio (pkgs.writeScriptBin name '' + ${shebang} + url=$( + ${getExe pkgs.lndconnect} --url \ + ${optionalString enableOnion "--host=$(cat ${config.nix-bitcoin.onionAddresses.dataDir}/${onionService})"} \ + --port=${toString port} \ + ${if enableOnion || certPath == null then "--nocert" else "--tlscertpath='${certPath}'"} \ + --adminmacaroonpath='${authSecretPath}' \ + --configfile=/dev/null "$@" + ) + + ${optionalString isClightning + # - Change URL procotcol to c-lightning-rest + # - Encode macaroon as hex (in uppercase) instead of base 64. + # Because `macaroon` is always the last URL fragment, the + # sed replacement below works correctly. + '' + macaroonHex=$(${getExe pkgs.xxd} -p -u -c 99999 '${authSecretPath}') + url=$( + echo "$url" | ${getExe pkgs.gnused} " + s|^lndconnect|c-lightning-rest| + s|macaroon=.*|macaroon=$macaroonHex| + "; + ) + '' + } + + ${optionalString isClnrest + # Change URL procotcol to clnrest + '' + url=$( + echo "$url" | ${getExe pkgs.gnused} " + s|^lndconnect|clnrest| + s|macaroon=.*|rune=$(cat '${authSecretPath}')| + "; + ) + '' + } + + # If --url is in args + if [[ " $* " =~ " --url " ]]; then + echo "$url" + else + # This UTF-8 encoding yields a smaller, more convenient output format + # compared to the native lndconnect output + echo -n "$url" | ${getExe pkgs.qrencode} -t UTF8 -o - + fi + ''); + + operatorName = config.nix-bitcoin.operator.name; +in { + inherit options; + + config = mkMerge [ + (mkIf (lnd.enable && lnd.lndconnect.enable) + (mkMerge [ + { + environment.systemPackages = [( + mkLndconnect { + name = "lndconnect"; + # Run as lnd user because the macaroon and cert are not group-readable + shebang = "#!/usr/bin/env -S ${runAsUser} ${lnd.user} ${pkgs.bash}/bin/bash"; + enableOnion = lnd.lndconnect.onion; + onionService = "${lnd.user}/lnd-rest"; + port = lnd.restPort; + certPath = lnd.certPath; + authSecretPath = "${lnd.networkDir}/admin.macaroon"; + } + )]; + + services.lnd.restAddress = mkIf (!lnd.lndconnect.onion) "0.0.0.0"; + } + + (mkIf lnd.lndconnect.onion { + services.tor = { + enable = true; + relay.onionServices.lnd-rest = nbLib.mkOnionService { + target.addr = nbLib.address lnd.restAddress; + target.port = lnd.restPort; + port = lnd.restPort; + }; + }; + nix-bitcoin.onionAddresses.access = { + ${lnd.user} = [ "lnd-rest" ]; + ${operatorName} = [ "lnd-rest" ]; + }; + }) + ])) + + (mkIf (clnrest.enable && clnrest.lnconnect.enable) + (mkMerge [ + { + environment.systemPackages = [( + mkLndconnect { + name = "lnconnect-clnrest"; + isClnrest = true; + enableOnion = clnrest.lnconnect.onion; + onionService = "${operatorName}/clnrest"; + port = clnrest.port; + certPath = "${clightning.networkDir}/client.pem"; + authSecretPath = "${clightning.networkDir}/admin-rune"; + } + )]; + + services.clightning.plugins.clnrest.address = mkIf (!clnrest.lnconnect.onion) "0.0.0.0"; + } + + (mkIf clnrest.lnconnect.onion { + services.tor = { + enable = true; + relay.onionServices.clnrest = nbLib.mkOnionService { + target.addr = nbLib.address clnrest.address; + target.port = clnrest.port; + port = clnrest.port; + }; + }; + # This also allows nodeinfo to show the clnrest onion address + nix-bitcoin.onionAddresses.access.${operatorName} = [ "clnrest" ]; + }) + ]) + ) + + (mkIf (clightning-rest.enable && clightning-rest.lndconnect.enable) + (mkMerge [ + { + environment.systemPackages = [( + mkLndconnect { + name = "lndconnect-clightning"; + isClightning = true; + enableOnion = clightning-rest.lndconnect.onion; + onionService = "${operatorName}/clightning-rest"; + port = clightning-rest.port; + certPath = "${clightning-rest.dataDir}/certs/certificate.pem"; + authSecretPath = "${clightning-rest.dataDir}/certs/access.macaroon"; + } + )]; + + # clightning-rest always binds to all interfaces + } + + (mkIf clightning-rest.lndconnect.onion { + services.tor = { + enable = true; + relay.onionServices.clightning-rest = nbLib.mkOnionService { + target.addr = nbLib.address clightning-rest.address; + target.port = clightning-rest.port; + port = clightning-rest.port; + }; + }; + # This also allows nodeinfo to show the clightning-rest onion address + nix-bitcoin.onionAddresses.access.${operatorName} = [ "clightning-rest" ]; + }) + ]) + ) + ]; +} diff --git a/modules/vendor/nix-bitcoin/mempool.nix b/modules/vendor/nix-bitcoin/mempool.nix new file mode 100644 index 0000000..ce357b2 --- /dev/null +++ b/modules/vendor/nix-bitcoin/mempool.nix @@ -0,0 +1,349 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + options.services = { + mempool = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Enable Mempool, a fully featured Bitcoin visualizer, explorer, and API service. + + Note: Mempool enables `txindex` in bitcoind (this is a requirement). + + This module has two components: + - A backend service (systemd service `mempool`) + + - An optional web interface run by nginx, defined by options `services.mempool.frontend.*`. + The frontend is enabled by default when mempool is enabled. + For details, see `services.mempool.frontend.enable`. + ''; + }; + + frontend = { + enable = mkOption { + type = types.bool; + default = cfg.enable; + description = '' + Enable the mempool frontend (web interface). + This starts a simple nginx instance, configured for local usage with + settings similar to the `mempool/frontend` Docker image. + + IMPORTANT: + If you want to expose the mempool frontend to the internet, you + should create a custom nginx config that includes TLS, backend caching, rate limiting + and performance tuning. + For this task, reuse the config snippets from option `services.mempool.frontend.nginxConfig`. + See also: https://github.com/fort-nix/nixbitcoin.org/blob/master/website/mempool.nix, + which contains a mempool nginx config for public hosting (running at + https://mempool.nixbitcoin.org). + ''; + }; + address = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "HTTP server address."; + }; + port = mkOption { + type = types.port; + default = 60845; # A random private port + description = "HTTP server port."; + }; + settings = mkOption { + type = with types; attrsOf anything; + default = {}; + example = { + TESTNET_ENABLED = true; + MEMPOOL_WEBSITE_URL = "mempool.mynode.org"; + }; + description = '' + Mempool frontend settings. + See here for available options: + https://github.com/mempool/mempool/blob/master/frontend/src/app/services/state.service.ts + (`interface Env` and `defaultEnv`) + ''; + }; + staticContentRoot = mkOption { + type = types.path; + default = pkgs.mempool-frontend.withConfig cfg.frontend.settings; + defaultText = "pkgs.mempool-frontend"; + description = " + Path of the static frontend content root. + "; + }; + nginxConfig = mkOption { + readOnly = true; + default = frontend.nginxConfig; + defaultText = "(See source)"; + description = " + An attrset of nginx config snippets for assembling a custom + mempool nginx config. + For details, see the source comments at the point of definition. + "; + }; + }; + + address = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "Mempool backend address."; + }; + port = mkOption { + type = types.port; + default = 8999; + description = "Mempool backend port."; + }; + electrumServer = mkOption { + type = types.enum [ "electrs" "fulcrum" ]; + default = "electrs"; + description = '' + The Electrum server to use for fetching address information. + + Possible options: + - electrs: + Small database size, slow when querying new addresses. + - fulcrum: + Large database size, quickly serves arbitrary address queries. + ''; + }; + settings = mkOption { + type = with types; attrsOf (attrsOf anything); + example = { + MEMPOOL = { + POLL_RATE_MS = 3000; + STDOUT_LOG_MIN_PRIORITY = "debug"; + }; + PRICE_DATA_SERVER = { + CLEARNET_URL = "https://myserver.org/prices"; + }; + }; + description = '' + Mempool backend settings. + See here for available options: + https://github.com/mempool/mempool/blob/master/backend/src/config.ts + ''; + }; + database = { + name = mkOption { + type = types.str; + default = "mempool"; + description = "Database name."; + }; + }; + package = mkOption { + type = types.package; + default = pkgs.mempool-backend; + defaultText = "pkgs.mempool-backend"; + description = "The package providing mempool binaries."; + }; + user = mkOption { + type = types.str; + default = "mempool"; + description = "The user as which to run Mempool."; + }; + group = mkOption { + type = types.str; + default = cfg.user; + description = "The group as which to run Mempool."; + }; + tor = nbLib.tor; + }; + + # Internal read-only options used by `./nodeinfo.nix` and `./onion-services.nix` + mempool-frontend = let + inherit (nbLib) mkAlias; + in { + enable = mkAlias cfg.frontend.enable; + address = mkAlias cfg.frontend.address; + port = mkAlias cfg.frontend.port; + }; + }; + + cfg = config.services.mempool; + nbLib = config.nix-bitcoin.lib; + nbPkgs = pkgs; # vendored: now alias to pkgs + secretsDir = config.nix-bitcoin.secretsDir; + + configFile = builtins.toFile "mempool-config" (builtins.toJSON cfg.settings); + cacheDir = "/var/cache/mempool"; + + inherit (config.services) + bitcoind + electrs + fulcrum; + + torSocket = config.services.tor.client.socksListenAddress; + + # See the `services.nginx` definition further below below + # on how to use these snippets. + frontend.nginxConfig = { + # This must be added to `services.nginx.commonHttpConfig` when + # `mempool/location-static.conf` is used + httpConfig = '' + include ${if pkgs ? mempool-nginx-conf then "${pkgs.mempool-nginx-conf}/http-language.conf" else "/dev/null"}; + ''; + + # Config for static website content. + # This should be added to `services.nginx.virtualHosts..extraConfig`. + # Adapted from mempool/nginx-mempool.conf and mempool/production/nginx/location-redirects.conf + staticContent = '' + index index.html; + + add_header Cache-Control "public, no-transform"; + add_header Vary Accept-Language; + add_header Vary Cookie; + + include ${if pkgs ? mempool-nginx-conf then "${pkgs.mempool-nginx-conf}/location-static.conf" else "/dev/null"}; + + # Redirect /api to /docs/api + location = /api { + return 308 https://$host/docs/api; + } + location = /api/ { + return 308 https://$host/docs/api; + } + ''; + + # Config for backend API. + # This should be added to `services.nginx.virtualHosts..extraConfig`. + # Adapted from mempool/nginx-mempool.conf and mempool/production/nginx/location-api.conf. + proxyApi = let + backend = "http://${nbLib.addressWithPort cfg.address cfg.port}"; + in '' + location /api/ { + proxy_pass ${backend}/api/v1/; + } + location /api/v1 { + proxy_pass ${backend}; + } + # Websocket API + location /api/v1/ws { + proxy_pass ${backend}; + + # Websocket header settings + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + + # Relevant settings from `recommendedProxyConfig` (nixos/nginx/default.nix) + # (In the above api locations, these are inherited from the parent scope) + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + ''; + }; + +in { + inherit options; + + config = mkIf cfg.enable { + services.bitcoind.txindex = true; + services.electrs.enable = mkIf (cfg.electrumServer == "electrs" ) true; + services.fulcrum.enable = mkIf (cfg.electrumServer == "fulcrum" ) true; + services.mysql = { + enable = true; + package = pkgs.mariadb; + ensureDatabases = [ cfg.database.name ]; + ensureUsers = [ + { + name = cfg.user; + ensurePermissions."${cfg.database.name}.*" = "ALL PRIVILEGES"; + } + ]; + }; + + # Available options: + # https://github.com/mempool/mempool/blob/master/backend/src/config.ts + services.mempool.settings = { + MEMPOOL = { + # mempool doesn't support regtest + NETWORK = "mainnet"; + BACKEND = "electrum"; + HTTP_PORT = cfg.port; + CACHE_DIR = "${cacheDir}/cache"; + STDOUT_LOG_MIN_PRIORITY = mkDefault "info"; + AUTOMATIC_POOLS_UPDATE = true; + }; + CORE_RPC = { + HOST = bitcoind.rpc.address; + PORT = bitcoind.rpc.port; + USERNAME = bitcoind.rpc.users.public.name; + PASSWORD = "@btcRpcPassword@"; + }; + ELECTRUM = let + server = config.services.${cfg.electrumServer}; + in { + HOST = server.address; + PORT = server.port; + TLS_ENABLED = false; + }; + DATABASE = { + ENABLED = true; + DATABASE = cfg.database.name; + SOCKET = "/run/mysqld/mysqld.sock"; + PID_DIR = cacheDir; + }; + } // optionalAttrs (cfg.tor.proxy) { + # Use Tor for rate fetching and pool updating + SOCKS5PROXY = { + ENABLED = true; + USE_ONION = true; + HOST = torSocket.addr; + PORT = torSocket.port; + }; + }; + + systemd.services.mempool = rec { + wantedBy = [ "multi-user.target" ]; + requires = [ "mysql.service" ]; + wants = [ "${cfg.electrumServer}.service" ]; + after = requires ++ wants; + preStart = '' + mkdir -p '${cacheDir}/cache' + <${configFile} sed \ + -e "s|@btcRpcPassword@|$(cat ${secretsDir}/bitcoin-rpcpassword-public)|" \ + > '${cacheDir}/config.json' + ''; + environment.MEMPOOL_CONFIG_FILE = "${cacheDir}/config.json"; + serviceConfig = nbLib.defaultHardening // { + ExecStart = "${cfg.package}/bin/mempool-backend"; + CacheDirectory = "mempool"; + CacheDirectoryMode = "770"; + # Show "mempool" instead of "node" in the journal + SyslogIdentifier = "mempool"; + User = cfg.user; + Restart = "on-failure"; + RestartSec = "10s"; + } // nbLib.allowedIPAddresses cfg.tor.enforce + // nbLib.nodejs; + }; + + services.nginx = mkIf cfg.frontend.enable { + enable = true; + enableReload = true; + recommendedBrotliSettings = true; + recommendedGzipSettings = true; + recommendedOptimisation = true; + recommendedProxySettings = true; + recommendedTlsSettings = true; + commonHttpConfig = frontend.nginxConfig.httpConfig; + virtualHosts."mempool" = { + serverName = "_"; + listen = [ { addr = cfg.frontend.address; port = cfg.frontend.port; } ]; + root = cfg.frontend.staticContentRoot; + extraConfig = + frontend.nginxConfig.staticContent + + frontend.nginxConfig.proxyApi; + }; + }; + + users.users.${cfg.user} = { + isSystemUser = true; + group = cfg.group; + extraGroups = [ "bitcoinrpc-public" ]; + }; + users.groups.${cfg.group} = {}; + }; +} diff --git a/modules/vendor/nix-bitcoin/modules.nix b/modules/vendor/nix-bitcoin/modules.nix new file mode 100644 index 0000000..4abeb46 --- /dev/null +++ b/modules/vendor/nix-bitcoin/modules.nix @@ -0,0 +1,27 @@ +# Vendored nix-bitcoin — MINIMAL subset used by Sovran_SystemsOS +# Original source: https://github.com/fort-nix/nix-bitcoin +# Only services actually used by Sovran are kept (6 services vs 20+ upstream) +# - backups.nix removed: Sovran uses rsnapshot to Second_Drive (configuration.nix) +# - netns-isolation.nix is now a stub (requires false for nwc-wallets) +{ + imports = [ + ./nix-bitcoin.nix + ./secrets/secrets.nix + ./operator.nix + ./bitcoind.nix + ./electrs.nix + ./lnd.nix + ./lndconnect.nix + ./rtl.nix + ./btcpayserver.nix + ./mempool.nix + ./security.nix + ./onion-addresses.nix + ./onion-services.nix + ./netns-isolation.nix + ./nodeinfo.nix + ./versioning.nix + ]; + + disabledModules = [ "services/networking/bitcoind.nix" ]; +} diff --git a/modules/vendor/nix-bitcoin/netns-isolation.nix b/modules/vendor/nix-bitcoin/netns-isolation.nix new file mode 100644 index 0000000..1a2aca3 --- /dev/null +++ b/modules/vendor/nix-bitcoin/netns-isolation.nix @@ -0,0 +1,16 @@ +{ config, lib, ... }: +with lib; +{ + options.nix-bitcoin.netns-isolation = { + enable = mkEnableOption "netns isolation (stub — disabled in Sovran_SystemsOS)"; + }; + # No config when enabled — isolation is intentionally not implemented. + # Sovran requires enable = false (see modules/nwc-wallets.nix assertion). + # The original nix-bitcoin implementation (365 lines, bridge nb-br, iptables, + # ip netns, 169.254.x.x) broke Caddy/AlbyHub/RTL and is not needed for + # desktop/server roles. Keep stub so `nix-bitcoin.netns-isolation.enable` + # remains a valid option. + config = mkIf config.nix-bitcoin.netns-isolation.enable { + warnings = [ "nix-bitcoin.netns-isolation.enable is a stub in vendored Sovran and does nothing. Set it to false." ]; + }; +} diff --git a/modules/vendor/nix-bitcoin/nix-bitcoin.nix b/modules/vendor/nix-bitcoin/nix-bitcoin.nix new file mode 100644 index 0000000..609415d --- /dev/null +++ b/modules/vendor/nix-bitcoin/nix-bitcoin.nix @@ -0,0 +1,60 @@ +{ config, pkgs, lib, ... }: + +with lib; +{ + options = { + nix-bitcoin = { + # Kept for compatibility, now simply aliases system pkgs + pkgs = mkOption { + type = types.attrs; + default = pkgs; + defaultText = "pkgs"; + description = "Alias to system pkgs (vendored nix-bitcoin now uses nixpkgs directly)."; + }; + + useVersionLockedPkgs = mkOption { + type = types.bool; + default = false; + description = "Deprecated — vendored modules always use system pkgs."; + }; + + pkgOverlays = mkOption { + internal = true; + type = with types; functionTo attrs; + default = _: _: {}; + description = "Deprecated stub."; + }; + + lib = mkOption { + readOnly = true; + default = import ./lib.nix lib pkgs config; + defaultText = "vendor/nix-bitcoin/lib.nix"; + }; + + torClientAddressWithPort = mkOption { + readOnly = true; + default = with config.services.tor.client.socksListenAddress; + "${addr}:${toString port}"; + defaultText = "(See source)"; + }; + + torify = mkOption { + readOnly = true; + default = pkgs.writers.writeBashBin "torify" '' + ${pkgs.tor}/bin/torify \ + --address ${config.services.tor.client.socksListenAddress.addr} \ + "$@" + ''; + defaultText = "(See source)"; + }; + + runAsUserCmd = mkOption { + readOnly = true; + default = if config.security.doas.enable + then "doas -u" + else "sudo -u"; + defaultText = "(See source)"; + }; + }; + }; +} diff --git a/modules/vendor/nix-bitcoin/nodeinfo.nix b/modules/vendor/nix-bitcoin/nodeinfo.nix new file mode 100644 index 0000000..3abeb57 --- /dev/null +++ b/modules/vendor/nix-bitcoin/nodeinfo.nix @@ -0,0 +1,167 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + options = { + nix-bitcoin.nodeinfo = { + enable = mkEnableOption "nodeinfo"; + + program = mkOption { + readOnly = true; + default = script; + defaultText = "(See source)"; + }; + + services = mkOption { + internal = true; + type = types.attrs; + default = {}; + defaultText = "(See source)"; + description = '' + Nodeinfo service definitions. + ''; + }; + + lib = mkOption { + internal = true; + readOnly = true; + default = nodeinfoLib; + defaultText = "(See source)"; + description = '' + Helper functions for defining nodeinfo services. + ''; + }; + }; + }; + + cfg = config.nix-bitcoin.nodeinfo; + nbLib = config.nix-bitcoin.lib; + + script = pkgs.writeScriptBin "nodeinfo" '' + #!${pkgs.python3}/bin/python + + import json + import subprocess + import sys + from collections import OrderedDict + + def success(*args): + return subprocess.call(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0 + + def is_active(unit): + return success("systemctl", "is-active", "--quiet", unit) + + def is_enabled(unit): + return success("systemctl", "is-enabled", "--quiet", unit) + + def cmd(*args): + return subprocess.run(args, stdout=subprocess.PIPE).stdout.decode('utf-8') + + def shell(*args): + return cmd("bash", "-c", *args).strip() + + infos = OrderedDict() + operator = "${config.nix-bitcoin.operator.name}" + + def get_onion_address(name, port): + path = f"/var/lib/onion-addresses/{operator}/{name}" + try: + with open(path, "r") as f: + onion_address = f.read().strip() + except OSError: + print(f"error reading file {path}", file=sys.stderr) + return + return f"{onion_address}:{port}" + + def add_service(service, make_info, systemd_service = None): + systemd_service = systemd_service or service + if not is_active(systemd_service): + infos[service] = f"'{systemd_service}.service' is not running" + else: + info = OrderedDict() + exec(make_info, globals(), locals()) + infos[service] = info + + if is_enabled("onion-adresses") and not is_active("onion-adresses"): + print("error: service 'onion-adresses' is not running") + exit(1) + + ${concatStrings infos} + + print(json.dumps(infos, indent=2)) + ''; + + infos = map (serviceName: + let serviceCfg = config.services.${serviceName}; + in optionalString serviceCfg.enable (cfg.services.${serviceName} serviceName serviceCfg) + ) (builtins.attrNames cfg.services); + + nodeinfoLib = rec { + mkInfo = extraCode: name: cfg: + mkInfoLong { + inherit extraCode name cfg; + }; + + mkInfoLong = { extraCode ? "", name, cfg, systemdServiceName ? name }: '' + add_service("${name}", """ + info["local_address"] = "${nbLib.addressWithPort cfg.address cfg.port}" + '' + mkIfOnionPort name (onionPort: '' + info["onion_address"] = get_onion_address("${name}", ${onionPort}) + '') + extraCode + '' + + """, "${systemdServiceName}") + ''; + + mkIfOnionPort = name: fn: + if onionServices ? ${name} then + fn (toString (builtins.elemAt onionServices.${name}.map 0).port) + else + ""; + }; + + inherit (config.services.tor.relay) onionServices; +in { + inherit options; + + config = mkIf cfg.enable { + environment.systemPackages = [ script ]; + + nix-bitcoin.operator.enable = true; + + nix-bitcoin.nodeinfo.services = with nodeinfoLib; { + bitcoind = mkInfo ""; + clightning = mkInfo '' + info["nodeid"] = shell("lightning-cli getinfo | jq -r '.id'") + if 'onion_address' in info: + info["id"] = f"{info['nodeid']}@{info['onion_address']}" + ''; + lnd = name: cfg: mkInfo ('' + info["rest_address"] = "${nbLib.addressWithPort cfg.restAddress cfg.restPort}" + '' + mkIfOnionPort "lnd-rest" (onionPort: '' + info["onion_rest_address"] = get_onion_address("lnd-rest", ${onionPort}) + '') + '' + info["nodeid"] = shell("lncli getinfo | jq -r '.identity_pubkey'") + '') name cfg; + clnrest = name: cfg: mkInfoLong { + inherit name cfg; + systemdServiceName = "clightning"; + }; + clightning-rest = mkInfo ""; + electrs = mkInfo ""; + fulcrum = mkInfo ""; + btcpayserver = mkInfo ""; + liquidd = mkInfo ""; + joinmarket-ob-watcher = mkInfo ""; + rtl = mkInfo ""; + mempool = mkInfo ""; + mempool-frontend = name: cfg: mkInfoLong { + inherit name cfg; + systemdServiceName = "nginx"; + }; + # Only add sshd when it has an onion service + sshd = name: cfg: mkIfOnionPort "sshd" (onionPort: '' + add_service("sshd", """info["onion_address"] = get_onion_address("sshd", ${onionPort})""") + ''); + }; + }; +} diff --git a/modules/vendor/nix-bitcoin/onion-addresses.nix b/modules/vendor/nix-bitcoin/onion-addresses.nix new file mode 100644 index 0000000..85193be --- /dev/null +++ b/modules/vendor/nix-bitcoin/onion-addresses.nix @@ -0,0 +1,106 @@ +# This module enables unprivileged users to read onion addresses. +# By default, onion addresses in /var/lib/tor/onion are only readable by the +# tor user. +# The included service copies onion addresses to /var/lib/onion-addresses// +# and sets permissions according to option 'access'. + +{ config, lib, ... }: + +with lib; +let + options.nix-bitcoin.onionAddresses = { + access = mkOption { + type = with types; attrsOf (listOf str); + default = {}; + description = '' + This option controls who is allowed to access onion addresses. + For example, the following allows user 'myuser' to access bitcoind + and clightning onion addresses: + ```nix + { + "myuser" = [ "bitcoind" "clightning" ]; + }; + ``` + The onion hostnames can then be read from + {file}`/var/lib/onion-addresses/myuser`. + ''; + }; + services = mkOption { + type = with types; listOf str; + default = []; + description = '' + Services that can access their onion address via file + {file}`/var/lib/onion-addresses/` + The file is readable only by the service user. + ''; + }; + dataDir = mkOption { + readOnly = true; + default = "/var/lib/onion-addresses"; + }; + }; + + cfg = config.nix-bitcoin.onionAddresses; + nbLib = config.nix-bitcoin.lib; +in { + inherit options; + + config = mkIf (cfg.access != {} || cfg.services != []) { + systemd.services.onion-addresses = { + wantedBy = [ "tor.service" ]; + bindsTo = [ "tor.service" ]; + after = [ "tor.service" ]; + serviceConfig = nbLib.defaultHardening // { + Type = "oneshot"; + RemainAfterExit = true; + StateDirectory = "onion-addresses"; + StateDirectoryMode = "771"; + PrivateNetwork = true; # This service needs no network access + PrivateUsers = false; + CapabilityBoundingSet = "CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_FOWNER CAP_IPC_OWNER"; + }; + script = '' + waitForFile() { + file=$1 + for ((i=0; i<300; i++)); do + if [[ -e $file ]]; then + return; + fi + sleep 0.1 + done + echo "Error: File $file did not appear after 30 sec." + exit 1 + } + + # Wait until tor is up + waitForFile /var/lib/tor/state + + cd ${cfg.dataDir} + rm -rf ./* + + ${concatMapStrings + (user: '' + mkdir -p -m 0700 ${user} + chown ${user} ${user} + ${concatMapStrings + (service: '' + onionFile='/var/lib/tor/onion/${service}/hostname' + waitForFile "$onionFile" + cp "$onionFile" '${user}/${service}' + chown '${user}' '${user}/${service}' + '') + cfg.access.${user} + } + '') + (builtins.attrNames cfg.access) + } + + ${concatMapStrings (service: '' + onionFile=/var/lib/tor/onion/${service}/hostname + waitForFile "$onionFile" + install -D -o ${config.systemd.services.${service}.serviceConfig.User} -m 400 "$onionFile" services/${service} + '') cfg.services} + ''; + }; + }; +} diff --git a/modules/vendor/nix-bitcoin/onion-services.nix b/modules/vendor/nix-bitcoin/onion-services.nix new file mode 100644 index 0000000..716ebb5 --- /dev/null +++ b/modules/vendor/nix-bitcoin/onion-services.nix @@ -0,0 +1,122 @@ +# This module creates onion-services for NixOS services. +# An onion service can be enabled for every service that defines +# options 'address', 'port' and optionally 'getPublicAddressCmd'. +# +# See it in use at ./presets/enable-tor.nix + +{ config, lib, pkgs, ... }: + +with lib; +let + options.nix-bitcoin.onionServices = mkOption { + default = {}; + type = with types; attrsOf (submodule ( + { config, ... }: { + options = { + enable = mkOption { + type = types.bool; + default = config.public; + description = '' + Create an onion service for the given service. + The service must define options {option}`address` and {option}`onionPort` (or `port`). + ''; + }; + public = mkOption { + type = types.bool; + default = false; + description = '' + Make the onion address accessible to the service. + If enabled, the onion service is automatically enabled. + Only available for services that define option {option}`getPublicAddressCmd`. + ''; + }; + externalPort = mkOption { + type = types.nullOr types.port; + default = null; + description = "Override the external port of the onion service."; + }; + }; + } + )); + }; + + cfg = config.nix-bitcoin.onionServices; + nbLib = config.nix-bitcoin.lib; + + onionServices = builtins.attrNames cfg; + + activeServices = builtins.filter (service: + config.services.${service}.enable && cfg.${service}.enable + ) onionServices; + + publicServices = builtins.filter (service: cfg.${service}.public) activeServices; +in { + inherit options; + + config = mkMerge [ + (mkIf (activeServices != []) { + # Define hidden services + services.tor = { + enable = true; + relay.onionServices = genAttrs activeServices (name: + let + service = config.services.${name}; + inherit (cfg.${name}) externalPort; + in nbLib.mkOnionService { + port = if externalPort != null then externalPort else service.port; + target.port = service.onionPort or service.port; + target.addr = nbLib.address service.address; + } + ); + }; + + nix-bitcoin.onionAddresses = { + # Enable public services to access their own onion addresses + services = publicServices; + + # Allow the operator user to access onion addresses for all active services + access.${config.nix-bitcoin.operator.name} = mkIf config.nix-bitcoin.operator.enable activeServices; + }; + systemd.services = let + onionAddresses = [ "onion-addresses.service" ]; + in genAttrs publicServices (service: { + # TODO-EXTERNAL: Instead of `wants`, use a future systemd dependency type + # that propagates initial start failures but no restarts + wants = onionAddresses; + after = onionAddresses; + }); + }) + + # Set getPublicAddressCmd for public services + { + services = let + # publicServices' doesn't depend on config.services.*.enable, + # so we can use it to define config.services without causing infinite recursion + publicServices' = builtins.filter (service: + let srv = cfg.${service}; + in srv.public && srv.enable + ) onionServices; + in genAttrs publicServices' (service: { + getPublicAddressCmd = "cat ${config.nix-bitcoin.onionAddresses.dataDir}/services/${service}"; + }); + } + + # Set sensible defaults for some services + { + nix-bitcoin.onionServices = { + btcpayserver = { + externalPort = 80; + }; + joinmarket-ob-watcher = { + externalPort = 80; + }; + rtl = { + externalPort = 80; + }; + mempool-frontend = { + externalPort = 80; + }; + }; + } + ]; +} diff --git a/modules/vendor/nix-bitcoin/operator.nix b/modules/vendor/nix-bitcoin/operator.nix new file mode 100644 index 0000000..173d2a4 --- /dev/null +++ b/modules/vendor/nix-bitcoin/operator.nix @@ -0,0 +1,58 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + options.nix-bitcoin.operator = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to define a user named `operator` for convenient interactive access + to nix-bitcoin features (like `bitcoin-cli`). + + When using nix-bitcoin as part of a larger system config, it makes sense + to set your main system user as the operator, by setting option + `nix-bitcoin.operator.name = "MAIN_USER_NAME";`. + ''; + }; + name = mkOption { + type = types.str; + default = "operator"; + description = "Name of the operator user."; + }; + groups = mkOption { + type = with types; listOf str; + default = []; + description = "Extra groups of the operatur user."; + }; + allowRunAsUsers = mkOption { + type = with types; listOf str; + default = []; + description = "Users as which the operator is allowed to run commands."; + }; + }; + + cfg = config.nix-bitcoin.operator; +in { + inherit options; + + config = mkIf cfg.enable { + users.users.${cfg.name} = { + isNormalUser = true; + extraGroups = [ + "systemd-journal" + "proc" # Enable full /proc access and systemd-status + ] ++ cfg.groups; + }; + + security = mkIf (cfg.allowRunAsUsers != []) { + # Use doas instead of sudo if enabled + doas.extraConfig = mkIf config.security.doas.enable '' + ${lib.concatMapStrings (user: "permit nopass ${cfg.name} as ${user}\n") cfg.allowRunAsUsers} + ''; + sudo.extraConfig = mkIf (!config.security.doas.enable) '' + ${cfg.name} ALL=(${builtins.concatStringsSep "," cfg.allowRunAsUsers}) NOPASSWD: ALL + ''; + }; + }; +} diff --git a/modules/vendor/nix-bitcoin/rtl.nix b/modules/vendor/nix-bitcoin/rtl.nix new file mode 100644 index 0000000..31ef138 --- /dev/null +++ b/modules/vendor/nix-bitcoin/rtl.nix @@ -0,0 +1,239 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + options.services.rtl = { + enable = mkEnableOption "Ride The Lightning, a web interface for lnd and clightning"; + address = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "HTTP server address."; + }; + port = mkOption { + type = types.port; + default = 3000; + description = "HTTP server port."; + }; + dataDir = mkOption { + type = types.path; + default = "/var/lib/rtl"; + description = "The data directory for RTL."; + }; + nodes = { + clightning = { + enable = mkOption { + type = types.bool; + default = false; + description = "Enable the clightning node interface."; + }; + extraConfig = mkOption { + type = with types; attrsOf anything; + default = {}; + example = { + Settings.userPersona = "MERCHANT"; + Settings.logLevel = "DEBUG"; + }; + description = '' + Extra clightning node configuration. + See here for all available options: + https://github.com/Ride-The-Lightning/RTL/blob/master/.github/docs/Application_configurations.md + ''; + }; + }; + lnd = { + enable = mkOption { + type = types.bool; + default = false; + description = "Enable the lnd node interface."; + }; + loop = mkOption { + type = types.bool; + default = false; + description = "Enable swaps with lightning-loop."; + }; + extraConfig = mkOption { + type = with types; attrsOf anything; + default = {}; + example = { + Settings.userPersona = "MERCHANT"; + Settings.logLevel = "DEBUG"; + }; + description = '' + Extra lnd node configuration. + See here for all available options: + https://github.com/Ride-The-Lightning/RTL/blob/master/.github/docs/Application_configurations.md + ''; + }; + }; + reverseOrder = mkOption { + type = types.bool; + default = false; + description = '' + Reverse the order of nodes shown in the UI. + By default, clightning is shown before lnd. + ''; + }; + }; + nightTheme = mkOption { + type = types.bool; + default = false; + description = "Enable the Night UI Theme."; + }; + extraCurrency = mkOption { + type = with types; nullOr str; + default = null; + example = "USD"; + description = '' + Currency code (ISO 4217) of the extra currency used for displaying balances. + When set, this option enables online currency rate fetching. + Warning: Rate fetching requires outgoing clearnet connections, so option + {option}`tor.enforce` is automatically disabled. + ''; + }; + user = mkOption { + type = types.str; + default = "rtl"; + description = "The user as which to run RTL."; + }; + group = mkOption { + type = types.str; + default = cfg.user; + description = "The group as which to run RTL."; + }; + tor.enforce = nbLib.tor.enforce; + }; + + cfg = config.services.rtl; + nbLib = config.nix-bitcoin.lib; + nbPkgs = pkgs; + secretsDir = config.nix-bitcoin.secretsDir; + runePath = "${cfg.dataDir}/clightning-admin-rune"; + + inherit (nbLib) optionalAttr; + + node = { isLnd, index }: { + inherit index; + lnNode = "Node"; + lnImplementation = if isLnd then "LND" else "CLT"; + Authentication = { + ${optionalAttr (isLnd && lndLoopEnabled) "swapMacaroonPath"} = "${lightning-loop.dataDir}/${bitcoind.network}"; + ${optionalAttr (isLnd) "macaroonPath"} = "${cfg.dataDir}/macaroons"; + ${optionalAttr (!isLnd) "runePath"} = runePath; + }; + Settings = { + userPersona = "OPERATOR"; + themeMode = if cfg.nightTheme then "NIGHT" else "DAY"; + themeColor = "PURPLE"; + ${optionalAttr isLnd "channelBackupPath"} = "${cfg.dataDir}/backup/lnd"; + logLevel = "INFO"; + fiatConversion = cfg.extraCurrency != null; + ${optionalAttr (cfg.extraCurrency != null) "currencyUnit"} = cfg.extraCurrency; + ${optionalAttr (isLnd && lndLoopEnabled) "swapServerUrl"} = + "https://${nbLib.addressWithPort lightning-loop.restAddress lightning-loop.restPort}"; + lnServerUrl = "https://${ + if isLnd + then nbLib.addressWithPort lnd.restAddress lnd.restPort + else nbLib.addressWithPort clightning.plugins.clnrest.address clightning.plugins.clnrest.port + }"; + }; + }; + + nodes' = + optional cfg.nodes.clightning.enable + (recursiveUpdate (node { isLnd = false; index = 1; }) cfg.nodes.clightning.extraConfig) ++ + optional cfg.nodes.lnd.enable + (recursiveUpdate (node { isLnd = true; index = 2; }) cfg.nodes.lnd.extraConfig); + + nodes = if cfg.nodes.reverseOrder then reverseList nodes' else nodes'; + + rtlConfig = { + multiPass = "@multiPass@"; + host = cfg.address; + port = cfg.port; + SSO.rtlSSO = 0; + inherit nodes; + }; + + configFile = builtins.toFile "config" (builtins.toJSON rtlConfig); + + inherit (config.services) + bitcoind + lnd + clightning + lightning-loop; + + lndLoopEnabled = cfg.nodes.lnd.enable && cfg.nodes.lnd.loop; +in { + inherit options; + + config = mkIf cfg.enable { + assertions = [ + { assertion = cfg.nodes.clightning.enable || cfg.nodes.lnd.enable; + message = '' + RTL: At least one of `nodes.lnd.enable` or `nodes.clightning.enable` must be `true`. + ''; + } + ]; + + services.lnd.enable = mkIf cfg.nodes.lnd.enable true; + services.lightning-loop.enable = mkIf lndLoopEnabled true; + services.clightning = mkIf cfg.nodes.clightning.enable { + enable = true; + plugins.clnrest.enable = true; + }; + + systemd.tmpfiles.rules = [ + "d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -" + ]; + + services.rtl.tor.enforce = mkIf (cfg.extraCurrency != null) false; + + systemd.services.rtl = rec { + wantedBy = [ "multi-user.target" ]; + wants = optional cfg.nodes.clightning.enable "clightning.service" ++ + optional cfg.nodes.lnd.enable "lnd.service"; + after = wants ++ [ "nix-bitcoin-secrets.target" ]; + environment.RTL_CONFIG_PATH = cfg.dataDir; + environment.DB_DIRECTORY_PATH = cfg.dataDir; + serviceConfig = nbLib.defaultHardening // { + ExecStartPre = [ + (nbLib.script "rtl-setup-config" '' + <${configFile} sed "s|@multiPass@|$(cat ${secretsDir}/rtl-password)|" \ + > '${cfg.dataDir}/RTL-Config.json' + '') + ] + ++ optional cfg.nodes.lnd.enable + # The lnd admin macaroon is not readable by group `lnd`, so copy it + (nbLib.rootScript "rtl-copy-macaroon" '' + install --compare -m 640 -o ${cfg.user} -g ${cfg.group} -D ${lnd.networkDir}/admin.macaroon \ + '${cfg.dataDir}/macaroons/admin.macaroon' + '') + ++ optional cfg.nodes.clightning.enable + (nbLib.rootScript "rtl-create-clnrest-rune-file" '' + rune=$(cat '${clightning.networkDir}/admin-rune') + install --compare -m 640 -o ${cfg.user} -g ${cfg.group} <(printf 'LIGHTNING_RUNE="%s"\n' "$rune") '${runePath}' + ''); + ExecStart = "${pkgs.rtl}/bin/rtl"; + # Show "rtl" instead of "node" in the journal + SyslogIdentifier = "rtl"; + User = cfg.user; + Restart = "on-failure"; + RestartSec = "10s"; + ReadWritePaths = [ cfg.dataDir ]; + } // nbLib.allowedIPAddresses cfg.tor.enforce + // nbLib.nodejs; + }; + + users.users.${cfg.user} = { + isSystemUser = true; + group = cfg.group; + extraGroups = optional lndLoopEnabled lnd.group; + }; + users.groups.${cfg.group} = {}; + + nix-bitcoin.secrets.rtl-password.user = cfg.user; + nix-bitcoin.generateSecretsCmds.rtl = '' + makePasswordSecret rtl-password + ''; + }; +} diff --git a/modules/vendor/nix-bitcoin/secrets/secrets.nix b/modules/vendor/nix-bitcoin/secrets/secrets.nix new file mode 100644 index 0000000..944c92b --- /dev/null +++ b/modules/vendor/nix-bitcoin/secrets/secrets.nix @@ -0,0 +1,235 @@ +{ config, pkgs, lib, ... }: + +with lib; +let + options.nix-bitcoin = { + secretsDir = mkOption { + type = types.path; + default = "/etc/nix-bitcoin-secrets"; + description = "Directory to store secrets"; + }; + + setupSecrets = mkOption { + type = types.bool; + default = false; + description = '' + Set permissions for existing secrets in {option}`nix-bitcoin.secretsDir` + before services are started. + ''; + }; + + generateSecrets = mkOption { + type = types.bool; + default = false; + description = '' + Automatically generate all required secrets before services are started. + Note: Make sure to create a backup of the generated secrets. + ''; + }; + + generateSecretsCmds = mkOption { + type = types.attrsOf types.lines; + default = {}; + description = '' + Bash expressions for generating secrets. + ''; + }; + + # Currently, this is used only by ../deployment/nixops.nix + deployment.secretsDir = mkOption { + type = types.path; + description = '' + Directory of local secrets that are transferred to the nix-bitcoin node on deployment + ''; + }; + + secrets = mkOption { + default = {}; + type = with types; attrsOf (submodule ( + { config, ... }: { + options = { + user = mkOption { + type = str; + default = "root"; + }; + group = mkOption { + type = str; + default = config.user; + }; + permissions = mkOption { + type = str; + default = "440"; + }; + }; + } + )); + }; + + secretsSetupMethod = mkOption { + type = with types; nullOr str; + default = null; + }; + + generateSecretsScript = mkOption { + internal = true; + default = let + rpcauthSrc = pkgs.fetchurl { + url = "https://raw.githubusercontent.com/bitcoin/bitcoin/d6cde007db9d3e6ee93bd98a9bbfdce9bfa9b15b/share/rpcauth/rpcauth.py"; + sha256 = "189mpplam6yzizssrgiyv70c9899ggh8cac76j4n7v0xqzfip07n"; + }; + rpcauth = pkgs.writers.writeBash "rpcauth" '' + exec ${pkgs.python3}/bin/python ${rpcauthSrc} "$@" + ''; + # Writes secrets to PWD + in pkgs.writers.writeBash "generate-secrets" '' + set -euo pipefail + + export PATH=${lib.makeBinPath (with pkgs; [ coreutils gnugrep ])} + + makePasswordSecret() { + # Passwords have alphabet {a-z, A-Z, 0-9} and ~119 bits of entropy + [[ -e $1 ]] || ${pkgs.pwgen}/bin/pwgen -s 20 1 > "$1" + } + makeBitcoinRPCPassword() { + user=$1 + file=bitcoin-rpcpassword-$user + HMACfile=bitcoin-HMAC-$user + makePasswordSecret "$file" + if [[ $file -nt $HMACfile ]]; then + ${rpcauth} $user $(cat "$file") | grep rpcauth | cut -d ':' -f 2 > "$HMACfile" + fi + } + makeCert() { + name=$1 + # Add leading comma if not empty + extraAltNames=''${2:+,}''${2:-} + if [[ ! -e $name-key ]]; then + # Create new key and cert + doMakeCert "-newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes -keyout $name-key" + elif [[ ! -e $name-cert \ + || $(cat "$name-cert-alt-names" 2>/dev/null) != $extraAltNames ]]; then + # Create cert from existing key + doMakeCert "-key $name-key" + fi; + } + doMakeCert() { + # This fn uses global variables `name` and `extraAltNames` + keyOpts=$1 + ${pkgs.openssl}/bin/openssl req -x509 \ + -sha256 -days 3650 $keyOpts -out "$name-cert" \ + -subj "/CN=localhost/O=$name" \ + -addext "subjectAltName=DNS:localhost,IP:127.0.0.1$extraAltNames" + echo "$extraAltNames" > "$name-cert-alt-names" + } + + umask u=rw,go= + ${builtins.concatStringsSep "\n" (builtins.attrValues cfg.generateSecretsCmds)} + ''; + }; + }; + + cfg = config.nix-bitcoin; +in { + inherit options; + + config = { + assertions = [ + { assertion = cfg.secretsSetupMethod != null; + message = '' + No secrets setup method has been defined. + To fix this, choose one of the following: + + - Use one of the deployment methods in ${toString ./../deployment} + + - Set `nix-bitcoin.generateSecrets = true` to automatically generate secrets + + - Set `nix-bitcoin.secretsSetupMethod = "manual"` if you want to manually setup secrets + ''; + } + ]; + + # This target is active when secrets have been setup successfully. + systemd.targets.nix-bitcoin-secrets = mkIf (cfg.secretsSetupMethod != "manual") { + # This ensures that the secrets target is always activated when switching + # configurations. + # In this way `switch-to-configuration` is guaranteed to show an error + # when activating the secrets target fails on deployment. + wantedBy = [ "multi-user.target" ]; + }; + + nix-bitcoin.setupSecrets = mkIf cfg.generateSecrets true; + + nix-bitcoin.secretsSetupMethod = mkIf cfg.setupSecrets "setup-secrets"; + + # Operation of this service: + # - Set owner and permissions for all used secrets + # - Make all other secrets accessible to root only + # For all steps make sure that no secrets are copied to the nix store. + # + systemd.services.setup-secrets = mkIf cfg.setupSecrets { + requiredBy = [ "nix-bitcoin-secrets.target" ]; + before = [ "nix-bitcoin-secrets.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + # Use the same sort order for globbing and sorting as in Nix attrsets. + # Required for `comm` below. + export LC_COLLATE=C + + ${optionalString cfg.generateSecrets '' + mkdir -p "${cfg.secretsDir}" + cd "${cfg.secretsDir}" + chown root: . + chmod 0700 . + ${cfg.generateSecretsScript} + ''} + + setupSecret() { + file="$1" + user="$2" + group="$3" + permissions="$4" + if [[ ! -e $file ]]; then + echo "Error: Secret file '$file' is missing" + exit 1 + fi + chown "$user:$group" "$file" + chmod "$permissions" "$file" + processedFiles+=("$file") + } + + dir="${cfg.secretsDir}" + if [[ ! -e $dir ]]; then + echo "Error: Secrets dir '$dir' is missing" + exit 1 + fi + chown root: "$dir" + cd "$dir" + + processedFiles=() + ${ + concatStrings (mapAttrsToList (n: v: '' + setupSecret ${n} ${v.user} ${v.group} ${v.permissions} + '') cfg.secrets) + } + + # Make all other files accessible to root only + unprocessedFiles=$( + comm -23 <(shopt -s nullglob; printf '%s\n' *) <(printf '%s\n' "''${processedFiles[@]}") + ) + if [[ $unprocessedFiles ]]; then + IFS=$'\n' + # shellcheck disable=SC2086 + chown root: $unprocessedFiles + # shellcheck disable=SC2086 + chmod 0440 $unprocessedFiles + fi + + # Now make the secrets dir accessible to other users + chmod 0751 "$dir" + ''; + }; + }; +} diff --git a/modules/vendor/nix-bitcoin/secure-node.nix b/modules/vendor/nix-bitcoin/secure-node.nix new file mode 100644 index 0000000..00a8a77 --- /dev/null +++ b/modules/vendor/nix-bitcoin/secure-node.nix @@ -0,0 +1,62 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services; + nbLib = config.nix-bitcoin.lib; + operatorName = config.nix-bitcoin.operator.name; +in { + imports = [ + ../modules.nix + ./enable-tor.nix + ]; + + options = { + # Used by ../versioning.nix + nix-bitcoin.secure-node-preset-enabled = {}; + }; + + config = { + networking.firewall.enable = true; + + nix-bitcoin.security.dbusHideProcessInformation = true; + + # Use doas instead of sudo + security.doas.enable = true; + security.sudo.enable = false; + environment.shellAliases.sudo = "doas"; + + environment.systemPackages = with pkgs; [ + jq + ]; + + # Add a SSH onion service + services.tor.relay.onionServices.sshd = nbLib.mkOnionService { port = 22; }; + nix-bitcoin.onionAddresses.access.${operatorName} = [ "sshd" ]; + + services.bitcoind = { + enable = true; + listen = true; + dbCache = 1000; + }; + + services.liquidd = { + # Enable `validatepegin` to verify that a transaction sending BTC into + # Liquid exists on Bitcoin. Without it, a malicious liquid federation can + # make the node accept a sidechain that is not fully backed. + validatepegin = true; + listen = true; + }; + + nix-bitcoin.nodeinfo.enable = true; + + # vendored: backups removed — was services.backups.frequency = "daily" + + # operator + nix-bitcoin.operator.enable = true; + users.users.${operatorName} = { + openssh.authorizedKeys.keys = config.users.users.root.openssh.authorizedKeys.keys; + }; + }; +} diff --git a/modules/vendor/nix-bitcoin/security.nix b/modules/vendor/nix-bitcoin/security.nix new file mode 100644 index 0000000..338d82a --- /dev/null +++ b/modules/vendor/nix-bitcoin/security.nix @@ -0,0 +1,48 @@ +{ config, lib, pkgs, ... }: + +with lib; +{ + options = { + nix-bitcoin.security.dbusHideProcessInformation = mkOption { + type = types.bool; + default = false; + description = '' + Only allow users with group `proc` to retrieve systemd unit information like + cgroup paths (i.e. (sub)process command lines) via D-Bus. + + This mitigates a systemd security issue where (sub)process command lines can + be retrieved by services even when their access to /proc is restricted + (via ProtectProc). + + This option works by restricting the D-Bus method `GetUnitProcesses`, which + is also used internally by {command}`systemctl status`. + ''; + }; + }; + + config = mkIf config.nix-bitcoin.security.dbusHideProcessInformation { + users.groups.proc = {}; + nix-bitcoin.operator.groups = [ "proc" ]; # Enable operator access to systemd-status + + services.dbus.packages = lib.mkAfter [ # Apply at the end to override the default policy + (pkgs.writeTextDir "etc/dbus-1/system.d/dbus.conf" '' + + + + + + + + + '') + ]; + }; +} diff --git a/modules/vendor/nix-bitcoin/versioning.nix b/modules/vendor/nix-bitcoin/versioning.nix new file mode 100644 index 0000000..c34d4ce --- /dev/null +++ b/modules/vendor/nix-bitcoin/versioning.nix @@ -0,0 +1,12 @@ +{ config, lib, ... }: +with lib; +let + options.nix-bitcoin.configVersion = mkOption { + type = with types; nullOr str; + default = null; + description = "Vendored stub — no version migration needed."; + }; +in { + inherit options; + config = {}; +} diff --git a/pkgs/README.md b/pkgs/README.md new file mode 100644 index 0000000..4b51955 --- /dev/null +++ b/pkgs/README.md @@ -0,0 +1,2 @@ +These packages are provided by nixpkgs directly. No extra pinning needed. +Missing helpers (lndinit) are vendored below if needed. diff --git a/pkgs/nbxplorer.nix b/pkgs/nbxplorer.nix new file mode 100644 index 0000000..85f385c --- /dev/null +++ b/pkgs/nbxplorer.nix @@ -0,0 +1,18 @@ +# Fallback nbxplorer package if not in nixpkgs +# This is rarely needed — nixpkgs-unstable usually has it +{ lib, buildDotnetModule, fetchFromGitHub, dotnetCorePackages }: +buildDotnetModule rec { + pname = "nbxplorer"; + version = "2.5.22"; + src = fetchFromGitHub { + owner = "dgarage"; + repo = "NBXplorer"; + rev = "v${version}"; + sha256 = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; + }; + projectFile = "NBXplorer/NBXplorer.csproj"; + nugetDeps = ./nbxplorer-deps.nix; # not needed if using nixpkgs version + dotnet-sdk = dotnetCorePackages.sdk_8_0; + dotnet-runtime = dotnetCorePackages.aspnetcore_8_0; + meta = with lib; { description = "NBXplorer fallback"; license = licenses.mit; }; +} diff --git a/pkgs/sovran-overlay.nix b/pkgs/sovran-overlay.nix new file mode 100644 index 0000000..fdcff92 --- /dev/null +++ b/pkgs/sovran-overlay.nix @@ -0,0 +1,37 @@ +# Sovran overlay — provides packages not in nixpkgs or needing overrides +# All Bitcoin packages are now sourced from nixpkgs (unstable) directly. +# This overlay only fills gaps where nixpkgs is missing/broken. +final: prev: let + # lndinit is not in nixpkgs — vendor it from nix-bitcoin source + lndinit = prev.buildGoModule rec { + pname = "lndinit"; + version = "0.1.3-beta"; + src = prev.fetchFromGitHub { + owner = "lightninglabs"; + repo = pname; + rev = "v${version}"; + sha256 = "sha256-sO1DpbppCurxr9g9nUl9Vx82FJK1mTcUw3rY1Fm1wEU="; + }; + vendorHash = "sha256-El44BS5Bu0K/klMxkajciU/R6uqiXBMOiLN536QztbE="; + subPackages = [ "." ]; + meta = with prev.lib; { + description = "Wallet initializer utility for lnd (vendored from nix-bitcoin)"; + homepage = "https://github.com/lightninglabs/lndinit"; + license = licenses.mit; + }; + }; + + # netns-exec stub — netns isolation is stubbed, this is no-op + # If not needed, stub it to coreutils + netns-exec = prev.writeShellScriptBin "netns-exec" '' + exec "$@" + ''; + + # nbxplorer is needed by btcpayserver but was removed from nixpkgs in some versions + # Use nixpkgs version if available, otherwise build from nix-bitcoin pin + nbxplorer = prev.nbxplorer or (prev.callPackage ./nbxplorer.nix {} ); +in { + inherit lndinit netns-exec; + # Re-expose nbxplorer only if missing + nbxplorer = if prev ? nbxplorer then prev.nbxplorer else nbxplorer; +}