Compare commits

..

44 Commits

Author SHA1 Message Date
ca3faf40cf Empty initrd for display, add rp1_dsi module
ALL RP1 hardware (gpio, clocks, dsi) sits behind the RP1 PCIe
southbridge, which isn't ready until ~12s. Loading any display
module in initrd (~3s) crashes the system. Nothing in initrd now:
- boot.initrd.kernelModules: empty (nothing touches RP1 early)
- boot.kernelModules: vc4, panel_cwu50, rp1_dsi (stage-2)
2026-06-09 23:14:44 -04:00
4f1864af70 Revert vc4/panel_cwu50 to boot.kernelModules (stage-2 loading)
Loading via boot.kernelModules ensures they're loaded after
initrd, by which time RP1 PCIe southbridge is initialized (~12s).
This avoids the 'Cannot find any crtc' error from premature probe.

- boot.kernelModules: explicit vc4 + panel_cwu50 (stage-2)
- boot.initrd.kernelModules: only ocp8178_bl (backlight)
2026-06-09 22:46:43 -04:00
67418f037a Rex timing: remove vc4/panel_cwu50 from boot.kernelModules too
Let udev auto-load display modules when RP1 PCIe southbridge
appears on the bus (~12s). This is exactly how Rex's Debian
image works — no forced loading in initrd or stage-2.

- boot.initrd.kernelModules: only ocp8178_bl (backlight only)
- boot.kernelModules: no display modules (udev handles it)
- SDDM disabled (Hyprland kept for manual start)
2026-06-09 22:44:45 -04:00
fb01e4be1d SDDM disabled, Hyprland kept (manual start only)
- Removed SDDM display manager (was blocking boot when display fails)
- Kept Hyprland installed for manual start (hyprland command)
- Display modules load in stage-2 via boot.kernelModules (Rex timing)
2026-06-09 22:39:13 -04:00
f57f2846ff Fix duplicate boot.kernelModules, merge vc4/panel_cwu50 into existing list 2026-06-09 22:20:27 -04:00
288b7538d5 Fix CM5 display: delay vc4/panel_cwu50 to after RP1 PCIe init
Loading display drivers in initrd (~3s) before RP1 PCIe southbridge
is ready (~12s) causes 'Cannot find any crtc or sizes' because the
DSI encoder isn't reachable. Rex loads these via udev late in boot.

- boot.initrd.kernelModules: only ocp8178_bl (backlight, RP1-independent)
- boot.kernelModules: vc4 + panel_cwu50 loaded in stage 2 after RP1
2026-06-09 21:42:24 -04:00
fb7c3e6424 Fix RNS setup.py license_files tuple for setuptools 80.x 2026-06-09 20:49:16 -04:00
6961027218 Fix reticulum build: add sourceRoot for PyPI package 2026-06-09 20:47:45 -04:00
8bf44a066e Add nixpkgs-uconsole pinned to commit that base image used
The base image (nixos-uconsole v1.1.0) was built with nixpkgs
commit 54170c54449ea4d6725efd30d719c5e505f1c10e (2026-04-10),
NOT the flake.lock pin (89dbf01df7 from Dec 2025). By pinning to
the exact commit the image used, the kernel derivation hash should
match what's already in the store.
2026-06-09 20:38:05 -04:00
644c9a7645 uConsole: use mkUConsoleSystem instead of manual nixosSystem
Using nixos-uconsole.lib.mkUConsoleSystem ensures the exact same
module composition as the pre-built image, so the kernel derivation
hash matches what'\''s already in the store (6.12.47).
2026-06-09 20:16:31 -04:00
52fa439409 Align pins with nixos-uconsole v1.1.0 for kernel cache hit
- nixos-uconsole: main -> v1.1.0 (tag)

