Compare commits

..

25 Commits

Author SHA1 Message Date
0f0c7824bc fix: update compose submodule for backup OS compat
Points compose to fix/backup-compose-os-upgrade which:
- restic/restic -> restic/rest-server
- embergarage/restic-browser -> floibach/restic-browser
- adds persistent browser-data volume
- adds backup/.env with RESTIC_PASSWORD
2026-07-05 21:35:25 -04:00
f8502d0114 fix: update compose submodule to fix hermes user ownership 2026-07-04 23:50:48 -04:00
65543e3ce8 Re-encrypt secrets for actual SSH host keys (bypass rekey until master key on server) 2026-06-22 00:08:17 -04:00
5de646d018 Fix rekey block placement (was outside module braces) 2026-06-22 00:06:35 -04:00
24761b58b2 Add agenix-rekey module + rekey config back to lazyworkhorse
agenix-rekey auto-re-encrypts secrets per host at build time:
- Encrypted once in git with master identity
- Re-encrypted for each host using their SSH host key
- No manual multi-recipient encryption needed
2026-06-22 00:05:22 -04:00
9bf38f1077 Remove age.rekey from lazyworkhorse (module not available, host key handles decryption directly) 2026-06-22 00:02:53 -04:00
cbb016d409 Fix agenix-rekey: add real uConsole host key, re-encrypt secrets
- Update uConsole hostPubkey from dummy to real SSH host key
- Add uConsole host key to keys.nix
- Add age.rekey config to lazyworkhorse
- Add SSH host key to identityPaths on both hosts
- Re-encrypt builder_key.age and gortium_password.age for both hosts
2026-06-22 00:02:22 -04:00
7e7bb73b0d fix: restore uConsole display rotation (transform 3) in dotfiles
The rotation was lost in the dotfiles repo PR #16 (rename host/ -> hosts/)
which was based on a branch predating the transform commits.
Fixed directly in dotfiles submodule.
2026-06-21 23:55:57 -04:00
ea5eac2234 Comment foxtrotgps, viking, gpsbabel for cross-compile bootstrap 2026-06-21 23:37:42 -04:00
c9072e86da Temporarily comment native-only packages for cross-compile bootstrap
Comment out wsjtx, fldigi, sdrpp, gqrx, inspectrum, chirp, trustedqsl
- these need native compilation on uConsole but remote builder
  is not yet deployed. Will uncomment after bootstrap.
2026-06-21 23:36:09 -04:00
3a0fb70d26 Fix: remove ca-derivations (not supported in Lix 2.96) 2026-06-21 23:28:05 -04:00
a0e0b6e78e Update compose submodule to master (adds vpn stack) 2026-06-21 23:26:55 -04:00
e03f09c247 Merge feat/remote-builders into uconsole-cm5-incremental
Bidirectional remote building + rename uconsole-cm5 -> uConsole:
- Rename all uconsole-cm5 refs to uConsole (nixosSystem key, paths, module)
- Replace single-machine remote-builder module with multi-machine config
- Enable remote builder on lazyworkhorse -> uConsole (aarch64)
- Enable remote builder on uConsole -> lazyworkhorse (x86_64)
- Add builder system user + SSH key via agenix-rekey (secrets/builder_key.age)
- Add builder_key + gortium_password age secrets to all hosts
- Fix pre-existing bugs: missing modules in lazyworkhorse/cyt-pi,
  SSH config structure (hostKeys outside settings), cyt-pi inline user
