From 03db1a61b1121954460300aeb3651c09b052169b Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Wed, 10 Mar 2021 14:08:37 +0100 Subject: [PATCH] lnd, joinmarket: don't write to secrets dir Keeping the secrets dir read-only is more simple and robust. - lnd seed mnemonic creation and joinmarket wallet creation can be run as the regular service user instead of root. - It is easier to switch to a third-party secrets deployment method in the future. Don't create a seed mnemonic for lnd when a wallet exists. This avoids creating unused mnemonics and helps simplifying the migration command in `versioning.nix`. --- examples/configuration.nix | 11 +++++++---- modules/backups.nix | 4 +--- modules/joinmarket.nix | 9 ++++----- modules/lnd.nix | 29 ++++++++++++----------------- modules/versioning.nix | 22 ++++++++++++++++++++++ test/tests.py | 14 ++++++++++---- 6 files changed, 56 insertions(+), 33 deletions(-) diff --git a/examples/configuration.nix b/examples/configuration.nix index 85c44eb..0b61906 100644 --- a/examples/configuration.nix +++ b/examples/configuration.nix @@ -71,11 +71,13 @@ ## WARNING # If you use lnd, you should manually backup your wallet mnemonic # seed. This will allow you to recover on-chain funds. You can run the - # following command after the lnd service starts: - # scp bitcoin-node:/secrets/lnd-seed-mnemonic ./secrets/lnd-seed-mnemonic + # following commands after the lnd service starts: + # mkdir -p ./backups/lnd/ + # scp bitcoin-node:/var/lib/lnd/lnd-seed-mnemonic ./backups/lnd/ + # # You should also backup your channel state after opening new channels. # This will allow you to recover off-chain funds, by force-closing channels. - # scp bitcoin-node:/var/lib/lnd/chain/bitcoin/mainnet/channel.backup /my-backup-path/channel.backup + # scp bitcoin-node:/var/lib/lnd/chain/bitcoin/mainnet/channel.backup ./backups/lnd/ ### SPARK WALLET # Enable this module to use spark-wallet, a minimalistic wallet GUI for @@ -229,5 +231,6 @@ # The nix-bitcoin release version that your config is compatible with. # When upgrading to a backwards-incompatible release, nix-bitcoin will display an # an error and provide hints for migrating your config to the new release. - nix-bitcoin.configVersion = "0.0.30"; + nix-bitcoin.configVersion = "0.0.41"; + } diff --git a/modules/backups.nix b/modules/backups.nix index 2669620..6209d64 100644 --- a/modules/backups.nix +++ b/modules/backups.nix @@ -4,7 +4,6 @@ with lib; let cfg = config.services.backups; - secretsDir = config.nix-bitcoin.secretsDir; filelist = pkgs.writeText "filelist.txt" '' ${optionalString (!cfg.with-bulk-data) "- ${config.services.bitcoind.dataDir}/blocks"} @@ -12,7 +11,6 @@ let ${config.services.bitcoind.dataDir} ${config.services.clightning.dataDir} ${config.services.lnd.dataDir} - ${secretsDir}/lnd-seed-mnemonic ${optionalString (!cfg.with-bulk-data) "- ${config.services.liquidd.dataDir}/*/blocks"} ${optionalString (!cfg.with-bulk-data) "- ${config.services.liquidd.dataDir}/*/chainstate"} ${config.services.liquidd.dataDir} @@ -20,8 +18,8 @@ let ${config.services.nbxplorer.dataDir} ${config.services.btcpayserver.dataDir} ${config.services.joinmarket.dataDir} - ${secretsDir}/jm-wallet-seed ${config.services.postgresqlBackup.location}/btcpaydb.sql.gz + ${optionalString config.nix-bitcoin.generateSecrets "${config.nix-bitcoin.secretsDir}"} /var/lib/tor # Extra files ${cfg.extraFiles} diff --git a/modules/joinmarket.nix b/modules/joinmarket.nix index 1dcb83a..8f46652 100644 --- a/modules/joinmarket.nix +++ b/modules/joinmarket.nix @@ -240,20 +240,19 @@ in { ''; # Generating wallets (jmclient/wallet.py) is only supported for mainnet or testnet ExecStartPost = mkIf (bitcoind.network == "mainnet") - (nbLib.privileged "joinmarket-create-wallet" '' + (nbLib.script "joinmarket-create-wallet" '' walletname=wallet.jmdat wallet=${cfg.dataDir}/wallets/$walletname if [[ ! -f $wallet ]]; then echo "Create wallet" pw=$(cat "${secretsDir}"/jm-wallet-password) cd ${cfg.dataDir} - if ! ${pkgs.utillinux}/bin/runuser -u ${cfg.user} -- \ - ${nbPkgs.joinmarket}/bin/jm-genwallet --datadir=${cfg.dataDir} $walletname $pw \ + if ! ${nbPkgs.joinmarket}/bin/jm-genwallet --datadir=${cfg.dataDir} $walletname $pw \ | grep 'recovery_seed' \ | cut -d ':' -f2 \ - | (umask u=r,go=; cat > "${secretsDir}/jm-wallet-seed"); then + | (umask u=r,go=; cat > jm-wallet-seed); then echo "wallet creation failed" - rm -f "$wallet" "${secretsDir}/jm-wallet-seed" + rm -f "$wallet" jm-wallet-seed exit 1 fi fi diff --git a/modules/lnd.nix b/modules/lnd.nix index aa6aada..00493a5 100644 --- a/modules/lnd.nix +++ b/modules/lnd.nix @@ -200,32 +200,28 @@ in { ExecStartPost = let restUrl = "https://${cfg.restAddress}:${toString cfg.restPort}/v1"; in [ - # Run fully privileged for secrets dir write access - (nbLib.privileged "lnd-create-mnemonic" '' + (nbLib.script "lnd-create-wallet" '' attempts=250 while ! { exec 3>/dev/tcp/${cfg.restAddress}/${toString cfg.restPort} && exec 3>&-; } &>/dev/null; do ((attempts-- == 0)) && { echo "lnd REST service unreachable"; exit 1; } sleep 0.1 done - mnemonic=${secretsDir}/lnd-seed-mnemonic - if [[ ! -f $mnemonic ]]; then - echo Create lnd seed - umask u=r,go= - ${pkgs.curl}/bin/curl -s \ - --cacert ${secretsDir}/lnd-cert \ - -X GET ${restUrl}/genseed | ${pkgs.jq}/bin/jq -c '.cipher_seed_mnemonic' > "$mnemonic" - fi - chown ${cfg.user}: "$mnemonic" - '') - (nbLib.script "lnd-create-wallet" '' if [[ ! -f ${networkDir}/wallet.db ]]; then - echo Create lnd wallet + mnemonic="${cfg.dataDir}/lnd-seed-mnemonic" + if [[ ! -f "$mnemonic" ]]; then + echo Create lnd seed + umask u=r,go= + ${pkgs.curl}/bin/curl -s \ + --cacert ${secretsDir}/lnd-cert \ + -X GET ${restUrl}/genseed | ${pkgs.jq}/bin/jq -c '.cipher_seed_mnemonic' > "$mnemonic" + fi + echo Create lnd wallet ${pkgs.curl}/bin/curl -s --output /dev/null --show-error \ --cacert ${secretsDir}/lnd-cert \ -X POST -d "{\"wallet_password\": \"$(cat ${secretsDir}/lnd-wallet-password | tr -d '\n' | base64 -w0)\", \ - \"cipher_seed_mnemonic\": $(cat ${secretsDir}/lnd-seed-mnemonic | tr -d '\n')}" \ + \"cipher_seed_mnemonic\": $(cat "$mnemonic" | tr -d '\n')}" \ ${restUrl}/initwallet # Guarantees that RPC calls with cfg.cli succeed after the service is started @@ -248,9 +244,8 @@ in { while ! { exec 3>/dev/tcp/${cfg.rpcAddress}/${toString cfg.rpcPort}; } &>/dev/null; do sleep 0.1 done - '') - # Run fully privileged for chown + # Setting macaroon permission for other users needs root permissions (nbLib.privileged "lnd-create-macaroons" '' umask ug=r,o= ${lib.concatMapStrings (macaroon: '' diff --git a/modules/versioning.nix b/modules/versioning.nix index f06a593..f34eaac 100644 --- a/modules/versioning.nix +++ b/modules/versioning.nix @@ -69,6 +69,28 @@ let (mkOnionServiceChange "clightning") (mkOnionServiceChange "lnd") (mkOnionServiceChange "btcpayserver") + { + version = "0.0.41"; + condition = config.services.lnd.enable || config.services.joinmarket.enable; + message = let + secretsDir = config.nix-bitcoin.secretsDir; + lnd = config.services.lnd; + jm = config.services.joinmarket; + in '' + Secret files generated by services at runtime are now stored in the service + data dirs instead of the global secrets dir. + + To migrate, run the following Bash script as root on your nix-bitcoin node: + + if [[ -e ${secretsDir}/lnd-seed-mnemonic ]]; then + install -o ${lnd.user} -g ${lnd.group} -m400 "${secretsDir}/lnd-seed-mnemonic" "${lnd.dataDir}" + fi + if [[ -e ${secretsDir}/jm-wallet-seed ]]; then + install -o ${jm.user} -g ${jm.group} -m400 "${secretsDir}/jm-wallet-seed" "${jm.dataDir}" + fi + rm -f "${secretsDir}"/{lnd-seed-mnemonic,jm-wallet-seed} + ''; + } ]; incompatibleChanges = optionals diff --git a/test/tests.py b/test/tests.py index dd37785..48cc4ca 100644 --- a/test/tests.py +++ b/test/tests.py @@ -326,16 +326,22 @@ def _(): files = { "bitcoind": "var/lib/bitcoind/test/wallet.dat", "clightning": "var/lib/clightning/bitcoin/hsm_secret", - "lnd": "secrets/lnd-seed-mnemonic", - "joinmarket": "secrets/jm-wallet-seed", + "lnd": "var/lib/lnd/lnd-seed-mnemonic", + "joinmarket": "var/lib/joinmarket/jm-wallet-seed", "btcpayserver": "var/backup/postgresql/btcpaydb.sql.gz", } actual_files = succeed(f"{run_duplicity} list-current-files file:///var/lib/localBackups") - for test, file in files.items(): - if test in enabled_tests and file not in actual_files: + def assert_file_exists(file): + if file not in actual_files: raise Exception(f"Backup file '{file}' is missing.") + for test, file in files.items(): + if test in enabled_tests: + assert_file_exists(file) + + assert_file_exists("secrets/lnd-wallet-password") + # Impure: restarts services @test("banlist-and-restart")