From e3db3e58b91a1138f266a301bad59b9aaaece97f Mon Sep 17 00:00:00 2001 From: padreug Date: Tue, 23 Dec 2025 11:33:36 +0100 Subject: [PATCH] add lamassu-lnbits module --- modules/lamassu-lnbits.nix | 512 +++++++++++++++++++++++++++++++++++++ 1 file changed, 512 insertions(+) create mode 100644 modules/lamassu-lnbits.nix diff --git a/modules/lamassu-lnbits.nix b/modules/lamassu-lnbits.nix new file mode 100644 index 0000000..35d1d42 --- /dev/null +++ b/modules/lamassu-lnbits.nix @@ -0,0 +1,512 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.lamassu-server; + secretsDir = "/var/src/secrets"; # krops deploys secrets here + + # Path to the deployed lamassu-server (krops puts it in /var/src) + lamassuServerPath = "/var/src/lamassu-server-built"; + + # Detect if a string is an IP address (simple regex check) + isIpAddress = str: builtins.match "([0-9]{1,3}\\.){3}[0-9]{1,3}" str != null; + + # Build Subject Alternative Name (SAN) extension for certificate + # Uses IP: prefix for IP addresses, DNS: prefix for hostnames + sanEntries = + [ (if isIpAddress cfg.hostname then "IP:${cfg.hostname}" else "DNS:${cfg.hostname}") ] ++ + (optional (cfg.ipAddress != null) "IP:${cfg.ipAddress}") ++ + (optional (cfg.nginx.enable && cfg.nginx.hostname != null && cfg.nginx.hostname != cfg.hostname) + (if isIpAddress cfg.nginx.hostname then "IP:${cfg.nginx.hostname}" else "DNS:${cfg.nginx.hostname}")); + + sanExtension = concatStringsSep "," sanEntries; + + # 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"; + }; + + adminPort = mkOption { + type = types.port; + default = 8070; + description = "Port for the lamassu admin interface"; + }; + + 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)"; + }; + + devMode = mkOption { + type = types.bool; + default = false; + description = "Run admin server in development mode (port 8070). When false, runs in production mode (port 443)."; + }; + + 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 = mkOption { + type = types.str; + default = "lamassu123"; + description = "PostgreSQL password for lamassu-server user"; + }; + }; + + hostname = mkOption { + type = types.str; + default = "localhost"; + description = "Hostname for the server (used by application and included in certificate)"; + }; + + ipAddress = mkOption { + type = types.nullOr types.str; + default = null; + description = "IP address to include in certificate SAN (optional)"; + example = "192.168.1.100"; + }; + + nginx = { + enable = mkEnableOption "Nginx reverse proxy on port 443"; + + hostname = mkOption { + type = types.nullOr types.str; + default = null; + description = "Hostname for nginx virtual host (if different from main hostname)"; + example = "lamassu.example.com"; + }; + }; + + enableBitcoin = mkOption { + type = types.bool; + default = false; + description = "Enable Bitcoin integration (requires bitcoind)"; + }; + }; + + config = mkIf cfg.enable { + # Nginx reverse proxy (optional, disabled by default) + services.nginx = mkIf cfg.nginx.enable { + enable = true; + recommendedTlsSettings = true; + recommendedProxySettings = true; + + virtualHosts.${if cfg.nginx.hostname != null then cfg.nginx.hostname else cfg.hostname} = { + forceSSL = true; + sslCertificate = "${cfg.dataDir}/ssl/cert.pem"; + sslCertificateKey = "${cfg.dataDir}/ssl/key.pem"; + + # Route API endpoints to main server (port 3000) + locations."/ca" = { + proxyPass = "https://127.0.0.1:${toString cfg.serverPort}"; + extraConfig = '' + proxy_ssl_verify off; + proxy_ssl_server_name on; + 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; + ''; + }; + + locations."/pair" = { + proxyPass = "https://127.0.0.1:${toString cfg.serverPort}"; + extraConfig = '' + proxy_ssl_verify off; + proxy_ssl_server_name on; + 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; + ''; + }; + + # Route everything else to admin server (port 8070) + locations."/" = { + proxyPass = "https://127.0.0.1:${toString cfg.adminPort}"; + extraConfig = '' + proxy_ssl_verify off; + proxy_ssl_server_name on; + 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; + ''; + }; + }; + }; + + # 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 + ''; + # Set initial password for lamassu-server user + initialScript = pkgs.writeText "postgres-init.sql" '' + ALTER USER "${cfg.database.user}" WITH PASSWORD '${cfg.database.password}'; + ''; + }; + + # Create system users and groups + users.users = { + # Lamassu server user + ${cfg.user} = { + isSystemUser = true; + group = cfg.group; + home = cfg.dataDir; + createHome = true; + }; + } // optionalAttrs cfg.nginx.enable { + # Add nginx user to lamassu-server group to access SSL certificates + nginx.extraGroups = [ cfg.group ]; + }; + + 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} - -" + "d '${cfg.dataDir}/ssl' 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 + systemd.services.lamassu-postgres-setup = { + description = "Setup PostgreSQL password for lamassu-server"; + wantedBy = [ "multi-user.target" ]; + after = [ "postgresql.service" ]; + wants = [ "postgresql.service" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + User = "postgres"; + }; + script = '' + # Wait for user to exist, then set password + 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}..." + ${pkgs.postgresql}/bin/psql -c "ALTER USER \"${cfg.database.user}\" WITH PASSWORD '${cfg.database.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" ]; + wants = [ "postgresql.service" "lamassu-postgres-setup.service" ]; + + environment = { + NODE_ENV = "production"; + + # Database configuration (using TCP with password auth) + POSTGRES_HOST = "127.0.0.1"; + POSTGRES_PORT = "5432"; + POSTGRES_DB = cfg.database.name; + POSTGRES_USER = cfg.database.user; + POSTGRES_PASSWORD = cfg.database.password; + + # Server configuration + SERVER_PORT = toString cfg.serverPort; + LOG_LEVEL = cfg.logLevel; + HOSTNAME = cfg.hostname; + + # SSL/TLS certificates + CA_PATH = "${cfg.dataDir}/ssl/cert.pem"; + CERT_PATH = "${cfg.dataDir}/ssl/cert.pem"; + KEY_PATH = "${cfg.dataDir}/ssl/key.pem"; + + # 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 + # Use TCP connection to localhost with password + export DATABASE_URL="postgresql://${cfg.database.user}:${cfg.database.password}@127.0.0.1:5432/${cfg.database.name}" + 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 --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 + mkdir -p ${cfg.dataDir}/ssl + + # 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" + + # Generate self-signed SSL certificates if they don't exist + if [ ! -f ${cfg.dataDir}/ssl/key.pem ]; then + echo "Generating self-signed SSL certificates..." + echo " Hostname: ${cfg.hostname}" + ${optionalString (cfg.ipAddress != null) '' + echo " IP Address: ${cfg.ipAddress}" + ''} + ${optionalString (cfg.nginx.enable && cfg.nginx.hostname != null) '' + echo " Nginx hostname: ${cfg.nginx.hostname}" + ''} + + # Generate certificate with SAN extension + ${pkgs.openssl}/bin/openssl req -x509 -newkey rsa:4096 \ + -keyout ${cfg.dataDir}/ssl/key.pem \ + -out ${cfg.dataDir}/ssl/cert.pem \ + -days 365 -nodes \ + -subj "/CN=${cfg.hostname}" \ + -addext "subjectAltName=${sanExtension}" + + chmod 640 ${cfg.dataDir}/ssl/key.pem # Allow group read for nginx + chmod 644 ${cfg.dataDir}/ssl/cert.pem + + echo "Certificate generated with SAN: ${sanExtension}" + fi + ''; + }; + + # 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"; + ADMIN_SERVER_PORT = toString cfg.adminPort; + LOG_LEVEL = cfg.logLevel; + HOSTNAME = cfg.hostname; + CA_PATH = "${cfg.dataDir}/ssl/cert.pem"; + CERT_PATH = "${cfg.dataDir}/ssl/cert.pem"; + KEY_PATH = "${cfg.dataDir}/ssl/key.pem"; + # Database configuration (using TCP with password auth) + POSTGRES_HOST = "127.0.0.1"; + POSTGRES_PORT = "5432"; + POSTGRES_DB = cfg.database.name; + POSTGRES_USER = cfg.database.user; + POSTGRES_PASSWORD = cfg.database.password; + 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 + # Use TCP connection to localhost with password + export DATABASE_URL="postgresql://${cfg.database.user}:${cfg.database.password}@127.0.0.1:5432/${cfg.database.name}" + 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 ${optionalString cfg.devMode "--dev"} --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 when in production mode (not dev mode) + AmbientCapabilities = optionals (!cfg.devMode) [ "CAP_NET_BIND_SERVICE" ]; + CapabilityBoundingSet = optionals (!cfg.devMode) [ "CAP_NET_BIND_SERVICE" ]; + }; + }; + + # Open firewall ports + # Port 3000 for machine API access (required for pairing and operation) + # Port 8070 for admin interface (only in dev mode) + # Port 443 for admin interface (production mode) or nginx reverse proxy + networking.firewall.allowedTCPPorts = + [ cfg.serverPort ] ++ + (optionals cfg.devMode [ cfg.adminPort ]) ++ + (optionals (cfg.nginx.enable || !cfg.devMode) [ 443 ]); + + # Add useful packages + environment.systemPackages = with pkgs; [ + nodejs_22 + nodePackages.pnpm + postgresql + (writeShellScriptBin "lamassu-register-user" '' + export NODE_PATH="${cfg.package}/node_modules:${cfg.package}/packages/server/node_modules" + export DATABASE_URL="postgresql://${cfg.database.user}:${cfg.database.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="${cfg.database.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: http://localhost:${toString cfg.serverPort}" + echo "Admin: http://localhost:${toString cfg.adminPort}" + '') + ]; + }; +}