475 lines
17 KiB
Nix
475 lines
17 KiB
Nix
{ config, lib, pkgs, ... }:
|
|
|
|
with lib;
|
|
|
|
let
|
|
cfg = config.services.lamassu-server;
|
|
nbLib = config.nix-bitcoin.lib;
|
|
secretsDir = config.nix-bitcoin.secretsDir;
|
|
|
|
# Path to the deployed lamassu-server (krops puts it in /var/src)
|
|
lamassuServerPath = "/var/src/lamassu-server-built";
|
|
|
|
# Basic hardening settings (simplified from nix-bitcoin)
|
|
defaultHardening = {
|
|
# Sandboxing
|
|
PrivateTmp = true;
|
|
ProtectSystem = "strict";
|
|
ProtectHome = true;
|
|
NoNewPrivileges = true;
|
|
|
|
# Kernel
|
|
ProtectKernelTunables = true;
|
|
ProtectKernelModules = true;
|
|
ProtectControlGroups = true;
|
|
|
|
# Misc
|
|
RestrictRealtime = true;
|
|
RestrictSUIDSGID = true;
|
|
LockPersonality = true;
|
|
};
|
|
|
|
in
|
|
{
|
|
options.services.lamassu-server = {
|
|
enable = mkEnableOption "Lamassu Bitcoin ATM server";
|
|
|
|
serverPort = mkOption {
|
|
type = types.port;
|
|
default = 3000;
|
|
description = "Port for the main lamassu server API";
|
|
};
|
|
|
|
# NOTE: Admin UI port is currently hardcoded in upstream lamassu-server:
|
|
# - Production mode (default): port 443
|
|
# - Dev mode (--dev flag): port 8070
|
|
# Future: Add --ui-port support to upstream to make this configurable.
|
|
# This would also enable nginx reverse proxy (which also needs port 443).
|
|
# See docs/lamassu-future-nginx.md for implementation details.
|
|
|
|
dataDir = mkOption {
|
|
type = types.path;
|
|
default = "/var/lib/lamassu-server";
|
|
description = "Data directory for lamassu-server";
|
|
};
|
|
|
|
user = mkOption {
|
|
type = types.str;
|
|
default = "lamassu-server";
|
|
description = "User to run lamassu-server as";
|
|
};
|
|
|
|
group = mkOption {
|
|
type = types.str;
|
|
default = cfg.user;
|
|
description = "Group to run lamassu-server as";
|
|
};
|
|
|
|
package = mkOption {
|
|
type = types.path;
|
|
default = lamassuServerPath;
|
|
description = "The path to the lamassu-server source directory";
|
|
};
|
|
|
|
logLevel = mkOption {
|
|
type = types.enum [ "error" "warn" "info" "verbose" "debug" "silly" ];
|
|
default = "info";
|
|
description = "Logging level for lamassu-server";
|
|
};
|
|
|
|
skip2FA = mkOption {
|
|
type = types.bool;
|
|
default = true;
|
|
description = "Skip 2FA authentication (useful for initial setup)";
|
|
};
|
|
|
|
# NOTE: devMode is disabled for now. Admin UI runs in production mode (port 443).
|
|
# Future: Re-enable when --ui-port is added to upstream lamassu-server.
|
|
# devMode = mkOption {
|
|
# type = types.bool;
|
|
# default = false;
|
|
# description = "Run admin server in development mode (port 8070).";
|
|
# };
|
|
|
|
database = {
|
|
name = mkOption {
|
|
type = types.str;
|
|
default = cfg.user;
|
|
description = "PostgreSQL database name";
|
|
};
|
|
|
|
user = mkOption {
|
|
type = types.str;
|
|
default = cfg.user;
|
|
description = "PostgreSQL username";
|
|
};
|
|
|
|
# Password is managed by nix-bitcoin secrets system.
|
|
# See: ${secretsDir}/lamassu-db-password
|
|
};
|
|
|
|
hostname = mkOption {
|
|
type = types.str;
|
|
default = "localhost";
|
|
description = ''
|
|
Hostname for the server. This is embedded in the pairing QR code
|
|
and tells ATMs where to connect. Can be an IP address or domain name.
|
|
'';
|
|
};
|
|
|
|
# Certificate options (same pattern as LND)
|
|
# TODO: When using an IP address, hostname and certificate.extraIPs are redundant.
|
|
# Consider auto-populating certificate.extraIPs from hostname if it's an IP,
|
|
# or unifying these options. For now, set both to the same IP address.
|
|
certificate = {
|
|
extraIPs = mkOption {
|
|
type = with types; listOf str;
|
|
default = [];
|
|
example = [ "192.168.1.100" ];
|
|
description = ''
|
|
Extra IP addresses to include in the certificate SAN.
|
|
'';
|
|
};
|
|
extraDomains = mkOption {
|
|
type = with types; listOf str;
|
|
default = [];
|
|
example = [ "lamassu.example.com" ];
|
|
description = ''
|
|
Extra domain names to include in the certificate SAN.
|
|
'';
|
|
};
|
|
};
|
|
|
|
# Read-only options for certificate paths
|
|
certPath = mkOption {
|
|
readOnly = true;
|
|
default = "${secretsDir}/lamassu-cert";
|
|
description = "Path to the TLS certificate.";
|
|
};
|
|
|
|
keyPath = mkOption {
|
|
readOnly = true;
|
|
default = "${secretsDir}/lamassu-key";
|
|
description = "Path to the TLS private key.";
|
|
};
|
|
|
|
# NOTE: nginx is disabled for now because admin UI binds directly to port 443.
|
|
# Enabling nginx would cause a port conflict.
|
|
# Future: Add --ui-port to upstream, run admin UI on internal port (e.g., 8070),
|
|
# and use nginx as reverse proxy on 443. See docs/lamassu-future-nginx.md
|
|
# nginx = {
|
|
# enable = mkEnableOption "Nginx reverse proxy on port 443";
|
|
# hostname = mkOption {
|
|
# type = types.nullOr types.str;
|
|
# default = null;
|
|
# description = "Hostname for nginx virtual host";
|
|
# };
|
|
# };
|
|
|
|
enableBitcoin = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = "Enable Bitcoin integration (requires bitcoind)";
|
|
};
|
|
};
|
|
|
|
config = mkIf cfg.enable {
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# nix-bitcoin secrets integration
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
nix-bitcoin.secrets = {
|
|
lamassu-key.user = cfg.user;
|
|
lamassu-cert = {
|
|
user = cfg.user;
|
|
permissions = "444"; # World readable (it's a public cert)
|
|
};
|
|
lamassu-db-password = {
|
|
user = cfg.user;
|
|
group = "postgres"; # PostgreSQL needs to read this too
|
|
};
|
|
};
|
|
|
|
nix-bitcoin.generateSecretsCmds.lamassu = ''
|
|
makeCert lamassu '${nbLib.mkCertExtraAltNames cfg.certificate}'
|
|
makePasswordSecret lamassu-db-password
|
|
'';
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
# NOTE: Nginx reverse proxy is disabled. See docs/lamassu-future-nginx.md
|
|
# for future implementation when --ui-port is added to upstream.
|
|
|
|
# Enable PostgreSQL
|
|
services.postgresql = {
|
|
enable = true;
|
|
package = pkgs.postgresql_15;
|
|
ensureDatabases = [ cfg.database.name ];
|
|
ensureUsers = [
|
|
{
|
|
name = cfg.database.user;
|
|
ensureDBOwnership = true;
|
|
}
|
|
];
|
|
# Enable password authentication for localhost connections
|
|
authentication = pkgs.lib.mkOverride 10 ''
|
|
# TYPE DATABASE USER ADDRESS METHOD
|
|
local all all peer
|
|
host all all 127.0.0.1/32 md5
|
|
host all all ::1/128 md5
|
|
'';
|
|
};
|
|
|
|
# Create system users and groups
|
|
users.users = {
|
|
# Lamassu server user
|
|
${cfg.user} = {
|
|
isSystemUser = true;
|
|
group = cfg.group;
|
|
home = cfg.dataDir;
|
|
createHome = true;
|
|
};
|
|
};
|
|
|
|
users.groups.${cfg.group} = {};
|
|
|
|
# Create data directory with proper permissions
|
|
systemd.tmpfiles.rules = [
|
|
"d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -"
|
|
"d '${cfg.dataDir}/logs' 0770 ${cfg.user} ${cfg.group} - -"
|
|
"d '${cfg.dataDir}/blockchain' 0770 ${cfg.user} ${cfg.group} - -"
|
|
"d '${cfg.dataDir}/ofac' 0770 ${cfg.user} ${cfg.group} - -"
|
|
"d '${cfg.dataDir}/photos' 0770 ${cfg.user} ${cfg.group} - -"
|
|
"d '${cfg.dataDir}/photos/idcards' 0770 ${cfg.user} ${cfg.group} - -"
|
|
"d '${cfg.dataDir}/photos/frontcamera' 0770 ${cfg.user} ${cfg.group} - -"
|
|
"d '${cfg.dataDir}/operator' 0770 ${cfg.user} ${cfg.group} - -"
|
|
# Ensure lamassu-server user can read/write to the source directory
|
|
"Z '${cfg.package}' 0755 ${cfg.user} ${cfg.group} - -"
|
|
];
|
|
|
|
# Service to set PostgreSQL password from nix-bitcoin secrets
|
|
systemd.services.lamassu-postgres-setup = {
|
|
description = "Setup PostgreSQL password for lamassu-server";
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [ "postgresql.service" "nix-bitcoin-secrets.target" ];
|
|
wants = [ "postgresql.service" ];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
User = "postgres";
|
|
};
|
|
script = ''
|
|
# Wait for user to exist, then set password from secrets
|
|
for i in {1..30}; do
|
|
if ${pkgs.postgresql}/bin/psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='${cfg.database.user}'" | grep -q 1; then
|
|
echo "Setting password for ${cfg.database.user}..."
|
|
password=$(cat ${secretsDir}/lamassu-db-password)
|
|
${pkgs.postgresql}/bin/psql -c "ALTER USER \"${cfg.database.user}\" WITH PASSWORD '$password';"
|
|
exit 0
|
|
fi
|
|
echo "Waiting for user ${cfg.database.user} to be created (attempt $i/30)..."
|
|
sleep 1
|
|
done
|
|
echo "ERROR: User ${cfg.database.user} was not created after 30 seconds"
|
|
exit 1
|
|
'';
|
|
};
|
|
|
|
# Main lamassu server service
|
|
systemd.services.lamassu-server = {
|
|
description = "Lamassu Bitcoin ATM Server";
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [ "network.target" "postgresql.service" "lamassu-postgres-setup.service" "nix-bitcoin-secrets.target" ];
|
|
wants = [ "postgresql.service" "lamassu-postgres-setup.service" ];
|
|
|
|
environment = {
|
|
NODE_ENV = "production";
|
|
|
|
# Database configuration (password read at runtime from secrets)
|
|
POSTGRES_HOST = "127.0.0.1";
|
|
POSTGRES_PORT = "5432";
|
|
POSTGRES_DB = cfg.database.name;
|
|
POSTGRES_USER = cfg.database.user;
|
|
|
|
# Server configuration
|
|
SERVER_PORT = toString cfg.serverPort;
|
|
LOG_LEVEL = cfg.logLevel;
|
|
HOSTNAME = cfg.hostname;
|
|
|
|
# SSL/TLS certificates (from nix-bitcoin secrets)
|
|
CA_PATH = cfg.certPath;
|
|
CERT_PATH = cfg.certPath;
|
|
KEY_PATH = cfg.keyPath;
|
|
|
|
# Wallet and mnemonic
|
|
MNEMONIC_PATH = "${cfg.dataDir}/lamassu-mnemonic";
|
|
|
|
# Data directories
|
|
OFAC_DATA_DIR = "${cfg.dataDir}/ofac";
|
|
ID_PHOTO_CARD_DIR = "${cfg.dataDir}/photos/idcards";
|
|
FRONT_CAMERA_DIR = "${cfg.dataDir}/photos/frontcamera";
|
|
OPERATOR_DATA_DIR = "${cfg.dataDir}/operator";
|
|
|
|
# Bitcoin RPC configuration (if enabled)
|
|
BTC_NODE_LOCATION = "remote";
|
|
BTC_WALLET_LOCATION = "remote";
|
|
BTC_NODE_USER = "lamassu";
|
|
BTC_NODE_RPC_HOST = "192.168.0.34";
|
|
BTC_NODE_RPC_PORT = "8332";
|
|
BTC_NODE_PASSWORD = "L3XF8iUrr5FNk2k6mILI";
|
|
|
|
# Security
|
|
SKIP_2FA = if cfg.skip2FA then "true" else "false";
|
|
};
|
|
|
|
serviceConfig = let
|
|
lamassuEnv = pkgs.writeShellScript "lamassu-env" ''
|
|
#!/bin/bash
|
|
set -euo pipefail
|
|
export PATH=${pkgs.nodejs_22}/bin:$PATH
|
|
# Read database password from nix-bitcoin secrets
|
|
DB_PASSWORD=$(cat ${secretsDir}/lamassu-db-password)
|
|
export DATABASE_URL="postgresql://${cfg.database.user}:$DB_PASSWORD@127.0.0.1:5432/${cfg.database.name}"
|
|
export POSTGRES_PASSWORD="$DB_PASSWORD"
|
|
export NODE_PATH=${cfg.package}/node_modules:${cfg.package}/packages/server/node_modules
|
|
cd ${cfg.package}
|
|
exec "$@"
|
|
'';
|
|
in defaultHardening // {
|
|
WorkingDirectory = "${cfg.package}/packages/server";
|
|
ExecStartPre = [
|
|
# Generate BIP39 mnemonic if it doesn't exist
|
|
"${pkgs.bash}/bin/bash -c 'if [[ ! -f ${cfg.dataDir}/lamassu-mnemonic ]]; then echo \"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about\" > ${cfg.dataDir}/lamassu-mnemonic && chmod 600 ${cfg.dataDir}/lamassu-mnemonic; fi'"
|
|
# Run database migration
|
|
"${lamassuEnv} ${pkgs.nodejs_22}/bin/node packages/server/bin/lamassu-migrate"
|
|
];
|
|
ExecStart = "${lamassuEnv} ${pkgs.nodejs_22}/bin/node packages/server/bin/lamassu-server --port ${toString cfg.serverPort} --logLevel ${cfg.logLevel}";
|
|
|
|
# Node.js specific overrides
|
|
MemoryDenyWriteExecute = false;
|
|
|
|
# Allow read/write access
|
|
ReadWritePaths = [ cfg.dataDir cfg.package "/tmp" ];
|
|
RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
|
|
|
|
# Service identity
|
|
User = cfg.user;
|
|
Group = cfg.group;
|
|
Restart = "on-failure";
|
|
RestartSec = "10s";
|
|
};
|
|
|
|
preStart = ''
|
|
mkdir -p ${cfg.dataDir}/logs
|
|
|
|
# Wait for PostgreSQL using peer authentication
|
|
timeout=30
|
|
while ! ${pkgs.postgresql}/bin/psql -h /run/postgresql -U ${cfg.database.user} -d ${cfg.database.name} -c '\q' 2>/dev/null; do
|
|
if [ $timeout -le 0 ]; then
|
|
echo "Timeout waiting for PostgreSQL"
|
|
exit 1
|
|
fi
|
|
echo "Waiting for PostgreSQL..."
|
|
sleep 1
|
|
((timeout--))
|
|
done
|
|
echo "PostgreSQL is ready"
|
|
'';
|
|
};
|
|
|
|
# Admin server service
|
|
systemd.services.lamassu-admin-server = {
|
|
description = "Lamassu Admin Server";
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [ "network.target" "lamassu-server.service" ];
|
|
wants = [ "lamassu-server.service" ];
|
|
|
|
environment = {
|
|
NODE_ENV = "production";
|
|
LOG_LEVEL = cfg.logLevel;
|
|
HOSTNAME = cfg.hostname;
|
|
CA_PATH = cfg.certPath;
|
|
CERT_PATH = cfg.certPath;
|
|
KEY_PATH = cfg.keyPath;
|
|
# Database configuration (password read at runtime from secrets)
|
|
POSTGRES_HOST = "127.0.0.1";
|
|
POSTGRES_PORT = "5432";
|
|
POSTGRES_DB = cfg.database.name;
|
|
POSTGRES_USER = cfg.database.user;
|
|
MNEMONIC_PATH = "${cfg.dataDir}/lamassu-mnemonic";
|
|
SKIP_2FA = if cfg.skip2FA then "true" else "false";
|
|
# Data directories
|
|
OFAC_DATA_DIR = "${cfg.dataDir}/ofac";
|
|
ID_PHOTO_CARD_DIR = "${cfg.dataDir}/photos/idcards";
|
|
FRONT_CAMERA_DIR = "${cfg.dataDir}/photos/frontcamera";
|
|
OPERATOR_DATA_DIR = "${cfg.dataDir}/operator";
|
|
};
|
|
|
|
serviceConfig = let
|
|
lamassuAdminEnv = pkgs.writeShellScript "lamassu-admin-env" ''
|
|
#!/bin/bash
|
|
set -euo pipefail
|
|
export PATH=${pkgs.nodejs_22}/bin:$PATH
|
|
# Read database password from nix-bitcoin secrets
|
|
DB_PASSWORD=$(cat ${secretsDir}/lamassu-db-password)
|
|
export DATABASE_URL="postgresql://${cfg.database.user}:$DB_PASSWORD@127.0.0.1:5432/${cfg.database.name}"
|
|
export POSTGRES_PASSWORD="$DB_PASSWORD"
|
|
export NODE_PATH=${cfg.package}/node_modules:${cfg.package}/packages/admin-server/node_modules
|
|
cd ${cfg.package}
|
|
exec "$@"
|
|
'';
|
|
in defaultHardening // {
|
|
WorkingDirectory = "${cfg.package}/packages/server";
|
|
ExecStart = "${lamassuAdminEnv} ${pkgs.nodejs_22}/bin/node packages/server/bin/lamassu-admin-server --logLevel ${cfg.logLevel}";
|
|
MemoryDenyWriteExecute = false;
|
|
ReadWritePaths = [ cfg.dataDir cfg.package ];
|
|
User = cfg.user;
|
|
Group = cfg.group;
|
|
Restart = "on-failure";
|
|
RestartSec = "10s";
|
|
|
|
# Allow binding to privileged port 443 (production mode)
|
|
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
|
|
CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
|
|
};
|
|
};
|
|
|
|
# Open firewall ports
|
|
# Port 3000 (configurable): machine API access (required for pairing and operation)
|
|
# Port 443: admin UI (production mode, hardcoded in upstream)
|
|
networking.firewall.allowedTCPPorts = [ cfg.serverPort 443 ];
|
|
|
|
# Add useful packages
|
|
environment.systemPackages = with pkgs; [
|
|
nodejs_22
|
|
nodePackages.pnpm
|
|
postgresql
|
|
(writeShellScriptBin "lamassu-register-user" ''
|
|
# Read database password from nix-bitcoin secrets
|
|
DB_PASSWORD=$(cat ${secretsDir}/lamassu-db-password)
|
|
export NODE_PATH="${cfg.package}/node_modules:${cfg.package}/packages/server/node_modules"
|
|
export DATABASE_URL="postgresql://${cfg.database.user}:$DB_PASSWORD@127.0.0.1:5432/${cfg.database.name}"
|
|
export HOSTNAME="${cfg.hostname}"
|
|
export POSTGRES_HOST="127.0.0.1"
|
|
export POSTGRES_PORT="5432"
|
|
export POSTGRES_DB="${cfg.database.name}"
|
|
export POSTGRES_USER="${cfg.database.user}"
|
|
export POSTGRES_PASSWORD="$DB_PASSWORD"
|
|
export SKIP_2FA="${if cfg.skip2FA then "true" else "false"}"
|
|
|
|
sudo -E -u ${cfg.user} bash -c "cd ${cfg.package}/packages/server && ${pkgs.nodejs_22}/bin/node bin/lamassu-register \"\$@\"" -- "$@"
|
|
'')
|
|
(writeShellScriptBin "lamassu-status" ''
|
|
echo "=== Lamassu Server Status ==="
|
|
systemctl status lamassu-server lamassu-admin-server
|
|
echo ""
|
|
echo "=== Database Status ==="
|
|
sudo -u ${cfg.database.user} ${pkgs.postgresql}/bin/psql -d ${cfg.database.name} -c "SELECT version();"
|
|
echo ""
|
|
echo "=== Network Access ==="
|
|
echo "Server API: https://localhost:${toString cfg.serverPort}"
|
|
echo "Admin UI: https://localhost:443"
|
|
'')
|
|
];
|
|
};
|
|
}
|