- Remove obsolete openclaw-node service + module + age secrets
2026-06-21 23:14:14 -04:00
db2d540b3c feat: server-side remote builder config (lazyworkhorse dispatches aarch64 to uConsole)
Server only — uConsole-side config will be added in uconsole-cm5-incremental branch.
Includes reusable module, builder system user, and SSH key placeholder.
2026-06-21 16:22:50 -04:00
5aca97e057 Merge pull request 'feat: add uConsole CM5 host with Reticulum mesh network support' (#61) from feat/uconsole-cm5-v3 into master
Reviewed-on: #61
2026-06-06 13:19:19 +00: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
16 changed files with 480 additions and 271 deletions

305
flake.nix
View File

@@ -8,46 +8,247 @@
inputs.darwin.follows = ""; inputs.darwin.follows = "";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
agenix-rekey = {
url = "github:oddlama/agenix-rekey";
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";
}; };
nixpkgs-uconsole.url = "github:NixOS/nixpkgs/nixos-25.11";
nixos-uconsole = { nixos-uconsole = {
url = "github:nixos-uconsole/nixos-uconsole"; url = "github:gortium/nixos-uconsole/pr/dcs-panel-detection";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs-uconsole";
inputs.nixos-raspberrypi.follows = "nixos-raspberrypi";
}; };
nixos-raspberrypi = { nixos-raspberrypi = {
url = "github:nvmd/nixos-raspberrypi/v1.20260517.0"; url = "github:gortium/nixos-raspberrypi/cm5-cross-v1";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs-uconsole";
};
home-manager = {
url = "github:nix-community/home-manager/release-25.11";
inputs.nixpkgs.follows = "nixpkgs";
}; };
self.submodules = true; self.submodules = true;
home-manager = {
url = "github:nix-community/home-manager/release-25.11";
inputs.nixpkgs.follows = "nixpkgs-uconsole";
};
}; };
outputs = { self, nixpkgs, agenix, lix, nixos-uconsole, nixos-raspberrypi, home-manager, ... }@inputs: outputs = { self, nixpkgs, agenix, agenix-rekey, lix
, nixpkgs-uconsole, nixos-uconsole, nixos-raspberrypi
, home-manager
, ... }@inputs:
let let
system = "x86_64-linux"; system = "x86_64-linux";
keys = import ./lib/keys.nix; keys = import ./lib/keys.nix;
paths = import ./lib/paths.nix; paths = {
overlays = [ flake = "/home/gortium/infra";
agenix.overlays.default identities = [
(import ./overlays/reticulum.nix) ]; "/home/gortium/.ssh/gortium_ssh_key"
"/etc/ssh/ssh_host_ed25519_key"
"/root/.age/bootstrap.key" ];
};
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;
config.permittedInsecurePackages = [ config.permittedInsecurePackages = [ "openclaw-2026.3.12" ];
"openclaw-2026.3.12"
];
}; };
devShell = import ./shells/nix_dev.nix { devShell = import ./shells/nix_dev.nix {
inherit pkgs system agenix; inherit pkgs system agenix;
}; };
in
##############################################################################
# CROSS-COMPILE WORKAROUNDS — packages that fail aarch64 cross-compile
#
# These packages need NATIVE COMPILATION on the uConsole itself (aarch64).
# They cannot cross-compile from x86_64 for various reasons listed below.
# We work around them in the overlay until we set up distributed builds
# with the uConsole as a native aarch64 builder.
#
# ==== Cross-compile failures ====
#
# libcamera / libcamera-rpi / libpisp:
# meta.platforms excludes aarch64. pipewire hard-depends on them in nixos-25.11.
# Fix: empty meta.platforms + strip from pipewire buildInputs.
#
# gjs:
# Need native display (GTK3/4 tests) for cross-compile configure.
# Fix: meson -Dskip_gtk_tests=true.
#
# hyprland:
# Qt6Quick missing from aarch64 qtdeclarative, breaks hyprland-qt-support.
# Fix: wrapRuntimeDeps=false (Qt UI components disabled, WM still works).
#
# boost.mpi:
# Boost.Build has no b2 architecture alternatives for ARM.
# Fix: useMpi=false.
#
# perl-ldap (perlPackages.perlldap):
# Module::Install requires Perl dynamic loading (Fcntl) which is
# unavailable in cross-compiled Perl.
# Fix: stripped from john.s propagatedBuildInputs.
#
# john (John the Ripper):
# Indirectly affected — depends on perl-ldap for perl utility scripts.
# Fix: perl-ldap stripped from propagatedBuildInputs (john still works,
# just loses sha-dump.pl etc. LDAP support).
#
# gss (GNU Generic Security Service):
# autogen.sh fails cross-compile. Pulled by mailutils → emacs-pgtk.
# Fix: emacs withMailutils=false.
#
# emacs-pgtk:
# Indirectly affected — depends on mailutils which depends on gss.
# Fix: withMailutils=false (no mail/IMAP within emacs).
#
# qtquick3d (Qt6):
# Qt::Quick not available in aarch64 cross-compile qtdeclarative.
# cmake skips build, ninja has no install target.
# Fix: removed js8call, switched wireshark → wireshark-cli.
#
# js8call:
# REMOVED from system packages. Depends on Qt6 multimedia → qtquick3d.
#
# wireshark-qt:
# SWITCHED to wireshark-cli. Same Qt6 multimedia → qtquick3d chain.
#
# neovim:
# `libnlua0.so` built for aarch64, luajit (x86_64) tries to load it
# during codegen (preload_nlua.lua). No clean override option.
# Fix: remove from system packages + install via native build
# once uConsole is set up as remote builder.
#
# clamav:
# cmake try_run() + Rust proc-macro can't find native linker in
# cross-compile (cc crate uses cross CC, no cc in PATH for build
# scripts). Chain: clamav → system-path → etc → dbus → polkit.
# Fix: remove from system packages; clamscan available from server.
#
# ==== Remote builder setup (bidirectional) — TODO ====
# To eliminate cross-compile exceptions, set up distributed builds:
# 1. Create a dedicated `builder` user on both hosts (no shell, home=/var/empty)
# 2. Add the same SSH key to both hosts (symmetric)
# 3. On lazyworkhorse — `nix.buildMachines` pointing to uConsole for aarch64-linux
# 4. On uConsole — `nix.buildMachines` pointing to lazyworkhorse for x86_64-linux
# 5. Remove the uconsoleCrossOverlay workarounds above
# 6. Nix auto-dispatches derivations by `system` — no per-package exceptions needed
# Example buildMachines config:
# Server dispatches aarch64 builds to uConsole (4 cores, less power):
# nix.buildMachines = [{
# hostName = "uConsole.local";
# systems = ["aarch64-linux"];
# maxJobs = 4;
# sshUser = "builder";
# sshKey = "/etc/ssh/builder_key";
# }];
# uConsole dispatches x86_64 builds to server (36 cores, 256GB RAM):
# nix.buildMachines = [{
# hostName = "lazyworkhorse.net";
# port = 2424;
# systems = ["x86_64-linux"];
# maxJobs = 36;
# sshUser = "builder";
# sshKey = "/etc/ssh/builder_key";
# }];
# ==== How to build natively on uConsole ====
# To native-compile these on the uConsole:
# 1. Add uConsole as a remote builder (nix.buildMachines)
# 2. Set nix.extra-platforms = [ "aarch64-linux" ] on server
# 3. Remove the overlay workarounds below
# 4. Packages will auto-dispatch to uConsole for native builds
##############################################################################
uconsoleCrossOverlay = final: prev: {
libcamera = prev.libcamera.overrideAttrs (_: { meta.platforms = []; });
libcamera-rpi = prev.libcamera-rpi.overrideAttrs (_: { meta.platforms = []; });
libpisp = prev.libpisp.overrideAttrs (_: { meta.platforms = []; });
pipewire = prev.pipewire.overrideAttrs (old: {
buildInputs = builtins.filter
(x: !(x?pname && x.pname == "libcamera"))
(old.buildInputs or []);
mesonFlags = builtins.filter
(flag: !(builtins.isString flag && builtins.match ".*libcamera.*" flag != null))
(old.mesonFlags or []) ++ [ "-Dlibcamera=disabled" ];
});
gjs = prev.gjs.overrideAttrs (old: {
mesonFlags = (old.mesonFlags or []) ++ [ "-Dskip_gtk_tests=true" ];
});
hyprland = prev.hyprland.override { wrapRuntimeDeps = false; };
boost = prev.boost.override { useMpi = false; };
# perl-ldap cannot cross-compile (Module::Install needs dynamic loading)
xdg-desktop-portal-hyprland = prev.xdg-desktop-portal-hyprland.overrideAttrs (old: {
preConfigure = (old.preConfigure or "") + ''
cmakeFlags="$cmakeFlags -Dhyprwayland-scanner_DIR=${prev.buildPackages.hyprwayland-scanner}/lib/cmake/hyprwayland-scanner" 2>/dev/null || true
export PKG_CONFIG_PATH="${prev.buildPackages.hyprwayland-scanner}/lib/pkgconfig:$PKG_CONFIG_PATH"
'';
});
emacs-pgtk = prev.emacs-pgtk.override { withMailutils = false; };
# perl-ldap fails cross-compile (Module::Install needs dynamic loading)
# Strip it from john deps -- the perl scripts that need it are not critical
john = prev.john.overrideAttrs (old: {
nativeBuildInputs = (old.nativeBuildInputs or []) ++ [
prev.buildPackages.python3 # python3 for opencl_generate_dynamic_loader.py
];
configureFlags = (old.configureFlags or []) ++ [
"ac_cv_prog_PYTHON=${prev.buildPackages.python3}/bin/python3"
];
propagatedBuildInputs = builtins.filter
(x: x?pname && x.pname != "perl-ldap")
(old.propagatedBuildInputs or []);
});
# clamav: removed from system packages (see note above).
};
uconsoleRpiPipewireOverlay = final: prev: {
pipewire = prev.pipewire.overrideAttrs (old: {
buildInputs = builtins.filter
(x: !(x?pname && x.pname == "libcamera"))
(old.buildInputs or []);
mesonFlags = builtins.filter
(flag: !(builtins.isString flag && builtins.match ".*libcamera.*" flag != null))
(old.mesonFlags or []) ++ [ "-Dlibcamera=disabled" ];
});
};
uconsoleBaseModules = [
{ {
nixpkgs.buildPlatform = "x86_64-linux";
nixpkgs.hostPlatform = "aarch64-linux";
nixpkgs.config.allowUnfree = true;
boot.loader.raspberry-pi.bootloader = "kernel";
nixpkgs.overlays = [ uconsoleCrossOverlay (import ./overlays/reticulum.nix) ];
}
nixos-raspberrypi.nixosModules.nixpkgs-rpi
({ config, lib, pkgs, ... }: {
nixpkgs.overlays = [ uconsoleRpiPipewireOverlay ];
})
nixos-raspberrypi.nixosModules.raspberry-pi-5.base
nixos-raspberrypi.lib.inject-overlays
nixos-raspberrypi.lib.inject-overlays-global
nixos-uconsole.nixosModules.uconsole-cm5
./modules/nixos/hardware/uConsole-aio-v2.nix
({ config, lib, pkgs, inputs, ... }: let
lixCross = import inputs.nixpkgs-uconsole {
localSystem = { system = "x86_64-linux"; };
crossSystem = { system = "aarch64-linux"; };
overlays = [ inputs.lix.overlays.default ];
};
in { nix.package = lixCross.lix; })
inputs.home-manager.nixosModules.home-manager
agenix.nixosModules.default
agenix-rekey.nixosModules.default
./hosts/uConsole/configuration.nix
./hosts/uConsole/hardware-configuration.nix
./modules/nixos/services/remote-builder.nix
./modules/nixos/services/wireguard-client.nix
./modules/nixos/services/clamav.nix
./modules/nixos/security/ai-worker-restricted.nix
./users/gortium/gortium.nix
./users/ai-worker/ai-worker.nix
];
in {
nixosConfigurations = { nixosConfigurations = {
lazyworkhorse = nixpkgs.lib.nixosSystem { lazyworkhorse = nixpkgs.lib.nixosSystem {
specialArgs = { inherit system self keys paths inputs; }; specialArgs = { inherit system self keys paths inputs; };
@@ -56,23 +257,26 @@
nixpkgs.overlays = overlays; nixpkgs.overlays = overlays;
nixpkgs.config.allowUnfree = true; nixpkgs.config.allowUnfree = true;
nixpkgs.config.rocmSupport = true; nixpkgs.config.rocmSupport = true;
nixpkgs.config.permittedInsecurePackages = [ nixpkgs.config.permittedInsecurePackages = [ "openclaw-2026.3.12" ];
"openclaw-2026.3.12"
];
nix.package = lix.packages.${system}.default; nix.package = lix.packages.${system}.default;
} }
inputs.home-manager.nixosModules.home-manager
agenix.nixosModules.default agenix.nixosModules.default
agenix-rekey.nixosModules.default
./hosts/lazyworkhorse/configuration.nix ./hosts/lazyworkhorse/configuration.nix
./hosts/lazyworkhorse/hardware-configuration.nix ./hosts/lazyworkhorse/hardware-configuration.nix
./modules/nixos/filesystem/hoardingcow-mount.nix ./modules/nixos/filesystem/hoardingcow-mount.nix
./modules/nixos/services/docker_manager.nix ./modules/nixos/services/docker_manager.nix
./modules/nixos/services/open_code_server.nix
./modules/nixos/services/ollama_init_custom_models.nix
./modules/nixos/services/openclaw_node.nix
./modules/nixos/filesystem/poup-16t-disk.nix ./modules/nixos/filesystem/poup-16t-disk.nix
./modules/nixos/services/ollama_init_custom_models.nix
./modules/nixos/services/open_code_server.nix
./modules/nixos/services/clamav.nix
./modules/nixos/services/wireguard-client.nix
./modules/nixos/services/remote-builder.nix
./modules/nixos/security/ai-worker-restricted.nix ./modules/nixos/security/ai-worker-restricted.nix
./users/gortium.nix ./users/gortium/gortium.nix
./users/ai-worker.nix ./users/ai-worker/ai-worker.nix
./users/builder.nix
]; ];
}; };
@@ -85,29 +289,46 @@
nixpkgs.hostPlatform = "aarch64-linux"; nixpkgs.hostPlatform = "aarch64-linux";
nix.package = lix.packages."aarch64-linux".default; nix.package = lix.packages."aarch64-linux".default;
} }
inputs.home-manager.nixosModules.home-manager
agenix.nixosModules.default
./hosts/cyt-pi/configuration.nix ./hosts/cyt-pi/configuration.nix
./hosts/cyt-pi/hardware-configuration.nix ./hosts/cyt-pi/hardware-configuration.nix
./modules/nixos/services/remote-builder.nix
./modules/nixos/services/wireguard-client.nix
./users/gortium/gortium.nix
]; ];
}; };
uConsole = nixos-raspberrypi.lib.nixosSystem { uConsole = nixpkgs-uconsole.lib.nixosSystem {
specialArgs = { inherit self keys paths inputs nixos-raspberrypi; }; system = "aarch64-linux";
modules = [ specialArgs = {
{ inherit self keys paths inputs;
nixpkgs.overlays = overlays; nixos-raspberrypi = nixos-raspberrypi;
nixpkgs.config.allowUnfree = true; isCM4 = false;
nixpkgs.hostPlatform = "aarch64-linux"; };
nix.package = lix.packages."aarch64-linux".default; modules = uconsoleBaseModules;
}
nixos-raspberrypi.nixosModules.raspberry-pi-5.base
nixos-uconsole.nixosModules.uconsole-cm5
agenix.nixosModules.default
./modules/nixos/hardware/uconsole-cm5-aio-v2.nix
./hosts/uConsole/configuration.nix
./hosts/uConsole/hardware-configuration.nix
];
}; };
}; };
agenix-rekey = agenix-rekey.configure {
userFlake = self;
nixosConfigurations = self.nixosConfigurations;
};
devShells.${system}.default = devShell; devShells.${system}.default = devShell;
packages.${system} = {
uConsole-image = (nixos-raspberrypi.lib.nixosSystem {
system = "aarch64-linux";
specialArgs = {
inherit self keys inputs;
nixos-raspberrypi = nixos-raspberrypi;
isCM4 = false;
};
modules = uconsoleBaseModules ++ [
nixos-raspberrypi.nixosModules.sd-image
];
}).config.system.build.sdImage;
};
}; };
} }

