vendor: replace nix-bitcoin flake input with minimal vendored modules (nixpkgs-only)
- 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
This commit is contained in:
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+479
@@ -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 `<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.
|
||||
'';
|
||||
};
|
||||
};
|
||||
}));
|
||||
};
|
||||
};
|
||||
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
|
||||
'';
|
||||
};
|
||||
}
|
||||
+282
@@ -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
|
||||
'';
|
||||
};
|
||||
}
|
||||
+106
@@ -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} = {};
|
||||
};
|
||||
}
|
||||
+54
@@ -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;
|
||||
};
|
||||
}
|
||||
Vendored
+132
@@ -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
|
||||
Vendored
+314
@@ -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}'
|
||||
'';
|
||||
};
|
||||
}
|
||||
+282
@@ -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" ];
|
||||
})
|
||||
])
|
||||
)
|
||||
];
|
||||
}
|
||||
+349
@@ -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.<mempool server name>.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.<mempool server name>.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} = {};
|
||||
};
|
||||
}
|
||||
+27
@@ -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" ];
|
||||
}
|
||||
+16
@@ -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." ];
|
||||
};
|
||||
}
|
||||
+60
@@ -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)";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
+167
@@ -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})""")
|
||||
'');
|
||||
};
|
||||
};
|
||||
}
|
||||
+106
@@ -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/<user>/
|
||||
# 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/<service>`
|
||||
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}
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
+122
@@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
];
|
||||
}
|
||||
+58
@@ -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
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
Vendored
+239
@@ -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
|
||||
'';
|
||||
};
|
||||
}
|
||||
+235
@@ -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"
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
+62
@@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
+48
@@ -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" ''
|
||||
<busconfig>
|
||||
<policy context="default">
|
||||
<deny
|
||||
send_destination="org.freedesktop.systemd1"
|
||||
send_interface="org.freedesktop.systemd1.Manager"
|
||||
send_member="GetUnitProcesses"
|
||||
/>
|
||||
</policy>
|
||||
<policy group="proc">
|
||||
<allow
|
||||
send_destination="org.freedesktop.systemd1"
|
||||
send_interface="org.freedesktop.systemd1.Manager"
|
||||
send_member="GetUnitProcesses"
|
||||
/>
|
||||
</policy>
|
||||
</busconfig>
|
||||
'')
|
||||
];
|
||||
};
|
||||
}
|
||||
+12
@@ -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 = {};
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
These packages are provided by nixpkgs directly. No extra pinning needed.
|
||||
Missing helpers (lndinit) are vendored below if needed.
|
||||
@@ -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; };
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user