bfe8ac972c
ec4a4dbe41
btcpayserver: fix whitelist security issue (Erik Arvstedt)df2070b44a
bitcoind: add separate p2p socket for tor connections (Erik Arvstedt) Pull request description: ACKs for top commit: jonasnick: ACKec4a4dbe41
Tree-SHA512: 457bfb5806dca65507261c1868ca89c86a39f63bd10833b7531fd74dd779816083270c8ccc95ad08a5306e9b31c440904e3cba35464d47c0d87418d0be3e732d
444 lines
15 KiB
Nix
444 lines
15 KiB
Nix
{ 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 = 8333;
|
|
description = "Port to listen for peer connections.";
|
|
};
|
|
onionPort = mkOption {
|
|
type = types.nullOr types.port;
|
|
default = null;
|
|
description = ''
|
|
Port to listen for Tor peer connections.
|
|
If set, inbound connections to this port are tagged as onion peers.
|
|
'';
|
|
};
|
|
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 = config.nix-bitcoin.pkgs.bitcoind;
|
|
description = "The package providing bitcoin binaries.";
|
|
};
|
|
extraConfig = mkOption {
|
|
type = types.lines;
|
|
default = "";
|
|
example = ''
|
|
par=16
|
|
logips=1
|
|
'';
|
|
description = "Extra lines appended to <filename>bitcoin.conf</filename>.";
|
|
};
|
|
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 = 8332;
|
|
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 = {};
|
|
example = {
|
|
alice.passwordHMAC = "f7efda5c189b999524f151318c0c86$d5b51b3beffbc02b724e5d095828e0bc8b2456e9ac8757ae3211a5d9b16a22ae";
|
|
bob.passwordHMAC = "b2dd077cb54591a2f3139e69a897ac$4e71f08d48b4347cf8eff3815c0e25ae2e9a4340474079f55705f40574f4ec99";
|
|
};
|
|
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 <SALT-HEX>$<HMAC-HEX>.
|
|
'';
|
|
};
|
|
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.
|
|
'';
|
|
};
|
|
};
|
|
}));
|
|
description = ''
|
|
RPC user information for JSON-RPC connections.
|
|
'';
|
|
};
|
|
};
|
|
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.enforceTor 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.
|
|
'';
|
|
};
|
|
listen = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = "Accept incoming connections.";
|
|
};
|
|
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 (types.ints.between 4 16384);
|
|
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.
|
|
'';
|
|
};
|
|
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.writeScriptBin "bitcoin-cli" ''
|
|
exec ${cfg.package}/bin/bitcoin-cli -datadir='${cfg.dataDir}' "$@"
|
|
'';
|
|
description = "Binary to connect with the bitcoind instance.";
|
|
};
|
|
enforceTor = nbLib.enforceTor;
|
|
};
|
|
};
|
|
|
|
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
|
|
|
|
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.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
|
|
${optionalString cfg.listen
|
|
"bind=${cfg.address}:${toString cfg.port}"}
|
|
${optionalString (cfg.listen && cfg.onionPort != null)
|
|
"bind=${cfg.address}:${toString cfg.onionPort}=onion"}
|
|
${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"}
|
|
|
|
listen=${if cfg.listen then "1" else "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}:${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);
|
|
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} - -"
|
|
"d '${cfg.dataDir}/blocks' 0770 ${cfg.user} ${cfg.group} - -"
|
|
];
|
|
|
|
systemd.services.bitcoind = {
|
|
# Use `wants` instead of `requires` 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.
|
|
wants = [ "nix-bitcoin-secrets.target" ];
|
|
after = [ "network.target" "nix-bitcoin-secrets.target" ];
|
|
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 "chmod -R g+rX '${cfg.dataDir}/blocks'"}
|
|
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'
|
|
'';
|
|
serviceConfig = nbLib.defaultHardening // {
|
|
Type = "notify";
|
|
NotifyAccess = "all";
|
|
User = cfg.user;
|
|
Group = cfg.group;
|
|
TimeoutStartSec = "5min";
|
|
TimeoutStopSec = "10min";
|
|
ExecStart = "${cfg.package}/bin/bitcoind -datadir='${cfg.dataDir}'";
|
|
Restart = "on-failure";
|
|
UMask = mkIf cfg.dataDirReadableByGroup "0027";
|
|
ReadWritePaths = cfg.dataDir;
|
|
} // nbLib.allowedIPAddresses cfg.enforceTor
|
|
// optionalAttrs zmqServerEnabled nbLib.allowNetlink;
|
|
};
|
|
|
|
# Use this to update the banlist:
|
|
# wget https://people.xiph.org/~greg/banlist.cli.txt
|
|
systemd.services.bitcoind-import-banlist = {
|
|
description = "Bitcoin daemon banlist importer";
|
|
wantedBy = [ "bitcoind.service" ];
|
|
bindsTo = [ "bitcoind.service" ];
|
|
after = [ "bitcoind.service" ];
|
|
script = ''
|
|
cd ${cfg.cli}/bin
|
|
echo "Importing node banlist..."
|
|
cat ${./banlist.cli.txt} | while read line; do
|
|
if ! err=$(eval "$line" 2>&1) && [[ $err != *already\ banned* ]]; then
|
|
# unexpected error
|
|
echo "$err"
|
|
exit 1
|
|
fi
|
|
done
|
|
'';
|
|
serviceConfig = nbLib.defaultHardening // {
|
|
User = cfg.user;
|
|
Group = cfg.group;
|
|
ReadWritePaths = cfg.dataDir;
|
|
} // nbLib.allowLocalIPAddresses;
|
|
};
|
|
|
|
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
|
|
'';
|
|
};
|
|
}
|