View File

@@ -21,14 +21,7 @@
settings.PermitRootLogin = "prohibit-password"; settings.PermitRootLogin = "prohibit-password";
}; };
# User # User (defined in users/gortium/gortium.nix)
users.users.gortium = {
isNormalUser = true;
extraGroups = [ "wheel" "networkmanager" "kismet" ];
openssh.authorizedKeys.keys = [
# Populate with your public key
];
};
# CYT Project Dependencies (Headless) # CYT Project Dependencies (Headless)
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
@@ -77,6 +70,12 @@
}; };
}; };
# Age secrets
age.secrets.gortium_password = {
file = ../../secrets/gortium_password.age;
};
# CYT Application Service # CYT Application Service
systemd.services.cyt-app = { systemd.services.cyt-app = {
description = "Chasing Your Tail - Target Detector"; description = "Chasing Your Tail - Target Detector";

View File

@@ -9,8 +9,12 @@
hoardingcow-mount.enable = true; hoardingcow-mount.enable = true;
# Flakesss # Flakesss
nix.settings.experimental-features = [ "nix-command" "flakes" "flake-self-attrs" "ca-derivations" ]; 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 = {
@@ -166,9 +170,8 @@
settings = { settings = {
PasswordAuthentication = false; PasswordAuthentication = false;
KbdInteractiveAuthentication = false; KbdInteractiveAuthentication = false;
# ============================================================ };
# ClamAV antivirus — daemon, hourly updates, daily scan, on-access hostKeys = [
# ============================================================
{ {
path = "/etc/ssh/ssh_host_ed25519_key"; path = "/etc/ssh/ssh_host_ed25519_key";
type = "ed25519"; type = "ed25519";
@@ -248,7 +251,9 @@
# Private host ssh key managed by agenix # Private host ssh key managed by agenix
age = { age = {
identityPaths = paths.identities; identityPaths = paths.identities ++ [
"/etc/ssh/ssh_host_ed25519_key"
];
secrets = { secrets = {
containers_env = { containers_env = {
file = ../../secrets/containers.env.age; file = ../../secrets/containers.env.age;
@@ -271,13 +276,6 @@
mode = "0600"; mode = "0600";
path = "/home/ai-worker/.ssh/ai_ssh_key"; path = "/home/ai-worker/.ssh/ai_ssh_key";
}; };
openclaw_gateway_token = {
file = ../../secrets/openclaw_gateway_token.age;
owner = "root";
group = "ai-worker";
mode = "0440";
path = "/run/secrets/openclaw_gateway_token";
};
wireguard_private_key = { wireguard_private_key = {
file = ../../secrets/wireguard_private_key.age; file = ../../secrets/wireguard_private_key.age;
owner = "root"; owner = "root";
@@ -292,17 +290,21 @@
mode = "0400"; mode = "0400";
path = "/run/secrets/wireguard_preshared_key"; path = "/run/secrets/wireguard_preshared_key";
}; };
builder_key = {
file = ../../secrets/builder_key.age;
owner = "root";
group = "root";
mode = "0600";
path = "/etc/ssh/builder_key";
};
gortium_password = {
file = ../../secrets/gortium_password.age;
owner = "gortium";
group = "gortium";
mode = "0400";
path = "/run/secrets/gortium_password";
}; };
}; };
# OpenClaw Node service (host-side execution for Docker gateway)
services.openclaw-node = {
enable = true;
user = "ai-worker";
gatewayHost = "127.0.0.1";
gatewayPort = 18789;
gatewayTokenFile = "/run/secrets/openclaw_gateway_token";
displayName = "lazyworkhorse-host";
}; };
# Public host ssh key (kept in sync with the private one) # Public host ssh key (kept in sync with the private one)
@@ -567,21 +569,29 @@
# For more information, see `man configuration.nix` or https://nixos.org/manual/nixos/stable/options#opt-system.stateVersion . # For more information, see `man configuration.nix` or https://nixos.org/manual/nixos/stable/options#opt-system.stateVersion .
system.stateVersion = "25.05"; # Did you read the comment? system.stateVersion = "25.05"; # Did you read the comment?
}
# Remote builder — uConsole for aarch64-linux native builds # ============================================================
nix.distributedBuilds = true; # Remote builder — dispatches aarch64-linux builds to uConsole
nix.buildMachines = [{ # ============================================================
services.remoteBuilder = {
enable = true;
machines = [
{
hostName = "192.168.1.120"; hostName = "192.168.1.120";
systems = ["aarch64-linux"]; port = 22;
maxJobs = 4;
supportedFeatures = ["big-parallel" "nixos-test" "benchmark" "gccarch-armv8-a"];
sshUser = "builder"; sshUser = "builder";
sshKey = "/home/ai-worker/id_deploy"; sshKey = config.age.secrets.builder_key.path;
}]; systems = [ "aarch64-linux" ];
nix.extraOptions = ' maxJobs = 4;
builders-use-substitutes = true }
fallback = true ];
'; };
# agenix-rekey — automatic re-encryption for each target host
age.rekey = {
masterIdentities = [
"/home/gortium/.ssh/gortium_ssh_key"
];
};
}

View File

@@ -106,18 +106,18 @@
htop htop
tmux tmux
# ===== HAM Radio ===== # ===== HAM Radio =====
wsjtx # removed for bootstrap - now native # wsjtx # removed for bootstrap - now native (native-only, needs remote build)
fldigi # removed for bootstrap - now native # fldigi # removed for bootstrap - now native (native-only, needs remote build)
pat # Winlink client pat # Winlink client
direwolf # AX.25 packet modem direwolf # AX.25 packet modem
chirp # Radio programming tool - now native # chirp # Radio programming tool - now native (native-only, needs remote build)
hamlib # Ham radio control libraries hamlib # Ham radio control libraries
trustedqsl # Logbook of the World (LoTW) - now native # trustedqsl # Logbook of the World (LoTW) - now native (native-only, needs remote build)
# ===== SDR / RF ===== # ===== SDR / RF =====
sdrpp # removed for bootstrap - now native # sdrpp # removed for bootstrap - now native (native-only, needs remote build)
gqrx # removed for bootstrap - now native # gqrx # removed for bootstrap - now native (native-only, needs remote build)
rtl-sdr # RTL-SDR drivers & utilities rtl-sdr # RTL-SDR drivers & utilities
inspectrum # removed for bootstrap - now native # inspectrum # removed for bootstrap - now native (native-only, needs remote build)
soapysdr-with-plugins # SoapySDR + hardware support plugins soapysdr-with-plugins # SoapySDR + hardware support plugins
# ===== Mesh / LoRa ===== # ===== Mesh / LoRa =====
reticulumStack # Reticulum Network Stack reticulumStack # Reticulum Network Stack
@@ -132,9 +132,9 @@
john # John the Ripper - now native john # John the Ripper - now native
sqlmap # SQL injection tool sqlmap # SQL injection tool
# ===== GPS / Maps ===== # ===== GPS / Maps =====
foxtrotgps # removed for bootstrap - now native #foxtrotgps # removed for bootstrap - now native (native-only, needs remote build)
viking # removed for bootstrap - now native #viking # removed for bootstrap - now native (native-only, needs remote build)
gpsbabel # GPS data conversion - now native #gpsbabel # GPS data conversion - now native (native-only, needs remote build)
]; ];
# ============================================================ # ============================================================
# Reticulum Service (rnsd) # Reticulum Service (rnsd)
@@ -192,6 +192,32 @@
networking.firewall.allowedTCPPorts = [ 22 ]; networking.firewall.allowedTCPPorts = [ 22 ];
networking.firewall.allowedUDPPorts = [ ]; networking.firewall.allowedUDPPorts = [ ];
# ============================================================ # ============================================================
# Remote builder — dispatches x86_64-linux builds to lazyworkhorse
# ============================================================
services.remoteBuilder = {
enable = true;
machines = [
{
hostName = "lazyworkhorse.net";
port = 2424;
sshUser = "builder";
sshKey = config.age.secrets.builder_key.path;
systems = [ "x86_64-linux" ];
maxJobs = 36;
supportedFeatures = [ "benchmark" "big-parallel" "nixos-test" ];
}
];
};
# Age secret for builder SSH key (remote building)
age.secrets.builder_key = {
file = ../../secrets/builder_key.age;
};
# ============================================================
age.identityPaths = [
"/etc/ssh/ssh_host_ed25519_key"
];
# agenix-rekey — automatic secret re-encryption at deploy time # agenix-rekey — automatic secret re-encryption at deploy time
# ============================================================ # ============================================================
age.rekey = { age.rekey = {
@@ -202,7 +228,7 @@
# uConsole SSH host pubkey — for automatic rekey at build time # uConsole SSH host pubkey — for automatic rekey at build time
# Once uConsole is deployed, replace with actual pubkey from: # Once uConsole is deployed, replace with actual pubkey from:
# ssh-keyscan uConsole.local | ssh-to-age # ssh-keyscan uConsole.local | ssh-to-age
hostPubkey = "age1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs3290gq"; # dummy — replace after bootstrap hostPubkey = "age1un8td4jzkhg5fm4jja7z3aznskc8hlcm8ky0j2f70tv74rulwgaqtpemn2"; # uConsole CM5 SSH host key
}; };
# Pipewire overlay: drop libcamera (fixes aarch64 cross-compile — rpi-pisp blocks) # Pipewire overlay: drop libcamera (fixes aarch64 cross-compile — rpi-pisp blocks)

View File

@@ -9,6 +9,13 @@
ai-worker = { ai-worker = {
main = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAXeGtPPcsP2IYRQNvII41NVWhJsarEk8c4qxs/a5sXf"; main = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAXeGtPPcsP2IYRQNvII41NVWhJsarEk8c4qxs/a5sXf";
}; };
builder = {
# Same key on both hosts for bidirectional remote building.
# Generate with: ssh-keygen -t ed25519 -f /etc/ssh/builder_key -N ""
# Replace the placeholder below with the public key (builder_key.pub).
main = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIByG79NM/rk13q3MTow9Ezb8pe3RWoQ76Mv8atASp3Ri";
};
}; };
hosts = { hosts = {
@@ -18,5 +25,9 @@
gitea = ""; gitea = "";
bootstrap = "age1r796v2uldtspawyh863pks74sd2pwcan8j4e4pjzsvkmr3vjja9qpz5ste"; bootstrap = "age1r796v2uldtspawyh863pks74sd2pwcan8j4e4pjzsvkmr3vjja9qpz5ste";
}; };
uConsole = {
main = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICasvsmaHuNVaWnjwuBfoMXyY/6PIqGL8yxstPBb46u4";
bootstrap = "age1un8td4jzkhg5fm4jja7z3aznskc8hlcm8ky0j2f70tv74rulwgaqtpemn2";
};
}; };
} }