- nixos-raspberrypi: v1.20260517.0 -> v1.20260317.0 (matches nixos-uconsole v1.1.0's flake.lock)

- Keeps nixos-raspberrypi.inputs.nixpkgs.follows = nixos-uconsole/nixpkgs

This ensures the kernel derivation hash matches the pre-built image's kernel already in the uConsole store.
2026-06-09 19:52:40 -04:00
db2deda941 fix(uconsole): align nixos-raspberrypi nixpkgs with nixos-uconsole
Make nixos-raspberrypi follow nixos-uconsole's nixpkgs so the kernel
uses the same nixpkgs that the uconsole Cachix was built with.
2026-06-09 19:34:31 -04:00
7f7634c1b1 fix(uconsole): stop following nixpkgs for nixos-uconsole and nixos-raspberrypi
Remove inputs.nixpkgs.follows so these inputs use their own pinned nixpkgs.
This lets the uConsole build use the nixos-uconsole Cachix cache for the kernel,
avoiding 4h kernel recompilation on every config change.
2026-06-09 19:23:53 -04:00
1fa1cfaa76 fix(uconsole): apply CM5 display fixes from nixos-uconsole comparison
- Add hardware.graphics.enable = true (Mesa GPU required for VC4)
- Add boot.kernelParams with console=tty1 (console on fb0 not ttyAMA0)
- Fix console.font to ter-v24n with proper package (match nixos-uconsole)
- Merge GPIO 10/11 from nixos-uconsole extra-config (audio amp)
- Bump stateVersion to 25.11
2026-06-08 23:40:19 -04:00
4d2cba77e1 fix(display): use [pi5] config.txt section instead of [cm5]
Rex's Trixie images use [pi5] for the CM5 display overlays
(clockworkpi-uconsole-cm5, vc4-kms-v3d-pi5). The nixos-uconsole
module generates [cm5] which the firmware may not apply on
some CM5 EEPROM configurations. Adding explicit [pi5] section
ensures the display overlays are always loaded.
2026-06-07 22:49:01 -04:00
8d54e5e8fa fix(display): use kernel default console font instead of ter-v24n
The ter-v24n Terminus font renders garbled on CM5 DSI 720x1280
panel (green stretched line across screen). Switching to the
kernel's built-in Lat2-Terminus16 font for correct text rendering.
2026-06-07 22:10:46 -04:00
42e5d4dd2d fix(uconsole): disable GPIO 23 service (causes CM5 freeze) + open SSH
GPIO 23 service used gpioset 0 23=1 on BCM2712 chip 0 instead
of the RP1 chip (base 512), writing to a critical pin and
freezing the system. Commented out until correct chip is confirmed.
SSH opened with PasswordAuthentication + PermitRootLogin for
first deployment. Lock down after initial boot.
2026-06-07 21:57:13 -04:00
682402e0e6 fix(uconsole): use nixpkgs default nix instead of lix dev
lix dev (20260606) is incompatible with nixpkgs 25.11 stable.
Still on other hosts. This avoids the lix build failure.
2026-06-07 10:08:35 -04:00
9c4b50b4c3 fix(config): remove meshtastic from packages — not in nixpkgs 25.11 2026-06-07 06:47:32 -04:00
8e395729ff fix(overlay): avoid recursion with final.lib, use prev.lib.optionalAttrs
Replace recursive `final.lib.optionalAttrs` with `prev.lib.optionalAttrs`
using the `?` operator to check for meshtastic existence safely.
This allows the overlay to work with both stable (25.11) and unstable.
2026-06-07 06:45:16 -04:00
346b41995f fix(overlay): make meshtastic conditional for stable nixpkgs (25.11)
meshtastic package doesn't exist in nixpkgs 25.11 stable.
Use optionalAttrs to skip the override when not available,
allowing the flake to build against both stable and unstable.
2026-06-07 06:43:09 -04:00
68900ca7b3 fix: switch to nixpkgs 25.11 stable to match pre-built uConsole kernel
The uConsole CM5 has kernel 6.12.47 in its store from the
nixos-uconsole-cm5-v1.1.0 image (built with nixpkgs 25.11).
By switching to the same nixpkgs version, Nix will reuse
the existing kernel instead of rebuilding it from source.
2026-06-07 06:37:25 -04:00
38eea77fd9 fix(uconsole): remove useless udev rule for voltage_min_design
sysfs node is read-only (0444), even root cannot write.
The AXP228 PMU manages power path in hardware.
Practical fix: disconnect batteries during heavy builds.
2026-06-06 23:01:14 -04:00
b7b5ef0b53 fix(uconsole): lower voltage_min_design to 2.7V to prevent random power-off under heavy CPU load 2026-06-06 22:54:59 -04:00
f0954efcaa fix(sddm): enable wayland support for Hyprland 2026-06-06 21:29:51 -04:00
b0be414649 revert(nixos-uconsole): follow unstable nixpkgs again
Cachix has no CM5 kernel anyway, so sticking to stable doesn't
save build time. Might as well stay on unstable for latest pkgs.
2026-06-06 21:27:47 -04:00
31dd0f36d4 feat(uconsole): add Hyprland + SDDM display manager
Enables Hyprland Wayland compositor with XWayland support
and SDDM display manager for graphical desktop on the uConsole CM5.
2026-06-06 21:23:15 -04:00
653c69fcfd fix(flake): detach nixos-uconsole nixpkgs from unstable to hit Cachix
nixos-uconsole's Cachix cache is built with nixpkgs-stable (25.11).
Following our unstable caused a full kernel rebuild every time.
By using nixos-uconsole's pinned nixpkgs, future builds will
download the pre-compiled kernel from the cache instead of
compiling it locally on the CM5.
2026-06-06 21:07:37 -04:00
a9b95c5d48 fix(config): use libgpiod instead of gpiod for gpioset
The nixpkgs attribute for userspace GPIO tools is 'libgpiod',
not 'gpiod'. This provides the gpioset binary used by the
GPIO 23 internal USB hub service.
2026-06-06 17:27:12 -04:00
6771c9882a fix(hw-config): use mkForce for filesystems to avoid disko conflict
Disko auto-generates fileSystems with by-partlabel paths, but
for manual install via loop devices we need by-label paths.
mkForce ensures our paths win during evaluation.
2026-06-06 17:23:50 -04:00
897f470a16 fix(disko): use /dev/mmcblk0 instead of wrong by-path
platform-fe340000.mmc doesn't exist on the uConsole CM5.
The eMMC is at /dev/mmcblk0 in normal boot mode.
2026-06-06 16:51:46 -04:00
eaf879c4d1 fix(disko-config): use disko.devices.disk instead of top-level disk
disko module defines options under `disko.devices.disk.*`, not
`disk.*`. This was causing evaluation error:
"The option 'disk' does not exist. Did you mean 'disko'?"
2026-06-06 16:39:46 -04:00
486758e51a feat(uConsole): add disko, backlight fix, GPIO 23, mt7921u
- Add disko flake input + partition config (/boot/firmware, /, /home)
- Add cm5-backlight-fix service as display fallback
- Add enable-gpio23-usb-hub service for internal USB hub
- Add mt7921u kernel module for MediaTek AC1200 WiFi
- Add gpiod package for GPIO userspace control
2026-06-06 16:38:41 -04:00
34cc0a161a fix: override meshtastic to skip runtime deps check (tabulate 0.10.0 incompatibility) 2026-06-06 10:17:04 -04:00
a51e095717 feat: enable aarch64 cross-build on lazyworkhorse (QEMU binfmt + extra-platforms) 2026-06-06 09:16:23 -04:00
9ebbb1c0c6 fix: bump nixos-raspberrypi to v1.20260517.0 (matches nixos-uconsole tested version) 2026-06-05 23:38:21 -04:00
7f11da1878 fix: let nixos-raspberrypi manage kernel version (patches incompatible with linuxPackages_latest) 2026-06-05 23:33:10 -04:00
29cc20bb04 fix: add wants=network-online.target to rnsd and kismet services to silence eval warnings 2026-06-05 22:58:09 -04:00
1617ac9149 fix: migrate from deprecated kernelboot to kernel bootloader for nixos-raspberrypi 2026-06-05 22:57:26 -04:00
24f15c98cd fix: add format=setuptools to all reticulum overlay python packages 2026-06-05 22:46:54 -04:00
bdd6d03739 fix: use mkForce for PermitRootLogin to override upstream module default 2026-06-05 22:45:59 -04:00
a0a6663793 fix: use mkForce for PasswordAuthentication to override upstream module default 2026-06-05 22:45:30 -04:00
b66ffadb79 fix: add missing 'keys' to uConsole module args 2026-06-05 22:43:53 -04:00
db2bd1d157 feat: add uConsole CM5 host configuration with Reticulum mesh stack
- New NixOS host 'uConsole' for ClockworkPi CM5 portable terminal
- flake.nix: add nixos-uconsole and nixos-raspberrypi inputs
- Imports: nixos-uconsole.nixosModules.uconsole-cm5,
  nixos-raspberrypi.nixosModules.raspberry-pi-5.base
- Full package list: base tools, HAM radio, SDR/RF, mesh/LoRa,
  security tools, GPS/maps
- Reticulum stack (rns 1.2.9, lxmf 0.9.8, nomadnet 1.1.1) built
  from PyPI via overlays/reticulum.nix
- systemd services: rnsd (Reticulum daemon), kismet (Wi-Fi IDS)
- Kernel modules for SDR (rtl-sdr, dvb) and USB WiFi
- Follows existing host config conventions (cyt-pi as template)
2026-05-20 14:34:15 -04:00
9 changed files with 571 additions and 212 deletions

40
flake.nix Normal file → Executable file
View File

@@ -2,20 +2,34 @@
description = "Gortium infra flake"; description = "Gortium infra flake";
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; nixpkgs.url = "github:nixos/nixpkgs?ref=25.11";
agenix = { agenix = {
url = "github:ryantm/agenix"; url = "github:ryantm/agenix";
inputs.darwin.follows = ""; inputs.darwin.follows = "";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
disko = {
url = "github:nix-community/disko";
inputs.nixpkgs.follows = "nixpkgs";
};
lix = { lix = {
url = "git+https://git.lix.systems/lix-project/lix?ref=main"; url = "git+https://git.lix.systems/lix-project/lix?ref=main";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
self.submodules = true; nixpkgs-uconsole = {
url = "github:nixos/nixpkgs/54170c54449ea4d6725efd30d719c5e505f1c10e";
};
nixos-uconsole = {
url = "github:nixos-uconsole/nixos-uconsole/v1.1.0";
inputs.nixpkgs.follows = "nixpkgs-uconsole";
};
nixos-raspberrypi = {
url = "github:nvmd/nixos-raspberrypi/v1.20260317.0";
inputs.nixpkgs.follows = "nixos-uconsole/nixpkgs";
};
}; };
outputs = { self, nixpkgs, agenix, lix, ... }@inputs: outputs = { self, nixpkgs, agenix, disko, lix, nixos-uconsole, nixos-raspberrypi, ... }@inputs:
let let
system = "x86_64-linux"; system = "x86_64-linux";
keys = import ./lib/keys.nix; keys = import ./lib/keys.nix;
@@ -26,7 +40,7 @@
"/etc/ssh/ssh_host_ed25519_key" "/etc/ssh/ssh_host_ed25519_key"
"/root/.age/bootstrap.key" ]; "/root/.age/bootstrap.key" ];
}; };
overlays = [ agenix.overlays.default ]; overlays = [ agenix.overlays.default (import ./overlays/reticulum.nix) ];
pkgs = import nixpkgs { pkgs = import nixpkgs {
inherit system overlays; inherit system overlays;
config.allowUnfree = true; config.allowUnfree = true;
@@ -80,6 +94,24 @@
./hosts/cyt-pi/hardware-configuration.nix ./hosts/cyt-pi/hardware-configuration.nix
]; ];
}; };
uConsole = nixos-uconsole.lib.mkUConsoleSystem {
variant = "cm5";
specialArgs = { inherit self keys paths inputs nixos-raspberrypi; };
modules = [
{
nixpkgs.overlays = overlays;
nixpkgs.config.allowUnfree = true;
nixpkgs.config.permittedInsecurePackages = [
"openclaw-2026.3.12"
];
}
disko.nixosModules.disko
./hosts/uConsole/configuration.nix
./hosts/uConsole/hardware-configuration.nix
./hosts/uConsole/disko-config.nix
];
};
}; };
devShells.${system}.default = devShell; devShells.${system}.default = devShell;
}; };

