{ 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" '') ]; }; }