View File

@@ -1,64 +0,0 @@
{ config, lib, pkgs, ... }:
let
cfg = config.services.openclaw-node;
openclawPkg = pkgs.openclaw;
in {
options.services.openclaw-node = {
enable = lib.mkEnableOption "OpenClaw Node service";
user = lib.mkOption {
type = lib.types.str;
default = "ai-worker";
description = "User to run the OpenClaw headless node as.";
};
gatewayHost = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "Gateway host (IP or hostname).";
};
gatewayPort = lib.mkOption {
type = lib.types.int;
default = 18789;
description = "Gateway WebSocket port.";
};
gatewayTokenFile = lib.mkOption {
type = lib.types.str;
default = "";
description = "Path to file containing the gateway auth token.";
};
displayName = lib.mkOption {
type = lib.types.str;
default = "lazyworkhorse-host";
description = "Display name for this node (shown in pairing).";
};
};
config = lib.mkIf cfg.enable {
systemd.services.openclaw-node = {
description = "OpenClaw Headless Node Service";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "exec";
User = cfg.user;
Group = cfg.user;
WorkingDirectory = "/home/${cfg.user}";
ExecStart = ''
${pkgs.bash}/bin/bash -c 'export OPENCLAW_GATEWAY_TOKEN=$(cat ${cfg.gatewayTokenFile}) && exec ${openclawPkg}/bin/openclaw node run --host ${cfg.gatewayHost} --port ${toString cfg.gatewayPort} --display-name "${cfg.displayName}"'
'';
Restart = "always";
RestartSec = 5;
};
environment = {
NODE_ENV = "production";
};
};
};
}

