Merge #202: RPC Whitelist

5086fc3234 bitcoin: drive-by prune fix (nixbitcoin)
21c0fb440d rpcwhitelist: add feature test (nixbitcoin)
1bf45a9547 bitcoind: add rpcwhitelist feature (nixbitcoin)
5a978a2836 bitcoind: switch from rpcpassword to rpcauth (nixbitcoin)

Pull request description:

ACKs for top commit:
  jonasnick:
    ACK 5086fc3234

Tree-SHA512: f456f3409b3bc22dc9ad1296fa00f7e8a442b4072cd4deda067bf2f951eb7d4302283b816ebf769abaa7017e26b19b734f66604cd435d99b810ce535735f7c08
This commit is contained in:
Jonas Nick 2020-07-28 17:37:52 +00:00
commit 9e453bab86
No known key found for this signature in database
GPG Key ID: 4861DBF262123605
11 changed files with 176 additions and 26 deletions

View File

@ -5,6 +5,7 @@ with lib;
let let
cfg = config.services.bitcoind; cfg = config.services.bitcoind;
inherit (config) nix-bitcoin-services; inherit (config) nix-bitcoin-services;
secretsDir = config.nix-bitcoin.secretsDir;
configFile = pkgs.writeText "bitcoin.conf" '' configFile = pkgs.writeText "bitcoin.conf" ''
# We're already logging via journald # We're already logging via journald
@ -12,7 +13,7 @@ let
${optionalString cfg.testnet "testnet=1"} ${optionalString cfg.testnet "testnet=1"}
${optionalString (cfg.dbCache != null) "dbcache=${toString cfg.dbCache}"} ${optionalString (cfg.dbCache != null) "dbcache=${toString cfg.dbCache}"}
"prune=${toString cfg.prune} prune=${toString cfg.prune}
${optionalString (cfg.sysperms != null) "sysperms=${if cfg.sysperms then "1" else "0"}"} ${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.disablewallet != null) "disablewallet=${if cfg.disablewallet then "1" else "0"}"}
${optionalString (cfg.assumevalid != null) "assumevalid=${cfg.assumevalid}"} ${optionalString (cfg.assumevalid != null) "assumevalid=${cfg.assumevalid}"}
@ -27,14 +28,18 @@ let
# RPC server options # RPC server options
rpcport=${toString cfg.rpc.port} rpcport=${toString cfg.rpc.port}
rpcwhitelistdefault=0
${concatMapStringsSep "\n" ${concatMapStringsSep "\n"
(rpcUser: "rpcauth=${rpcUser.name}:${rpcUser.passwordHMAC}") (rpcUser: ''
rpcauth=${rpcUser.name}:${rpcUser.passwordHMAC}
${optionalString (rpcUser.rpcwhitelist != []) "rpcwhitelist=${rpcUser.name}:${lib.strings.concatStringsSep "," rpcUser.rpcwhitelist}"}
'')
(attrValues cfg.rpc.users) (attrValues cfg.rpc.users)
} }
${lib.concatMapStrings (rpcbind: "rpcbind=${rpcbind}\n") cfg.rpcbind} ${lib.concatMapStrings (rpcbind: "rpcbind=${rpcbind}\n") cfg.rpcbind}
${lib.concatMapStrings (rpcallowip: "rpcallowip=${rpcallowip}\n") cfg.rpcallowip} ${lib.concatMapStrings (rpcallowip: "rpcallowip=${rpcallowip}\n") cfg.rpcallowip}
${optionalString (cfg.rpcuser != null) "rpcuser=${cfg.rpcuser}"} # Credentials for bitcoin-cli
${optionalString (cfg.rpcpassword != null) "rpcpassword=${cfg.rpcpassword}"} rpcuser=${cfg.rpc.users.privileged.name}
# Wallet options # Wallet options
${optionalString (cfg.addresstype != null) "addresstype=${cfg.addresstype}"} ${optionalString (cfg.addresstype != null) "addresstype=${cfg.addresstype}"}
@ -110,13 +115,21 @@ in {
''; '';
}; };
passwordHMAC = mkOption { passwordHMAC = mkOption {
type = with types; uniq (strMatching "[0-9a-f]+\\$[0-9a-f]{64}"); type = types.str;
example = "f7efda5c189b999524f151318c0c86$d5b51b3beffbc02b724e5d095828e0bc8b2456e9ac8757ae3211a5d9b16a22ae"; example = "f7efda5c189b999524f151318c0c86$d5b51b3beffbc02b724e5d095828e0bc8b2456e9ac8757ae3211a5d9b16a22ae";
description = '' description = ''
Password HMAC-SHA-256 for JSON-RPC connections. Must be a string of the Password HMAC-SHA-256 for JSON-RPC connections. Must be a string of the
format <SALT-HEX>$<HMAC-HEX>. format <SALT-HEX>$<HMAC-HEX>.
''; '';
}; };
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.
'';
};
}; };
config = { config = {
name = mkDefault name; name = mkDefault name;
@ -141,16 +154,6 @@ in {
Allow JSON-RPC connections from specified source. Allow JSON-RPC connections from specified source.
''; '';
}; };
rpcuser = mkOption {
type = types.nullOr types.str;
default = "bitcoinrpc";
description = "Username for JSON-RPC connections";
};
rpcpassword = mkOption {
type = types.nullOr types.str;
default = null;
description = "Password for JSON-RPC connections";
};
testnet = mkOption { testnet = mkOption {
type = types.bool; type = types.bool;
default = false; default = false;
@ -297,7 +300,10 @@ in {
preStart = '' preStart = ''
${optionalString cfg.dataDirReadableByGroup "chmod -R g+rX '${cfg.dataDir}/blocks'"} ${optionalString cfg.dataDirReadableByGroup "chmod -R g+rX '${cfg.dataDir}/blocks'"}
cfg=$(cat ${configFile}; printf "rpcpassword="; cat "${config.nix-bitcoin.secretsDir}/bitcoin-rpcpassword") 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")
confFile='${cfg.dataDir}/bitcoin.conf' confFile='${cfg.dataDir}/bitcoin.conf'
if [[ ! -e $confFile || $cfg != $(cat $confFile) ]]; then 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
@ -355,9 +361,13 @@ in {
users.groups.${cfg.group} = {}; users.groups.${cfg.group} = {};
users.groups.bitcoinrpc = {}; users.groups.bitcoinrpc = {};
nix-bitcoin.secrets.bitcoin-rpcpassword = { nix-bitcoin.secrets.bitcoin-rpcpassword-privileged.user = "bitcoin";
nix-bitcoin.secrets.bitcoin-rpcpassword-public = {
user = "bitcoin"; user = "bitcoin";
group = "bitcoinrpc"; group = "bitcoinrpc";
}; };
nix-bitcoin.secrets.bitcoin-HMAC-privileged.user = "bitcoin";
nix-bitcoin.secrets.bitcoin-HMAC-public.user = "bitcoin";
}; };
} }

View File

@ -13,7 +13,7 @@ let
always-use-proxy=${if cfg.always-use-proxy then "true" else "false"} always-use-proxy=${if cfg.always-use-proxy then "true" else "false"}
${optionalString (cfg.bind-addr != null) "bind-addr=${cfg.bind-addr}"} ${optionalString (cfg.bind-addr != null) "bind-addr=${cfg.bind-addr}"}
${optionalString (cfg.bitcoin-rpcconnect != null) "bitcoin-rpcconnect=${cfg.bitcoin-rpcconnect}"} ${optionalString (cfg.bitcoin-rpcconnect != null) "bitcoin-rpcconnect=${cfg.bitcoin-rpcconnect}"}
bitcoin-rpcuser=${config.services.bitcoind.rpcuser} bitcoin-rpcuser=${config.services.bitcoind.rpc.users.public.name}
rpc-file-mode=0660 rpc-file-mode=0660
''; '';
in { in {
@ -112,7 +112,7 @@ in {
# The RPC socket has to be removed otherwise we might have stale sockets # The RPC socket has to be removed otherwise we might have stale sockets
rm -f ${cfg.dataDir}/bitcoin/lightning-rpc rm -f ${cfg.dataDir}/bitcoin/lightning-rpc
chmod 600 ${cfg.dataDir}/config chmod 600 ${cfg.dataDir}/config
echo "bitcoin-rpcpassword=$(cat ${config.nix-bitcoin.secretsDir}/bitcoin-rpcpassword)" >> '${cfg.dataDir}/config' echo "bitcoin-rpcpassword=$(cat ${config.nix-bitcoin.secretsDir}/bitcoin-rpcpassword-public)" >> '${cfg.dataDir}/config'
${optionalString cfg.announce-tor "echo announce-addr=$(cat /var/lib/onion-chef/clightning/clightning) >> '${cfg.dataDir}/config'"} ${optionalString cfg.announce-tor "echo announce-addr=$(cat /var/lib/onion-chef/clightning/clightning) >> '${cfg.dataDir}/config'"}
''; '';
serviceConfig = nix-bitcoin-services.defaultHardening // { serviceConfig = nix-bitcoin-services.defaultHardening // {

View File

@ -74,7 +74,7 @@ in {
requires = [ "bitcoind.service" ]; requires = [ "bitcoind.service" ];
after = [ "bitcoind.service" ]; after = [ "bitcoind.service" ];
preStart = '' preStart = ''
echo "cookie = \"${config.services.bitcoind.rpcuser}:$(cat ${secretsDir}/bitcoin-rpcpassword)\"" \ echo "cookie = \"${config.services.bitcoind.rpc.users.public.name}:$(cat ${secretsDir}/bitcoin-rpcpassword-public)\"" \
> electrs.toml > electrs.toml
''; '';
serviceConfig = nix-bitcoin-services.defaultHardening // { serviceConfig = nix-bitcoin-services.defaultHardening // {

View File

@ -246,7 +246,7 @@ in {
chmod 640 '${cfg.dataDir}/elements.conf' chmod 640 '${cfg.dataDir}/elements.conf'
chown -R '${cfg.user}:${cfg.group}' '${cfg.dataDir}' chown -R '${cfg.user}:${cfg.group}' '${cfg.dataDir}'
echo "rpcpassword=$(cat ${secretsDir}/liquid-rpcpassword)" >> '${cfg.dataDir}/elements.conf' echo "rpcpassword=$(cat ${secretsDir}/liquid-rpcpassword)" >> '${cfg.dataDir}/elements.conf'
echo "mainchainrpcpassword=$(cat ${secretsDir}/bitcoin-rpcpassword)" >> '${cfg.dataDir}/elements.conf' echo "mainchainrpcpassword=$(cat ${secretsDir}/bitcoin-rpcpassword-public)" >> '${cfg.dataDir}/elements.conf'
''; '';
serviceConfig = nix-bitcoin-services.defaultHardening // { serviceConfig = nix-bitcoin-services.defaultHardening // {
Type = "simple"; Type = "simple";

View File

@ -25,7 +25,7 @@ let
${optionalString (cfg.tor-socks != null) "tor.socks=${cfg.tor-socks}"} ${optionalString (cfg.tor-socks != null) "tor.socks=${cfg.tor-socks}"}
bitcoind.rpchost=${cfg.bitcoind-host} bitcoind.rpchost=${cfg.bitcoind-host}
bitcoind.rpcuser=${config.services.bitcoind.rpcuser} bitcoind.rpcuser=${config.services.bitcoind.rpc.users.public.name}
bitcoind.zmqpubrawblock=${config.services.bitcoind.zmqpubrawblock} bitcoind.zmqpubrawblock=${config.services.bitcoind.zmqpubrawblock}
bitcoind.zmqpubrawtx=${config.services.bitcoind.zmqpubrawtx} bitcoind.zmqpubrawtx=${config.services.bitcoind.zmqpubrawtx}
@ -145,7 +145,7 @@ in {
after = [ "bitcoind.service" ] ++ onion-chef-service; after = [ "bitcoind.service" ] ++ onion-chef-service;
preStart = '' preStart = ''
install -m600 ${configFile} '${cfg.dataDir}/lnd.conf' install -m600 ${configFile} '${cfg.dataDir}/lnd.conf'
echo "bitcoind.rpcpass=$(cat ${secretsDir}/bitcoin-rpcpassword)" >> '${cfg.dataDir}/lnd.conf' echo "bitcoind.rpcpass=$(cat ${secretsDir}/bitcoin-rpcpassword-public)" >> '${cfg.dataDir}/lnd.conf'
${optionalString cfg.announce-tor "echo externalip=$(cat /var/lib/onion-chef/lnd/lnd) >> '${cfg.dataDir}/lnd.conf'"} ${optionalString cfg.announce-tor "echo externalip=$(cat /var/lib/onion-chef/lnd/lnd) >> '${cfg.dataDir}/lnd.conf'"}
''; '';
serviceConfig = nix-bitcoin-services.defaultHardening // { serviceConfig = nix-bitcoin-services.defaultHardening // {

View File

@ -73,6 +73,76 @@ in {
discover = false; discover = false;
addresstype = "bech32"; addresstype = "bech32";
dbCache = 1000; dbCache = 1000;
rpc.users.privileged = {
name = "bitcoinrpc";
# Placeholder to be sed'd out by bitcoind preStart
passwordHMAC = "bitcoin-HMAC-privileged";
};
rpc.users.public = {
name = "publicrpc";
# Placeholder to be sed'd out by bitcoind preStart
passwordHMAC = "bitcoin-HMAC-public";
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; }; services.tor.hiddenServices.bitcoind = mkHiddenService { port = cfg.bitcoind.port; toHost = cfg.bitcoind.bind; };
@ -96,7 +166,7 @@ in {
rpcuser = "liquidrpc"; rpcuser = "liquidrpc";
prune = 1000; prune = 1000;
extraConfig = '' extraConfig = ''
mainchainrpcuser=${cfg.bitcoind.rpcuser} mainchainrpcuser=${config.services.bitcoind.rpc.users.public.name}
mainchainrpcport=8332 mainchainrpcport=8332
''; '';
validatepegin = true; validatepegin = true;

View File

@ -1,6 +1,9 @@
{ pkgs }: with pkgs; { pkgs }: with pkgs;
let
rpcauth = pkgs.writeScriptBin "rpcauth" (builtins.readFile ./rpcauth/rpcauth.py);
in
writeScript "generate-secrets" '' writeScript "generate-secrets" ''
export PATH=${lib.makeBinPath [ coreutils apg openssl ]} export PATH=${lib.makeBinPath [ coreutils apg openssl gnugrep rpcauth python35 ]}
. ${./generate-secrets.sh} ${./openssl.cnf} . ${./generate-secrets.sh} ${./openssl.cnf}
'' ''

View File

@ -6,12 +6,15 @@ makePasswordSecret() {
[[ -e $1 ]] || apg -m 20 -x 20 -M Ncl -n 1 > "$1" [[ -e $1 ]] || apg -m 20 -x 20 -M Ncl -n 1 > "$1"
} }
makePasswordSecret bitcoin-rpcpassword makePasswordSecret bitcoin-rpcpassword-privileged
makePasswordSecret bitcoin-rpcpassword-public
makePasswordSecret lnd-wallet-password makePasswordSecret lnd-wallet-password
makePasswordSecret liquid-rpcpassword makePasswordSecret liquid-rpcpassword
makePasswordSecret lightning-charge-token makePasswordSecret lightning-charge-token
makePasswordSecret spark-wallet-password makePasswordSecret spark-wallet-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 lightning-charge-env ]] || echo "API_TOKEN=$(cat lightning-charge-token)" > lightning-charge-env [[ -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 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 [[ -e spark-wallet-login ]] || echo "login=spark-wallet:$(cat spark-wallet-password)" > spark-wallet-login

View File

@ -0,0 +1,46 @@
#!/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()

View File

@ -7,6 +7,15 @@ succeed('[[ $(stat -c "%U:%G %a" /secrets/dummy) = "root:root 440" ]]')
assert_running("bitcoind") assert_running("bitcoind")
machine.wait_until_succeeds("bitcoin-cli getnetworkinfo") machine.wait_until_succeeds("bitcoin-cli getnetworkinfo")
assert_matches("su operator -c 'bitcoin-cli getnetworkinfo' | jq", '"version"') 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
machine.fail(
"bitcoin-cli -rpcuser=publicrpc -rpcpassword=$(cat /secrets/bitcoin-rpcpassword-public) help"
)
machine.wait_until_succeeds(
log_has_string("bitcoind", "RPC User publicrpc not allowed to call method help")
)
assert_running("electrs") assert_running("electrs")
machine.wait_for_open_port(4224) # prometeus metrics provider machine.wait_for_open_port(4224) # prometeus metrics provider

View File

@ -19,6 +19,15 @@ succeed('[[ $(stat -c "%U:%G %a" /secrets/dummy) = "root:root 440" ]]')
assert_running("bitcoind") assert_running("bitcoind")
machine.wait_until_succeeds("bitcoin-cli getnetworkinfo") machine.wait_until_succeeds("bitcoin-cli getnetworkinfo")
assert_matches("su operator -c 'bitcoin-cli getnetworkinfo' | jq", '"version"') 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
machine.fail(
"bitcoin-cli -rpcuser=publicrpc -rpcpassword=$(cat /secrets/bitcoin-rpcpassword-public) help"
)
machine.wait_until_succeeds(
log_has_string("bitcoind", "RPC User publicrpc not allowed to call method help")
)
assert_running("electrs") assert_running("electrs")
machine.wait_until_succeeds( machine.wait_until_succeeds(