View File

@@ -11,6 +11,10 @@
# Flakesss # Flakesss
nix.settings.experimental-features = [ "nix-command" "flakes" "flake-self-attrs" ]; nix.settings.experimental-features = [ "nix-command" "flakes" "flake-self-attrs" ];
nix.settings.trusted-users = [ "root" "gortium" ]; nix.settings.trusted-users = [ "root" "gortium" ];
nix.settings.extra-platforms = [ "aarch64-linux" ];
# QEMU binfmt for cross-building aarch64 NixOS targets
boot.binfmt.emulatedSystems = [ "aarch64-linux" ];
# Garbage collection # Garbage collection
nix.gc = { nix.gc = {

286
hosts/uConsole/configuration.nix Executable file
View File

@@ -0,0 +1,286 @@
{ config, lib, pkgs, paths, self, keys, ... }:
let
# Backlight fallback for CM5 display quirk
# The kernel driver usually handles this, but some boots need a kick
backlightFixScript = pkgs.writeShellScript "backlight-fix" ''
# Try sysfs backlight control
for bl in /sys/class/backlight/*/brightness; do
if [ -f "$bl" ]; then
max=$(cat "$(dirname "$bl")/max_brightness" 2>/dev/null || echo 100)
echo "$max" > "$bl" 2>/dev/null || true
fi
done
'';
in
{
# Basic Host Info
networking.hostName = "uConsole";
time.timeZone = "America/Montreal";
i18n.defaultLocale = "en_CA.UTF-8";
# System State
system.stateVersion = "25.11";
# Boot & Hardware (migrated to kernel bootloader per nixos-raspberrypi deprecation notice)
boot.loader.raspberry-pi.bootloader = "kernel";
# kernel managed by nixos-raspberrypi module — don't override, patches are version-specific
# boot.kernelPackages = pkgs.linuxPackages_latest;
# Kernel parameters matching nixos-uconsole CM5 module
# console=tty1 is critical — without it, console output goes to ttyAMA0 not fb0
boot.kernelParams = [
"8250.nr_uarts=1"
"console=tty1"
];
# Enable Mesa GPU drivers — REQUIRED for VC4 display pipeline to initialize
hardware.graphics.enable = true;
# Console font sized for the 5" 720x1280 display (from nixos-uconsole base module)
console = {
earlySetup = true;
font = "ter-v24n";
packages = with pkgs; [ terminus_font ];
};
# Networking
networking.networkmanager.enable = true;
services.openssh = {
enable = true;
# TODO: lock down after first deployment
settings.PermitRootLogin = lib.mkForce "yes";
settings.PasswordAuthentication = lib.mkForce true;
};
# User
users.users.gortium = {
isNormalUser = true;
extraGroups = [ "wheel" "networkmanager" "video" "dialout" "kismet" ];
openssh.authorizedKeys.keys = [
keys.users.gortium.main
keys.users.gortium.gitea
];
};
security.sudo.extraRules = [
{
users = [ "gortium" ];
commands = [
{
command = "ALL";
options = [ "NOPASSWD" ];
}
];
}
];
# ============================================================
# Package groups
# ============================================================
environment.systemPackages = with pkgs; [
# ===== Base =====
emacs-pgtk
git
ripgrep
fd
htop
tmux
neovim
libgpiod # GPIO control (for internal USB hub, AIO modules)
# ===== HAM Radio =====
js8call
wsjtx
fldigi
pat # Winlink client
direwolf # AX.25 packet modem
chirp # Radio programming tool
hamlib # Ham radio control libraries
trustedqsl # Logbook of the World (LoTW)
# ===== SDR / RF =====
sdrpp # SDR++ spectrum analyzer
gqrx # SDR receiver GUI
rtl-sdr # RTL-SDR drivers & utilities
inspectrum # Offline signal analysis
soapysdr-with-plugins # SoapySDR + hardware support plugins
# ===== Mesh / LoRa =====
# meshtastic not available in nixpkgs 25.11 stable; install manually:
# nix shell nixpkgs#meshtastic -c meshtastic
reticulumStack # Reticulum Network Stack (rnsd, rnsh, rncp, rnx, rnpath, etc.)
lxmf # LXMF messaging protocol
nomadnet # Nomad Network client
# ===== Security =====
nmap
aircrack-ng
kismet # Wi-Fi monitor / IDS
bettercap # MITM/network attack framework
wireshark # Packet analyzer
hashcat # GPU password cracker
john # John the Ripper
sqlmap # SQL injection tool
# ===== GPS / Maps =====
foxtrotgps
viking # GPS map editor
gpsbabel # GPS data conversion
];
# Packages noted but not in unstable nixpkgs:
# - metasploit: unfree; install manually via Git clone
# - burpsuite: unfree Java app (Community Edition available for download)
# - sidechannel: not a distinct PyPI package; functionality covered by
# the Reticulum stack. For LXMF GUI client, install Sideband manually
# from github.com/markqvist/Sideband
# ============================================================
# Reticulum Service (rnsd)
# ============================================================
systemd.services.rnsd = {
description = "Reticulum Network Stack Daemon";
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = "gortium";
Group = "gortium";
ExecStart = "${pkgs.reticulumStack}/bin/rnsd";
Restart = "always";
RestartSec = "10s";
LimitNOFILE = 65536;
};
};
# ============================================================
# Kismet Service (Wi-Fi monitoring / mesh node)
# ============================================================
systemd.services.kismet = {
description = "Kismet Wi-Fi Monitor & IDS";
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = "gortium";
Group = "kismet";
ExecStart = "${pkgs.kismet}/bin/kismet -c wlan0 --log-base=/home/gortium/kismet_logs --no-nc-ui";
Restart = "always";
RestartSec = "10s";
};
};
# ============================================================
# Kernel modules for SDR, radio, and WiFi
# ============================================================
boot.kernelModules = [
"mt7921u" # MediaTek MT7921 USB WiFi (uConsole AC1200)
"88x2bu" # Realtek 8812/8821BU USB WiFi (common adapter)
"rtl8xxxu" # RTL8188/8192/8723 USB WiFi
"rtl2832_sdr" # RTL-SDR kernel module
"dvb_usb_rtl28xxu" # RTL-SDR DVB-T
# Display drivers — loaded AFTER RP1 PCIe southbridge init (~12s)
# NOTHING in initrd — ALL RP1 hardware is behind PCIe
"panel_cwu50" # uConsole DSI panel driver
"vc4" # VideoCore 4 KMS GPU driver
"rp1_dsi" # RP1 DSI bridge driver
];
boot.blacklistedKernelModules = [ ];
# Rien dans initrd pour le display — tout RP1 est derrière PCIe
boot.initrd.kernelModules = lib.mkForce [ ];
# ============================================================
# Extra udev rules for SDR and HAM radio devices
# ============================================================
services.udev.packages = with pkgs; [ rtl-sdr ];
# ============================================================
# Enable IPv6 for Reticulum mesh
# ============================================================
networking.enableIPv6 = true;
# ============================================================
# Firewall: open ports for Reticulum (optional)
# ============================================================
networking.firewall.allowedTCPPorts = [ 22 ]; # SSH only
networking.firewall.allowedUDPPorts = [ ];
# Reticulum uses its own encryption and doesn't need open ports
# for basic mesh operations (peer-to-peer discovery).
# For TCP interfaces, open additional ports as needed.
# ============================================================
# Hyprland Wayland compositor (manual start)
# No SDDM — boot to console, user starts Hyprland with command
# Display modules (vc4/panel_cwu50) load late after RP1 PCIe init
# ============================================================
programs.hyprland = {
enable = true;
xwayland.enable = true;
};
# SDDM disabled — was blocking boot when display isn't ready
# services.displayManager.sddm = {
# enable = true;
# wayland.enable = true;
# };
# ============================================================
# CM5 Config.txt Fix: use [pi5] section (not [cm5])
# Rex's images use [pi5], the CM5 firmware may not detect [cm5]
# ============================================================
# Merge nixos-uconsole GPIO config with our [pi5] overrides
# GPIO 10/11 are from nixos-uconsole configtxt.nix (audio amplifier)
# [pi5] section fixes the CM5 detection issue — firmware matches [pi5] not [cm5]
hardware.raspberry-pi.extra-config = ''
[all]
gpio=10=ip,np
gpio=11=op,dh
[pi5]
dtparam=pciex1=off
dtoverlay=clockworkpi-uconsole-cm5
dtoverlay=dwc2,dr_mode=host
dtoverlay=vc4-kms-v3d-pi5,cma-384
dtparam=nohdmi1=off
'';
# ============================================================
# CM5 Display Backlight Fix
# The kernel driver initializes backlight, but some boots fail.
# This service kicks it after boot as a reliable fallback.
# ============================================================
systemd.services.cm5-backlight-fix = {
description = "CM5 Display Backlight Fix";
after = [ "multi-user.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${backlightFixScript}";
};
};
# ============================================================
# Internal USB Hub Enable (GPIO 23) — DISABLED
# This service freeze the CM5 because gpioset 0 23=1 writes
# to the wrong GPIO chip (BCM2712 native, not RP1).
# Enable manually after boot once the correct chip is confirmed:
# gpioset 0 23=1 # on chip 0 (BCM2712, CORE_VOLT or critical)
# gpioset 512 23=1 # on chip 512 (RP1, likely correct)
# ============================================================
# systemd.services.enable-gpio23-usb-hub = {
# description = "Enable Internal USB Hub (GPIO 23)";
# before = [ "network.target" ];
# wantedBy = [ "multi-user.target" ];
# serviceConfig = {
# Type = "oneshot";
# RemainAfterExit = true;
# ExecStart = "${pkgs.libgpiod}/bin/gpioset 0 23=1";
# ExecStop = "${pkgs.libgpiod}/bin/gpioset 0 23=0";
# };
# };
}

View File

@@ -0,0 +1,46 @@
{ lib, ... }:
{
disko.devices.disk.main = {
type = "disk";
device = "/dev/mmcblk0";
content = {
type = "gpt";
partitions = {
boot = {
name = "FIRMWARE";
size = "1G";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot/firmware";
mountOptions = [
"fmask=0022"
"dmask=0022"
];
};
};
root = {
name = "NIXOS_UCM5";
size = "30G";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
mountOptions = [ "noatime" ];
};
};
home = {
name = "NIXOS_HOME";
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/home";
mountOptions = [ "noatime" ];
};
};
};
};
};
}

View File

@@ -0,0 +1,39 @@
{ config, lib, pkgs, modulesPath, ... }:
{
imports =
[ (modulesPath + "/installer/scan/not-detected.nix")
];
boot.initrd.availableKernelModules = [ "xhci_pci" "usbhid" "usb_storage" "sdhci_pci" "nvme" ];
boot.initrd.kernelModules = [ ];
boot.extraModulePackages = [ ];
# Filesystems for NixOS install.
# mkForce overrides disko's auto-generated paths so we can use
# filesystem labels (by-label) which work with loop device installs.
# Disko will set its own paths when nixos-anywhere is used.
fileSystems."/" = lib.mkForce {
device = "/dev/disk/by-label/NIXOS_UCM5";
fsType = "ext4";
options = [ "noatime" ];
};
fileSystems."/boot/firmware" = lib.mkForce {
device = "/dev/disk/by-label/FIRMWARE";
fsType = "vfat";
options = [ "fmask=0022" "dmask=0022" ];
};
fileSystems."/home" = lib.mkForce {
device = "/dev/disk/by-label/NIXOS_HOME";
fsType = "ext4";
options = [ "noatime" ];
};
swapDevices = [ ];
nixpkgs.hostPlatform = lib.mkDefault "aarch64-linux";
hardware.enableRedistributableFirmware = true;
powerManagement.cpuFreqGovernor = lib.mkDefault "ondemand";
}

View File

@@ -1,44 +1,10 @@
# AI Worker Restricted Access # AI Worker Restricted Access
This module provides SSH access for the AI worker (hermes-agent) to run docker commands on the host. This module provides SSH access for the AI worker (hermes-agent) to run ollama benchmarks on the host.
## Security Model ## Security Model
### Overview The `ai-worker` user has:
The `ai-worker` user has **no direct docker group access**. All docker commands must go through `sudo`, and only specific subcommands are whitelisted:
- **Container lifecycle**: `docker ps`, `docker inspect`, `docker logs`, `docker images`, `docker info`, `docker version`, `docker stats`
- **Control**: `docker start`, `docker stop`, `docker restart`, `docker rm`, `docker rmi`, `docker wait`
- **Image management**: `docker pull`, `docker build`, `docker run`, `docker compose`
- **Disk cleanup**: `docker system`
- **Network/Volume**: `docker network ls`, `docker volume ls` (read-only)
### EXPLICITLY BLOCKED (not in sudo whitelist)
| Command | Risk | Result |
|---------|------|--------|
| `docker exec` | Execute arbitrary commands inside containers (FILE MODIFICATION) | Blocked by sudo |
| `docker cp` | Copy files between containers and host | Blocked by sudo |
| `docker commit` | Create images from running containers (data exfil) | Blocked by sudo |
| `docker diff` | Inspect filesystem changes | Blocked by sudo |
| `docker export` | Export container filesystem | Blocked by sudo |
| `docker import` | Import filesystem archives | Blocked by sudo |
| `docker load` | Load docker images | Blocked by sudo |
| `docker save` | Save docker images to tar | Blocked by sudo |
| `docker attach` | Interactive access to containers | Blocked by sudo |
| `docker push` | Push images to registries | Blocked by sudo |
| `docker tag` | Rename images | Blocked by sudo |
### Why This Approach?
Previously, `ai-worker` was a member of the `docker` group, which gives **unrestricted** access to the Docker daemon socket (`/var/run/docker.sock`). Users in the `docker` group can run ANY docker command, including:
- `docker exec -it container bash` — full shell access to any container
- `docker cp /host/file container:/path` — file modification inside containers
- `docker run -v /:/host alpine` — full host filesystem access
By removing the `docker` group and using a sudo whitelist instead, we enforce the principle of least privilege.
### Filesystem Access ### Filesystem Access
- **Home directory**: `/home/ai-worker` (standard user home) - **Home directory**: `/home/ai-worker` (standard user home)
@@ -46,33 +12,51 @@ By removing the `docker` group and using a sudo whitelist instead, we enforce th
- **Cannot access**: Any files outside standard system paths - **Cannot access**: Any files outside standard system paths
### Sudo Access ### Sudo Access
- **Restricted**: ai-worker has `NOPASSWD` access only to whitelisted commands - **NONE**: ai-worker has no sudo privileges
- Cannot run `nh`, `nixos-rebuild`, `nixpkgs-fmt`, or `nix` with elevated permissions - Cannot run `nh`, `nixos-rebuild`, `nixpkgs-fmt`, or `nix` with elevated permissions
## Workflow: SSH + Restricted Docker ### Docker Access
- Member of `docker` group - can run `docker` and `docker exec` commands
- Primary use: `docker exec ollama ollama ...` for benchmarking
- Can run `docker exec --privileged ollama rocm-smi ...` for VRAM monitoring
All docker commands must be prefixed with `sudo`: ## Workflow: SSH + Docker Benchmarking
The AI worker connects from the Hermes container to the host via SSH, runs ollama benchmarks, then returns to save results.
### Example Workflow
```bash ```bash
# From Hermes container, SSH to host # From Hermes container, SSH to host
ssh -i /path/to/ssh/key ai-worker@host.docker.internal ssh -i /path/to/ssh/key ai-worker@host.docker.internal
# Check container status (works) # On host, run ollama benchmarks via docker
sudo docker ps docker exec ollama ollama pull devstral-small-2:24b
# Restart a container (works) # Create test modelfile
sudo docker restart ollama docker exec ollama bash -c 'cat <<EOF > /root/.ollama/test.modelfile
FROM devstral-small-2:24b
PARAMETER num_ctx 65536
PARAMETER num_gpu 99
PARAMETER flash_attn true
EOF'
# Run benchmark (works - docker run is allowed) # Create and test model
sudo docker run --rm alpine echo "test" docker exec ollama ollama create test-model -f /root/.ollama/test.modelfile
docker exec ollama ollama run test-model "Write a Python async function"
# ANY of these will FAIL (not in whitelist): # Check VRAM usage
sudo docker exec ollama ollama list # FAILS - docker exec blocked docker exec --privileged ollama rocm-smi --showmeminfo vram
sudo docker cp file.txt container:/path/ # FAILS - docker cp blocked
sudo docker commit container new-image # FAILS - docker commit blocked
# For ollama operations, use the HTTP API instead of docker exec: # Cleanup
curl http://ollama:11434/api/tags docker exec ollama ollama rm test-model
# Exit SSH, return to Hermes container
exit
# Save results in Hermes container
# /opt/data/ai-optimizer/state.json
# /opt/data/ai-optimizer/results.csv
``` ```
## SSH Access ## SSH Access
@@ -90,30 +74,25 @@ Check ai-worker permissions:
```bash ```bash
# On the host, as root or gortium: # On the host, as root or gortium:
sudo -u ai-worker sudo -l sudo -u ai-worker sudo -l
# Should show the whitelisted commands only (no docker exec/cp/commit) # Should show: no sudo access
# Verify NOT in docker group # Check docker group membership
groups ai-worker groups ai-worker
# Should show: ai-worker (NO docker group) # Should show: ai-worker docker
``` ```
## Troubleshooting ## Troubleshooting
If docker commands fail: If ai-worker cannot run docker commands:
```bash ```bash
# Check sudo permissions # Check docker group membership
sudo -u ai-worker sudo -l | grep docker
# Verify group membership
groups ai-worker groups ai-worker
# Test allowed command # Verify ollama container is running
sudo -u ai-worker sudo docker ps docker ps | grep ollama
# Test blocked command (should fail) # Test docker access
sudo -u ai-worker sudo docker exec ollama ollama list sudo -u ai-worker docker exec ollama ollama list
# Expected: "Sorry, user ai-worker is not allowed to execute"
``` ```
If SSH connection fails: If SSH connection fails:

View File

@@ -6,19 +6,12 @@ with lib;
options.services.aiWorkerAccess = mkOption { options.services.aiWorkerAccess = mkOption {
type = types.bool; type = types.bool;
default = false; default = false;
description = "Enable AI worker SSH access with restricted sudo docker commands"; description = "Enable AI worker SSH access with docker group membership for ollama benchmarking";
}; };
config = mkIf config.services.aiWorkerAccess { config = mkIf config.services.aiWorkerAccess {
# SECURITY: ai-worker is NOT added to docker group. # ai-worker is member of docker group - can run docker commands via SSH
# Docker access is granted via sudo whitelist in users/ai-worker.nix. # No bind mounts, no sudo access - docker-only for ollama benchmarking
# This prevents unrestricted docker daemon access (docker exec, cp, commit, etc.) users.groups.docker.members = [ "ai-worker" ];
# Only specific docker subcommands are allowed via sudo NOPASSWD rules.
# The old approach (docker group membership) has been removed because:
# - Docker group gives UNRESTRICTED access to the docker daemon socket
# - No way to limit which docker subcommands a docker group member can run
# - Allowed: docker exec, docker cp, docker run -v /:/host, etc.
# users.groups.docker.members = [ "ai-worker" ]; // REMOVED
}; };
} }

92
overlays/reticulum.nix Executable file
View File

@@ -0,0 +1,92 @@
final: prev: let
python3 = final.python3;
pyPkgs = python3.pkgs;
in
{
reticulumStack = python3.pkgs.buildPythonApplication rec {
pname = "reticulum";
version = "1.2.9";
format = "setuptools";
src = pyPkgs.fetchPypi {
pname = "rns";
inherit version;
sha256 = "554814231c237b9caacf8df669312e57dd7d3f84b6d4810125087d1a79a75d75";
};
patchPhase = ''
# Fix license_files syntax: ("LICENSE") is a string not tuple
# Newer setuptools iterates over it char by char, fails on 'S'
substituteInPlace setup.py \
--replace-fail 'license_files = ("LICENSE")' 'license_files = ("LICENSE",)'
'';
propagatedBuildInputs = with pyPkgs; [ cryptography pyserial ];
doCheck = false;
pythonImportsCheck = [ "RNS" ];
meta = with final.lib; {
description = "Self-configuring, encrypted and resilient mesh networking stack";
homepage = "https://reticulum.network/";
license = licenses.mit;
platforms = platforms.linux;
};
};
lxmf = python3.pkgs.buildPythonApplication rec {
pname = "lxmf";
version = "0.9.8";
format = "setuptools";
src = pyPkgs.fetchPypi {
inherit pname version;
sha256 = "30f39f3a975a049c12ee2cfceb3261d24cb5adec881c6821f7354464b3f3650c";
};
propagatedBuildInputs = [ final.reticulumStack ];
doCheck = false;
pythonImportsCheck = [ "LXMF" ];
meta = with final.lib; {
description = "Lightweight Extensible Message Format for Reticulum";
homepage = "https://github.com/markqvist/lxmf";
license = licenses.mit;
platforms = platforms.linux;
};
};
nomadnet = python3.pkgs.buildPythonApplication rec {
pname = "nomadnet";
version = "1.1.1";
format = "setuptools";
src = pyPkgs.fetchPypi {
inherit pname version;
sha256 = "fa13b64a10e75b705a58024815ab72451700aa726af96d415ba99dec28dfc40a";
};
propagatedBuildInputs = with pyPkgs; [ final.reticulumStack final.lxmf urwid qrcode ];
doCheck = false;
pythonImportsCheck = [ "nomadnet" ];
meta = with final.lib; {
description = "Nomad Network resilient mesh communications platform";
homepage = "https://github.com/markqvist/NomadNet";
license = licenses.mit;
platforms = platforms.linux;
};
};
rnsh = python3.pkgs.buildPythonApplication rec {
pname = "rnsh";
version = "0.1.7";
format = "setuptools";
src = pyPkgs.fetchPypi {
inherit pname version;
sha256 = "9cb72f25abb1c6d300f8014b264184ff78f592fe88e36094938012990b797c93";
};
propagatedBuildInputs = [ final.reticulumStack ];
doCheck = false;
pythonImportsCheck = [ "rnsh" ];
meta = with final.lib; {
description = "Remote shell over Reticulum";
homepage = "https://github.com/acehoss/rnsh";
license = licenses.mit;
platforms = platforms.linux;
};
};
}
# meshtastic may not exist in all nixpkgs versions (e.g. not in 25.11)
// prev.lib.optionalAttrs (prev ? meshtastic) {
inherit (prev) meshtastic;
}

View File

@@ -4,9 +4,7 @@
group = "ai-worker"; group = "ai-worker";
home = "/home/ai-worker"; home = "/home/ai-worker";
createHome = true; createHome = true;
# SECURITY: ai-worker is NOT in the docker group. extraGroups = [ "docker" ];
# Docker access is restricted via sudo whitelist — only specific subcommands allowed.
# extraGroups = [ "docker" ]; — REMOVED: docker group gives unrestricted docker daemon access
shell = pkgs.bashInteractive; shell = pkgs.bashInteractive;
openssh.authorizedKeys.keys = [ openssh.authorizedKeys.keys = [
keys.users.ai-worker.main keys.users.ai-worker.main
@@ -19,134 +17,19 @@
# Enable restricted AI worker SSH access for ollama benchmarking # Enable restricted AI worker SSH access for ollama benchmarking
# SECURITY: ai-worker can only: # SECURITY: ai-worker can only:
# - SSH into host from Hermes container # - SSH into host from Hermes container
# - Run docker commands via sudo (whitelist below — no exec/cp/commit) # - Run docker commands (docker exec ollama ...) via docker group
# - Run specific security audit commands # - Run specific security audit commands
# - NO access to infra repo (no bind mount) # - NO access to infra repo (no bind mount)
# - NO nix/nixos-rebuild/nh commands # - NO sudo access (no nh, nixos-rebuild, nixpkgs-fmt, nix)
# WORKFLOW: SSH from Hermes container, run docker commands via sudo, return and save results # WORKFLOW: SSH from Hermes container, run docker benchmarks, return and save results to /opt/data/ai-optimizer/
services.aiWorkerAccess = true; services.aiWorkerAccess = true;
# Restricted sudo for ai-worker # Restricted sudo for ai-worker - security checks only
# IMPORTANT: ai-worker is NOT in docker group. All docker access goes through sudo.
# Only the subcommands listed below are allowed — everything else is denied.
# This prevents: docker exec, docker cp, docker commit, and other file-modifying operations.
security.sudo.extraRules = [ security.sudo.extraRules = [
{ {
users = [ "ai-worker" ]; users = [ "ai-worker" ];
commands = [ commands = [
# === Docker commands: lifecycle management (NO file modification) === # Firewall checks
# ps/inspect/logs — read-only status checks
{
command = "/run/current-system/sw/bin/docker ps";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/docker inspect *";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/docker logs *";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/docker images";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/docker info";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/docker version";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/docker stats *";
options = [ "NOPASSWD" ];
}
# start/stop/restart — container lifecycle
{
command = "/run/current-system/sw/bin/docker start *";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/docker stop *";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/docker restart *";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/docker rm *";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/docker rmi *";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/docker wait *";
options = [ "NOPASSWD" ];
}
# pull/build/run — image management and container creation
{
command = "/run/current-system/sw/bin/docker pull *";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/docker build *";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/docker run *";
options = [ "NOPASSWD" ];
}
# compose — orchestration
{
command = "/run/current-system/sw/bin/docker compose *";
options = [ "NOPASSWD" ];
}
# system — disk cleanup
{
command = "/run/current-system/sw/bin/docker system *";
options = [ "NOPASSWD" ];
}
# network — list only (create/modify not needed)
{
command = "/run/current-system/sw/bin/docker network ls";
options = [ "NOPASSWD" ];
}
# volume — list only (create/modify not needed)
{
command = "/run/current-system/sw/bin/docker volume ls";
options = [ "NOPASSWD" ];
}
# === EXPLICITLY DENIED docker commands (not in whitelist — sudo rejects them) ===
# docker exec — executes arbitrary commands inside running containers (FILE MODIFICATION)
# docker cp — copies files between containers and host (FILE ACCESS)
# docker commit — creates images from running containers (DATA EXFIL)
# docker diff — inspects filesystem changes (INFO LEAK)
# docker export — exports container filesystem (DATA EXFIL)
# docker import — imports filesystem archives
# docker load — loads docker images
# docker save — saves docker images to tar (DATA EXFIL)
# docker attach — attaches to running containers (INTERACTIVE ACCESS)
# docker push — pushes images to registries (DATA EXFIL)
# docker tag — renames images
# docker create — creates containers (use 'docker run' instead)
# docker plugin — manages plugins
# docker network create/rm — network management
# docker volume create/rm — volume management
# === Firewall checks ===
{ {
command = "/run/wrappers/bin/sudo iptables -L -n -v"; command = "/run/wrappers/bin/sudo iptables -L -n -v";
options = [ "NOPASSWD" ]; options = [ "NOPASSWD" ];
@@ -155,8 +38,7 @@
command = "/run/wrappers/bin/sudo iptables -S"; command = "/run/wrappers/bin/sudo iptables -S";
options = [ "NOPASSWD" ]; options = [ "NOPASSWD" ];
} }
# Fail2ban status
# === Fail2ban status ===
{ {
command = "/run/current-system/sw/bin/fail2ban-client status"; command = "/run/current-system/sw/bin/fail2ban-client status";
options = [ "NOPASSWD" ]; options = [ "NOPASSWD" ];
@@ -169,8 +51,7 @@
command = "/run/current-system/sw/bin/fail2ban-client get * banned"; command = "/run/current-system/sw/bin/fail2ban-client get * banned";
options = [ "NOPASSWD" ]; options = [ "NOPASSWD" ];
} }
# Log inspection
# === Log inspection ===
{ {
command = "/run/current-system/sw/bin/journalctl -t kernel -n 100"; command = "/run/current-system/sw/bin/journalctl -t kernel -n 100";
options = [ "NOPASSWD" ]; options = [ "NOPASSWD" ];
@@ -183,14 +64,21 @@
command = "/run/current-system/sw/bin/journalctl -u firewall -n 50"; command = "/run/current-system/sw/bin/journalctl -u firewall -n 50";
options = [ "NOPASSWD" ]; options = [ "NOPASSWD" ];
} }
# SSH config verification
# === SSH config verification ===
{ {
command = "/run/current-system/sw/bin/sshd -T"; command = "/run/current-system/sw/bin/sshd -T";
options = [ "NOPASSWD" ]; options = [ "NOPASSWD" ];
} }
# Docker service checks
# === Network diagnostics === {
command = "/run/current-system/sw/bin/docker ps";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/docker inspect *";
options = [ "NOPASSWD" ];
}
# Network diagnostics
{ {
command = "/run/current-system/sw/bin/ss -tlnp"; command = "/run/current-system/sw/bin/ss -tlnp";
options = [ "NOPASSWD" ]; options = [ "NOPASSWD" ];