View File

@@ -3,69 +3,71 @@ let
cfg = config.services.remoteBuilder; cfg = config.services.remoteBuilder;
in { in {
options.services.remoteBuilder = { options.services.remoteBuilder = {
enable = lib.mkEnableOption "remote Nix build machine (lazyworkhorse server)"; enable = lib.mkEnableOption "remote Nix build machine";
buildMachine = { machines = lib.mkOption {
host = lib.mkOption { type = lib.types.listOf (lib.types.submodule {
options = {
hostName = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = "lazyworkhorse.net";
description = "Hostname or IP of the remote build machine."; description = "Hostname or IP of the remote build machine.";
}; };
sshUser = lib.mkOption {
type = lib.types.str;
default = "ai-worker";
description = "SSH user on the remote build machine.";
};
port = lib.mkOption { port = lib.mkOption {
type = lib.types.port; type = lib.types.port;
default = 2424; default = 22;
description = "SSH port added via ~root/.ssh/config since nix.buildMachines has no sshPort option."; description = "SSH port.";
};
sshUser = lib.mkOption {
type = lib.types.str;
default = "builder";
description = "SSH user on the remote build machine.";
};
sshKey = lib.mkOption {
type = lib.types.str;
description = "Path to SSH private key for the builder.";
}; };
systems = lib.mkOption { systems = lib.mkOption {
type = lib.types.listOf lib.types.str; type = lib.types.listOf lib.types.str;
default = [ "aarch64-linux" "x86_64-linux" ]; default = [ "aarch64-linux" ];
description = "System types the remote builder can build for."; description = "System types the remote builder can build for.";
}; };
maxJobs = lib.mkOption { maxJobs = lib.mkOption {
type = lib.types.int; type = lib.types.int;
default = 16; default = 4;
description = "Max parallel jobs on the remote builder."; description = "Max parallel jobs on the remote builder.";
}; };
supportedFeatures = lib.mkOption { supportedFeatures = lib.mkOption {
type = lib.types.listOf lib.types.str; type = lib.types.listOf lib.types.str;
default = [ "big-parallel" "nixos-test" "benchmark" ]; default = [ "benchmark" "big-parallel" "nixos-test" ];
description = "Features the remote builder supports."; description = "Features the remote builder supports.";
}; };
}; };
});
fallbackLocal = lib.mkOption { default = [];
type = lib.types.bool; description = "List of remote Nix build machines.";
default = true;
description = "Fall back to local build when remote builder is unreachable.";
}; };
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
nix.distributedBuilds = true; nix.distributedBuilds = true;
nix.buildMachines = [{ nix.buildMachines = map (m: {
hostName = cfg.buildMachine.host; hostName = m.hostName;
sshUser = cfg.buildMachine.sshUser; sshUser = m.sshUser;
systems = cfg.buildMachine.systems; sshKey = m.sshKey;
maxJobs = cfg.buildMachine.maxJobs; systems = m.systems;
supportedFeatures = cfg.buildMachine.supportedFeatures; maxJobs = m.maxJobs;
}]; supportedFeatures = m.supportedFeatures;
}) cfg.machines;
nix.extraOptions = lib.optionalString cfg.fallbackLocal '' # SSH config for port + key (nix.buildMachines has no port option)
builders-use-substitutes = true programs.ssh.extraConfig = lib.concatStringsSep "\n" (map (m: ''
fallback = true Host ${m.hostName}
''; HostName ${m.hostName}
Port ${toString m.port}
# SSH config for the remote builder (since nix.buildMachines has no port option) User ${m.sshUser}
programs.ssh.extraConfig = '' IdentityFile ${m.sshKey}
Host ${cfg.buildMachine.host} StrictHostKeyChecking no
HostName ${cfg.buildMachine.host} UserKnownHostsFile /dev/null
Port ${toString cfg.buildMachine.port} '') cfg.machines);
User ${cfg.buildMachine.sshUser}
'';
}; };
} }

