From 205829b91fad2cc233f2dd60999576f16437d762 Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Wed, 26 Aug 2020 21:15:31 +0200 Subject: [PATCH 1/9] bitcoind: remove whitespace --- modules/bitcoind.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/bitcoind.nix b/modules/bitcoind.nix index 440b6ed..8497da3 100644 --- a/modules/bitcoind.nix +++ b/modules/bitcoind.nix @@ -299,7 +299,7 @@ in { after = [ "network.target" "nix-bitcoin-secrets.target" ]; wantedBy = [ "multi-user.target" ]; preStart = '' - ${optionalString cfg.dataDirReadableByGroup "chmod -R g+rX '${cfg.dataDir}/blocks'"} + ${optionalString cfg.dataDirReadableByGroup "chmod -R g+rX '${cfg.dataDir}/blocks'"} cfgpre=$(cat ${configFile}; printf "rpcpassword="; cat "${secretsDir}/bitcoin-rpcpassword-privileged") cfg=$(echo "$cfgpre" | \ @@ -307,7 +307,7 @@ in { sed "s/bitcoin-HMAC-public/$(cat ${secretsDir}/bitcoin-HMAC-public)/g") confFile='${cfg.dataDir}/bitcoin.conf' if [[ ! -e $confFile || $cfg != $(cat $confFile) ]]; then - install -o '${cfg.user}' -g '${cfg.group}' -m 640 <(echo "$cfg") $confFile + install -o '${cfg.user}' -g '${cfg.group}' -m 640 <(echo "$cfg") $confFile fi ''; postStart = '' From 59434e79f09ba8e5fe352d621b05908635d181be Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Wed, 26 Aug 2020 21:15:32 +0200 Subject: [PATCH 2/9] bitcoind: simplify default rpc user name config --- modules/bitcoind.nix | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/modules/bitcoind.nix b/modules/bitcoind.nix index 8497da3..29d5f30 100644 --- a/modules/bitcoind.nix +++ b/modules/bitcoind.nix @@ -109,6 +109,7 @@ in { options = { name = mkOption { type = types.str; + default = name; example = "alice"; description = '' Username for JSON-RPC connections. @@ -131,9 +132,6 @@ in { ''; }; }; - config = { - name = mkDefault name; - }; })); description = '' RPC user information for JSON-RPC connections. From 876cfadf1af8b92b2c6cd4a290994698ff61c134 Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Wed, 26 Aug 2020 21:15:33 +0200 Subject: [PATCH 3/9] bitcoind: add rpc user option 'passwordHMACFromFile' This allows adding additional rpc users without the need for user-specific code in preStart. --- modules/bitcoind.nix | 35 ++++++++++++++++++++++----------- modules/presets/secure-node.nix | 6 ++---- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/modules/bitcoind.nix b/modules/bitcoind.nix index 29d5f30..6e785ad 100644 --- a/modules/bitcoind.nix +++ b/modules/bitcoind.nix @@ -30,12 +30,11 @@ let ${optionalString (cfg.rpcthreads != null) "rpcthreads=${toString cfg.rpcthreads}"} rpcport=${toString cfg.rpc.port} rpcwhitelistdefault=0 - ${concatMapStringsSep "\n" - (rpcUser: '' - rpcauth=${rpcUser.name}:${rpcUser.passwordHMAC} - ${optionalString (rpcUser.rpcwhitelist != []) "rpcwhitelist=${rpcUser.name}:${lib.strings.concatStringsSep "," rpcUser.rpcwhitelist}"} - '') - (attrValues cfg.rpc.users) + ${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 (rpcbind: "rpcbind=${rpcbind}\n") cfg.rpcbind} ${lib.concatMapStrings (rpcallowip: "rpcallowip=${rpcallowip}\n") cfg.rpcallowip} @@ -123,6 +122,11 @@ in { format $. ''; }; + passwordHMACFromFile = mkOption { + type = lib.types.bool; + internal = true; + default = false; + }; rpcwhitelist = mkOption { type = types.listOf types.str; default = []; @@ -296,13 +300,20 @@ in { requires = [ "nix-bitcoin-secrets.target" ]; after = [ "network.target" "nix-bitcoin-secrets.target" ]; wantedBy = [ "multi-user.target" ]; - preStart = '' + 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'"} - - cfgpre=$(cat ${configFile}; printf "rpcpassword="; cat "${secretsDir}/bitcoin-rpcpassword-privileged") - cfg=$(echo "$cfgpre" | \ - sed "s/bitcoin-HMAC-privileged/$(cat ${secretsDir}/bitcoin-HMAC-privileged)/g" | \ - sed "s/bitcoin-HMAC-public/$(cat ${secretsDir}/bitcoin-HMAC-public)/g") + cfg=$( + cat ${configFile}; + ${extraRpcauth} + printf "rpcpassword="; cat "${secretsDir}/bitcoin-rpcpassword-privileged"; + ) confFile='${cfg.dataDir}/bitcoin.conf' if [[ ! -e $confFile || $cfg != $(cat $confFile) ]]; then install -o '${cfg.user}' -g '${cfg.group}' -m 640 <(echo "$cfg") $confFile diff --git a/modules/presets/secure-node.nix b/modules/presets/secure-node.nix index d49307f..8194b3e 100644 --- a/modules/presets/secure-node.nix +++ b/modules/presets/secure-node.nix @@ -77,13 +77,11 @@ in { rpcthreads = 16; rpc.users.privileged = { name = "bitcoinrpc"; - # Placeholder to be sed'd out by bitcoind preStart - passwordHMAC = "bitcoin-HMAC-privileged"; + passwordHMACFromFile = true; }; rpc.users.public = { name = "publicrpc"; - # Placeholder to be sed'd out by bitcoind preStart - passwordHMAC = "bitcoin-HMAC-public"; + passwordHMACFromFile = true; rpcwhitelist = [ "echo" "getinfo" From 4790c601a1a7b2e73e5e84344ae0bbc3df279e30 Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Wed, 26 Aug 2020 21:15:34 +0200 Subject: [PATCH 4/9] bitcoind: move rpc user config to bitcoind This enables modules-only usage. The privileged user is needed by bitcoind (cli), the public user is needed by other services. --- modules/bitcoind-rpc-public-whitelist.nix | 61 ++++++++++++++++++++ modules/bitcoind.nix | 21 +++++-- modules/presets/secure-node.nix | 68 ----------------------- 3 files changed, 78 insertions(+), 72 deletions(-) create mode 100644 modules/bitcoind-rpc-public-whitelist.nix diff --git a/modules/bitcoind-rpc-public-whitelist.nix b/modules/bitcoind-rpc-public-whitelist.nix new file mode 100644 index 0000000..dada244 --- /dev/null +++ b/modules/bitcoind-rpc-public-whitelist.nix @@ -0,0 +1,61 @@ +# RPC calls that are safe for public use +[ + "echo" + "getinfo" + # Blockchain + "getbestblockhash" + "getblock" + "getblockchaininfo" + "getblockcount" + "getblockfilter" + "getblockhash" + "getblockheader" + "getblockstats" + "getchaintips" + "getchaintxstats" + "getdifficulty" + "getmempoolancestors" + "getmempooldescendants" + "getmempoolentry" + "getmempoolinfo" + "getrawmempool" + "gettxout" + "gettxoutproof" + "gettxoutsetinfo" + "scantxoutset" + "verifytxoutproof" + # Mining + "getblocktemplate" + "getmininginfo" + "getnetworkhashps" + # Network + "getnetworkinfo" + # Rawtransactions + "analyzepsbt" + "combinepsbt" + "combinerawtransaction" + "converttopsbt" + "createpsbt" + "createrawtransaction" + "decodepsbt" + "decoderawtransaction" + "decodescript" + "finalizepsbt" + "fundrawtransaction" + "getrawtransaction" + "joinpsbts" + "sendrawtransaction" + "signrawtransactionwithkey" + "testmempoolaccept" + "utxoupdatepsbt" + # Util + "createmultisig" + "deriveaddresses" + "estimatesmartfee" + "getdescriptorinfo" + "signmessagewithprivkey" + "validateaddress" + "verifymessage" + # Zmq + "getzmqnotifications" +] diff --git a/modules/bitcoind.nix b/modules/bitcoind.nix index 6e785ad..d62ca35 100644 --- a/modules/bitcoind.nix +++ b/modules/bitcoind.nix @@ -285,10 +285,23 @@ in { config = mkIf cfg.enable { environment.systemPackages = [ cfg.package (hiPrio cfg.cli) ]; - services.bitcoind = mkIf cfg.dataDirReadableByGroup { - disablewallet = true; - sysperms = true; - }; + services.bitcoind = mkMerge [ + (mkIf cfg.dataDirReadableByGroup { + disablewallet = true; + sysperms = true; + }) + { + rpc.users.privileged = { + name = "bitcoinrpc"; + passwordHMACFromFile = true; + }; + rpc.users.public = { + name = "publicrpc"; + passwordHMACFromFile = true; + rpcwhitelist = import ./bitcoind-rpc-public-whitelist.nix; + }; + } + ]; systemd.tmpfiles.rules = [ "d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -" diff --git a/modules/presets/secure-node.nix b/modules/presets/secure-node.nix index 8194b3e..1447532 100644 --- a/modules/presets/secure-node.nix +++ b/modules/presets/secure-node.nix @@ -75,74 +75,6 @@ in { # higher rpcthread count due to reports that lightning implementations fail # under high bitcoind rpc load rpcthreads = 16; - rpc.users.privileged = { - name = "bitcoinrpc"; - passwordHMACFromFile = true; - }; - rpc.users.public = { - name = "publicrpc"; - passwordHMACFromFile = true; - rpcwhitelist = [ - "echo" - "getinfo" - # Blockchain - "getbestblockhash" - "getblock" - "getblockchaininfo" - "getblockcount" - "getblockfilter" - "getblockhash" - "getblockheader" - "getblockstats" - "getchaintips" - "getchaintxstats" - "getdifficulty" - "getmempoolancestors" - "getmempooldescendants" - "getmempoolentry" - "getmempoolinfo" - "getrawmempool" - "gettxout" - "gettxoutproof" - "gettxoutsetinfo" - "scantxoutset" - "verifytxoutproof" - # Mining - "getblocktemplate" - "getmininginfo" - "getnetworkhashps" - # Network - "getnetworkinfo" - # Rawtransactions - "analyzepsbt" - "combinepsbt" - "combinerawtransaction" - "converttopsbt" - "createpsbt" - "createrawtransaction" - "decodepsbt" - "decoderawtransaction" - "decodescript" - "finalizepsbt" - "fundrawtransaction" - "getrawtransaction" - "joinpsbts" - "sendrawtransaction" - "signrawtransactionwithkey" - "testmempoolaccept" - "utxoupdatepsbt" - # Util - "createmultisig" - "deriveaddresses" - "estimatesmartfee" - "getdescriptorinfo" - "signmessagewithprivkey" - "validateaddress" - "verifymessage" - # Zmq - "getzmqnotifications" - ]; - }; }; services.tor.hiddenServices.bitcoind = mkHiddenService { port = cfg.bitcoind.port; toHost = cfg.bitcoind.bind; }; From 1408403decedd3e5b8772dedc2f35af79c817c7a Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Wed, 26 Aug 2020 21:15:35 +0200 Subject: [PATCH 5/9] bitcoind: clarify how bitcoin-cli RPC access is enabled It's not immediately clear why rpcuser/rpcpassword are needed in addition to the rpcauth config entries. --- modules/bitcoind.nix | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/modules/bitcoind.nix b/modules/bitcoind.nix index d62ca35..c7bcd14 100644 --- a/modules/bitcoind.nix +++ b/modules/bitcoind.nix @@ -38,8 +38,6 @@ let } ${lib.concatMapStrings (rpcbind: "rpcbind=${rpcbind}\n") cfg.rpcbind} ${lib.concatMapStrings (rpcallowip: "rpcallowip=${rpcallowip}\n") cfg.rpcallowip} - # Credentials for bitcoin-cli - rpcuser=${cfg.rpc.users.privileged.name} # Wallet options ${optionalString (cfg.addresstype != null) "addresstype=${cfg.addresstype}"} @@ -325,7 +323,8 @@ in { cfg=$( cat ${configFile}; ${extraRpcauth} - printf "rpcpassword="; cat "${secretsDir}/bitcoin-rpcpassword-privileged"; + ${/* Enable bitcoin-cli for group 'bitcoin' */ ""} + printf "rpcuser=${cfg.rpc.users.privileged.name}\nrpcpassword="; cat "${secretsDir}/bitcoin-rpcpassword-privileged"; ) confFile='${cfg.dataDir}/bitcoin.conf' if [[ ! -e $confFile || $cfg != $(cat $confFile) ]]; then From 9d610991be3b7b1d2f5292a80d76ad78645ac441 Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Wed, 26 Aug 2020 21:15:36 +0200 Subject: [PATCH 6/9] bitcoind: remove custom rpc user names Simpler. We've just removed option 'bitcoind.rpcuser', so we can also remove the old name 'bitcoinrpc'. --- modules/bitcoind.nix | 2 -- test/base.py | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/modules/bitcoind.nix b/modules/bitcoind.nix index c7bcd14..5862190 100644 --- a/modules/bitcoind.nix +++ b/modules/bitcoind.nix @@ -290,11 +290,9 @@ in { }) { rpc.users.privileged = { - name = "bitcoinrpc"; passwordHMACFromFile = true; }; rpc.users.public = { - name = "publicrpc"; passwordHMACFromFile = true; rpcwhitelist = import ./bitcoind-rpc-public-whitelist.nix; }; diff --git a/test/base.py b/test/base.py index 7df7108..c35d380 100644 --- a/test/base.py +++ b/test/base.py @@ -50,10 +50,10 @@ def run_tests(extra_tests): machine.wait_until_succeeds("su operator -c 'bitcoin-cli help'") # Restating rpcuser & rpcpassword overrides privileged credentials machine.fail( - "bitcoin-cli -rpcuser=publicrpc -rpcpassword=$(cat /secrets/bitcoin-rpcpassword-public) help" + "bitcoin-cli -rpcuser=public -rpcpassword=$(cat /secrets/bitcoin-rpcpassword-public) help" ) machine.wait_until_succeeds( - log_has_string("bitcoind", "RPC User publicrpc not allowed to call method help") + log_has_string("bitcoind", "RPC User public not allowed to call method help") ) assert_running("electrs") From 4d6127bb7684291fe52647a0e2c838af60ad63f7 Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Thu, 27 Aug 2020 12:17:39 +0200 Subject: [PATCH 7/9] bitcoind: clarify RPC whitelist test - Remove redundant comment - Test with obviously unsafe RPC call 'stop' - No need to test privileged user who has no whitelist --- test/base.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test/base.py b/test/base.py index c35d380..0e72de3 100644 --- a/test/base.py +++ b/test/base.py @@ -46,14 +46,12 @@ def run_tests(extra_tests): assert_running("bitcoind") machine.wait_until_succeeds("bitcoin-cli getnetworkinfo") assert_matches("su operator -c 'bitcoin-cli getnetworkinfo' | jq", '"version"') - # Test RPC Whitelist - machine.wait_until_succeeds("su operator -c 'bitcoin-cli help'") - # Restating rpcuser & rpcpassword overrides privileged credentials + # RPC access for user 'public' should be restricted machine.fail( - "bitcoin-cli -rpcuser=public -rpcpassword=$(cat /secrets/bitcoin-rpcpassword-public) help" + "bitcoin-cli -rpcuser=public -rpcpassword=$(cat /secrets/bitcoin-rpcpassword-public) stop" ) machine.wait_until_succeeds( - log_has_string("bitcoind", "RPC User public not allowed to call method help") + log_has_string("bitcoind", "RPC User public not allowed to call method stop") ) assert_running("electrs") From ca18ffb90ab2011070cbbfc0f25a67516916e33a Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Thu, 27 Aug 2020 12:17:40 +0200 Subject: [PATCH 8/9] generate-secrets: fetch rpcauth.py from github No need to vendor this. --- pkgs/generate-secrets/default.nix | 10 ++++-- pkgs/generate-secrets/rpcauth/rpcauth.py | 46 ------------------------ 2 files changed, 8 insertions(+), 48 deletions(-) delete mode 100644 pkgs/generate-secrets/rpcauth/rpcauth.py diff --git a/pkgs/generate-secrets/default.nix b/pkgs/generate-secrets/default.nix index 10c20f2..00a15cf 100644 --- a/pkgs/generate-secrets/default.nix +++ b/pkgs/generate-secrets/default.nix @@ -1,9 +1,15 @@ { pkgs }: with pkgs; let - rpcauth = pkgs.writeScriptBin "rpcauth" (builtins.readFile ./rpcauth/rpcauth.py); + rpcauthSrc = builtins.fetchurl { + url = "https://raw.githubusercontent.com/bitcoin/bitcoin/d6cde007db9d3e6ee93bd98a9bbfdce9bfa9b15b/share/rpcauth/rpcauth.py"; + sha256 = "189mpplam6yzizssrgiyv70c9899ggh8cac76j4n7v0xqzfip07n"; + }; + rpcauth = pkgs.writeScriptBin "rpcauth" '' + exec ${pkgs.python35}/bin/python ${rpcauthSrc} "$@" + ''; in writeScript "generate-secrets" '' - export PATH=${lib.makeBinPath [ coreutils apg openssl gnugrep rpcauth python35 ]} + export PATH=${lib.makeBinPath [ coreutils apg openssl gnugrep rpcauth ]} . ${./generate-secrets.sh} ${./openssl.cnf} '' diff --git a/pkgs/generate-secrets/rpcauth/rpcauth.py b/pkgs/generate-secrets/rpcauth/rpcauth.py deleted file mode 100644 index b14c801..0000000 --- a/pkgs/generate-secrets/rpcauth/rpcauth.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) 2015-2018 The Bitcoin Core developers -# Distributed under the MIT software license, see the accompanying -# file COPYING or http://www.opensource.org/licenses/mit-license.php. - -from argparse import ArgumentParser -from base64 import urlsafe_b64encode -from binascii import hexlify -from getpass import getpass -from os import urandom - -import hmac - -def generate_salt(size): - """Create size byte hex salt""" - return hexlify(urandom(size)).decode() - -def generate_password(): - """Create 32 byte b64 password""" - return urlsafe_b64encode(urandom(32)).decode('utf-8') - -def password_to_hmac(salt, password): - m = hmac.new(bytearray(salt, 'utf-8'), bytearray(password, 'utf-8'), 'SHA256') - return m.hexdigest() - -def main(): - parser = ArgumentParser(description='Create login credentials for a JSON-RPC user') - parser.add_argument('username', help='the username for authentication') - parser.add_argument('password', help='leave empty to generate a random password or specify "-" to prompt for password', nargs='?') - args = parser.parse_args() - - if not args.password: - args.password = generate_password() - elif args.password == '-': - args.password = getpass() - - # Create 16 byte hex salt - salt = generate_salt(16) - password_hmac = password_to_hmac(salt, args.password) - - print('String to be appended to bitcoin.conf:') - print('rpcauth={0}:{1}${2}'.format(args.username, salt, password_hmac)) - print('Your password:\n{0}'.format(args.password)) - -if __name__ == '__main__': - main() From 9b6a3ec8359953bd4e8d51df55af55593568c113 Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Thu, 27 Aug 2020 12:17:41 +0200 Subject: [PATCH 9/9] generate-secrets: extract fn 'makeHMAC' --- pkgs/generate-secrets/generate-secrets.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pkgs/generate-secrets/generate-secrets.sh b/pkgs/generate-secrets/generate-secrets.sh index ef3611b..5a60cbe 100755 --- a/pkgs/generate-secrets/generate-secrets.sh +++ b/pkgs/generate-secrets/generate-secrets.sh @@ -5,6 +5,10 @@ opensslConf=${1:-openssl.cnf} makePasswordSecret() { [[ -e $1 ]] || apg -m 20 -x 20 -M Ncl -n 1 > "$1" } +makeHMAC() { + user=$1 + rpcauth $user $(cat bitcoin-rpcpassword-$user) | grep rpcauth | cut -d ':' -f 2 > bitcoin-HMAC-$user +} makePasswordSecret bitcoin-rpcpassword-privileged makePasswordSecret bitcoin-rpcpassword-public @@ -14,8 +18,8 @@ makePasswordSecret lightning-charge-token makePasswordSecret spark-wallet-password makePasswordSecret backup-encryption-password -[[ -e bitcoin-HMAC-privileged ]] || rpcauth privileged $(cat bitcoin-rpcpassword-privileged) | grep rpcauth | cut -d ':' -f 2 > bitcoin-HMAC-privileged -[[ -e bitcoin-HMAC-public ]] || rpcauth public $(cat bitcoin-rpcpassword-public) | grep rpcauth | cut -d ':' -f 2 > bitcoin-HMAC-public +[[ -e bitcoin-HMAC-privileged ]] || makeHMAC privileged +[[ -e bitcoin-HMAC-public ]] || makeHMAC public [[ -e lightning-charge-env ]] || echo "API_TOKEN=$(cat lightning-charge-token)" > lightning-charge-env [[ -e nanopos-env ]] || echo "CHARGE_TOKEN=$(cat lightning-charge-token)" > nanopos-env [[ -e spark-wallet-login ]] || echo "login=spark-wallet:$(cat spark-wallet-password)" > spark-wallet-login