The disk image was ~6.2 GiB of closure, largely desktop/multimedia baggage a single-purpose Electron kiosk never uses. Cut the clearly-unused stacks: - services.speechd off → drops speech-dispatcher's espeak-ng + mbrola voices (~1 GB text-to-speech). An ATM does not talk. - v4l-utils built withGUI=false → drops the entire Qt6 stack (~0.5 GB) that only backed the qv4l2 GUI; the v4l2-ctl CLI we actually use for the camera stays. - documentation off (man/info/NixOS manual) — nobody reads them on a kiosk. Closure 6.2 → 5.0 GiB. The remaining bulk is electron's own runtime (gtk4/ gstreamer/pipewire, unavoidable), mesa+llvm (GPU), and linux-firmware — those need heavier / riskier work to touch. Distribute the image as .img.zst. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
228 lines
6.5 KiB
Nix
228 lines
6.5 KiB
Nix
# bitSpire ATM NixOS Configuration
|
|
# Base system configuration for ATM kiosk
|
|
|
|
{ config, lib, pkgs, pkgs-unstable, ... }:
|
|
|
|
{
|
|
# System basics
|
|
system.stateVersion = "24.05";
|
|
|
|
# ── Image slimming (bitspire#70 sizing) ──────────────────────────────
|
|
# This is a single-purpose Electron kiosk; strip the desktop/multimedia
|
|
# baggage NixOS pulls in by default so the disk image stays lean.
|
|
# - speechd: text-to-speech (speech-dispatcher → espeak-ng → mbrola, ~1GB).
|
|
# An ATM does not talk.
|
|
# - documentation: man/info/NixOS manual — no one reads them on a kiosk.
|
|
services.speechd.enable = lib.mkForce false;
|
|
documentation.enable = false;
|
|
documentation.nixos.enable = false;
|
|
|
|
# Networking
|
|
networking = {
|
|
hostName = "bitspire";
|
|
|
|
# Use NetworkManager for easy WiFi configuration
|
|
networkmanager.enable = true;
|
|
|
|
# Firewall - minimal exposure
|
|
firewall = {
|
|
enable = true;
|
|
allowedTCPPorts = [ ]; # ATM initiates all connections
|
|
allowedUDPPorts = [ 51820 ]; # WireGuard
|
|
};
|
|
|
|
# WireGuard VPN tunnel to VPS for remote SSH access
|
|
# IP address set per-machine in hardware/*.nix via networking.wireguard.interfaces.wg0.ips
|
|
wireguard.interfaces.wg0 = {
|
|
listenPort = 51820;
|
|
privateKeyFile = "/var/lib/wireguard/wg0.key";
|
|
|
|
peers = [{
|
|
publicKey = "R6uB4o5ELEKEHCvK+llRYbzdkZGDHegVmS0f08aRtWM=";
|
|
endpoint = "170.75.161.21:51820";
|
|
allowedIPs = [ "10.0.0.0/24" ];
|
|
persistentKeepalive = 25;
|
|
}];
|
|
};
|
|
};
|
|
|
|
# Timezone - set to your location
|
|
time.timeZone = "America/Guatemala";
|
|
|
|
# Locale
|
|
i18n.defaultLocale = "en_US.UTF-8";
|
|
|
|
# Users
|
|
users.groups.bitspire = { };
|
|
users.users.bitspire = {
|
|
isNormalUser = true;
|
|
group = "bitspire";
|
|
description = "bitSpire ATM";
|
|
home = "/home/bitspire";
|
|
extraGroups = [
|
|
"wheel" # For admin access
|
|
"video" # GPU access
|
|
"audio" # Sound
|
|
"dialout" # Serial ports
|
|
"plugdev" # USB devices
|
|
"networkmanager" # Network config
|
|
];
|
|
# No password - kiosk mode
|
|
initialPassword = "bitspire"; # pragma: allowlist secret
|
|
};
|
|
|
|
# Kiosk display configuration
|
|
services.xserver = {
|
|
enable = true;
|
|
|
|
# No desktop environment - just the ATM app
|
|
desktopManager.xterm.enable = false;
|
|
|
|
# Basic window manager for Electron
|
|
windowManager.openbox.enable = true;
|
|
|
|
# Disable screen blanking
|
|
serverFlagsSection = ''
|
|
Option "BlankTime" "0"
|
|
Option "StandbyTime" "0"
|
|
Option "SuspendTime" "0"
|
|
Option "OffTime" "0"
|
|
'';
|
|
|
|
# Intel driver
|
|
videoDrivers = [ "modesetting" ];
|
|
};
|
|
|
|
# Display manager - auto-login (top-level since NixOS 24.11+)
|
|
services.displayManager.autoLogin = {
|
|
enable = true;
|
|
user = "bitspire";
|
|
};
|
|
|
|
# Audio (for transaction sounds)
|
|
security.rtkit.enable = true;
|
|
services.pipewire = {
|
|
enable = true;
|
|
alsa.enable = true;
|
|
pulse.enable = true;
|
|
};
|
|
|
|
# System packages
|
|
environment.systemPackages = with pkgs; [
|
|
# System utilities
|
|
htop
|
|
vim
|
|
git
|
|
curl
|
|
wget
|
|
|
|
# Hardware debugging
|
|
usbutils
|
|
pciutils
|
|
lsof
|
|
|
|
# Serial port tools
|
|
minicom
|
|
screen
|
|
|
|
# For the Electron app
|
|
pkgs-unstable.electron
|
|
|
|
# Node.js for the application
|
|
pkgs-unstable.nodejs_22
|
|
|
|
# Camera support. v4l-utils' default build drags in the whole Qt6 stack
|
|
# for its qv4l2 GUI (~0.5GB) — we only ever use the v4l2-ctl CLI, so drop
|
|
# the GUI.
|
|
(v4l-utils.override { withGUI = false; })
|
|
fswebcam
|
|
|
|
# ATM operations
|
|
sqlite
|
|
(writeShellScriptBin "atm-transactions" (builtins.readFile ./atm-transactions.sh))
|
|
];
|
|
|
|
# Enable SSH for remote administration
|
|
services.openssh = {
|
|
enable = true;
|
|
settings = {
|
|
PasswordAuthentication = false;
|
|
PermitRootLogin = "prohibit-password";
|
|
};
|
|
};
|
|
|
|
# Root SSH key access
|
|
users.users.root.openssh.authorizedKeys.keys = [
|
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIES8I43AgXppATvtBqnUycakMMs68T2J52cTwNt6qPak padreug@gizmo"
|
|
];
|
|
|
|
# Auto-updates (optional - disabled by default for stability)
|
|
# system.autoUpgrade.enable = false;
|
|
|
|
# pragma: allowlist secret
|
|
# Ensure WireGuard private key directory exists with correct permissions
|
|
system.activationScripts.wireguard-key = ''
|
|
mkdir -p /var/lib/wireguard
|
|
chmod 700 /var/lib/wireguard
|
|
if [ -f /var/lib/wireguard/wg0.key ]; then
|
|
chmod 600 /var/lib/wireguard/wg0.key
|
|
fi
|
|
'';
|
|
|
|
# In-place rename migration: lamassu user → bitspire user.
|
|
# Runs after `users` activation so the bitspire user exists with its UID.
|
|
# Idempotent: re-running on an already-migrated system is a chown no-op.
|
|
# Leaves /home/lamassu in place as evidence — operator can `rm -rf` after
|
|
# confirming bitspire works.
|
|
system.activationScripts.bitspire-user-migration = {
|
|
deps = [ "users" ];
|
|
text = ''
|
|
# SSH key migration: copy authorized_keys to /home/bitspire if missing,
|
|
# so the dev box can still SSH in as bitspire after the rename.
|
|
if [ -f /home/lamassu/.ssh/authorized_keys ] \
|
|
&& [ ! -f /home/bitspire/.ssh/authorized_keys ]; then
|
|
mkdir -p /home/bitspire/.ssh
|
|
cp /home/lamassu/.ssh/authorized_keys /home/bitspire/.ssh/authorized_keys
|
|
chown -R bitspire:bitspire /home/bitspire/.ssh
|
|
chmod 700 /home/bitspire/.ssh
|
|
chmod 600 /home/bitspire/.ssh/authorized_keys
|
|
fi
|
|
|
|
# Data dir ownership: state.db / .env / branding/ may still be owned by
|
|
# the now-removed lamassu UID. Reset every boot — cheap no-op once done.
|
|
if [ -d /var/lib/bitspire ]; then
|
|
chown -R bitspire:bitspire /var/lib/bitspire
|
|
fi
|
|
'';
|
|
};
|
|
|
|
# Journal configuration
|
|
services.journald = {
|
|
extraConfig = ''
|
|
SystemMaxUse=100M
|
|
MaxRetentionSec=1week
|
|
'';
|
|
};
|
|
|
|
# Nix settings
|
|
nix = {
|
|
settings = {
|
|
experimental-features = [ "nix-command" "flakes" ];
|
|
auto-optimise-store = true;
|
|
};
|
|
|
|
# Garbage collection — daily at 03:30, 30 min before the 04:00 auto-
|
|
# upgrade so each upgrade attempt gets the freshest headroom. 15GB
|
|
# eMMC + ~7GB closure means cross-release upgrades are always tight;
|
|
# weekly was leaving up to a week of generations stacked when the
|
|
# upgrade ran (caught 2026-05-26 on the 24.05 → 24.11 attempt).
|
|
# persistent so a Sintra that was powered off at 03:30 still runs the
|
|
# GC on next boot rather than skipping until next week.
|
|
gc = {
|
|
automatic = true;
|
|
dates = "03:30";
|
|
options = "--delete-older-than 7d";
|
|
persistent = true;
|
|
};
|
|
};
|
|
}
|