BIN
secrets/builder_key.age Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -1,11 +0,0 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEdoTUQ4QSBCWEpO
cG9yNnFpcHFqTkNzTngxU1MxN0NYK0hrZFhUTjVORWFrK3JNd2tZCmtMTGpwQk1E
WlUwL3N6SGRWblpnNEkrWkkyU2hQMkRIK0M3R0pOVEREV3MKLT4gY2osLWdyZWFz
ZSBacSozVVQgUCAxRS1OQSAuKXxDPCoKbStWNW1BZjBZQzNDaTlDbU5EZkxsRWxM
cXJ3dDU1RDNpOXRlV0tzdEp2NUo3S1lhRG5Md0RHTGlJdkFSYmt5YQo4R1hiQWRG
V2VxekJKZwotLS0geG1XSi9VbkhXZHQzcEFVS3hKNzVueXFLa2xnZTc3Q2tJTVZ5
eXJabWk5Ywp6bJCP3s0xxzjE+eTR+cv7ZUnkoliT/n7uIprq1BTn/LIRLkUTUqs3
NiDwrXcoq4/QKd0Dt+8ap3vFAuusjGxRlnYMaRrZie2AGtTV8U7Q7durm9o2K+/4
QzRQ/MtumIQm
-----END AGE ENCRYPTED FILE-----

View File

@@ -12,5 +12,7 @@ in
"home_wifi.age".publicKeys = authorizedKeys; "home_wifi.age".publicKeys = authorizedKeys;
"lazyworkhorse_host_ssh_key.age".publicKeys = authorizedKeys; "lazyworkhorse_host_ssh_key.age".publicKeys = authorizedKeys;
"n8n_ssh_key.age".publicKeys = authorizedKeys; "n8n_ssh_key.age".publicKeys = authorizedKeys;
"openclaw_gateway_token.age".publicKeys = authorizedKeys; "builder_key.age".publicKeys = authorizedKeys ++ [
keys.hosts.uConsole.bootstrap
];
} }

13
users/builder.nix Normal file
View File

@@ -0,0 +1,13 @@
{ config, lib, pkgs, keys, ... }: {
users.users.builder = {
isSystemUser = true;
group = "builder";
home = "/var/empty";
createHome = false;
shell = "${pkgs.shadow}/bin/nologin";
openssh.authorizedKeys.keys = with keys; [
users.builder.main
];
};
users.groups.builder = {};
}