Compare commits

..

3 Commits

Author SHA1 Message Date
f4b666284a feat: add Hyperspace Pods NixOS module and enable on lazyworkhorse
Hyperspace Pods let multiple machines pool their GPUs into one private
P2P mesh AI cluster. Models are split across all connected GPUs —
e.g. two machines with 16GB VRAM each can run Qwen 3.5 32B together.

Changes:
- Add modules/nixos/services/hyperspace.nix — NixOS module that:
  * Fetches the Hyperspace CLI binary (v5.45.30) via fetchurl
  * Sets up systemd service for the agent
  * Opens firewall ports (libp2p 4001, chain 30301, API 8080)
  * Configures GPU passthrough for AMD MI50 (ROCm)
- Register module in flake.nix for lazyworkhorse
- Enable hyperspace service on lazyworkhorse (ai-worker user, port 8080)

Usage after deployment:
  hyperspace pod create "tdnde-lab"   # create pod
  hyperspace pod invite                # share invite with cyt-pi
  curl http://localhost:8080/v1/chat/completions  # OpenAI API

See skill: nixos-hyperspace-pods
2026-05-02 15:36:15 +00:00
815ca3afa6 chore: update compose submodule to traefik logging branch 2026-05-02 15:30:28 +00:00
e983775c04 docs: add merge priority order with security hardening as #1 priority
- Updated roadmap phase status (Phase 4 complete)
- Added merge priority table with PR #28 (security) at top
- Documented that security must merge before new services exposed
- Added deployment command reference
2026-05-02 15:30:28 +00:00
23 changed files with 358 additions and 1136 deletions

View File

@@ -13,7 +13,9 @@ None
-**Phase 1: Foundation Setup** - Establish core NixOS configuration with flakes
-**Phase 2: Docker Service Integration** - Integrate Docker Compose services
-**Phase 3: AI Assistant Integration** - Enable AI-assisted infrastructure management
- [ ] **Phase 4: Internet Access & MCP** - MCP server for web access
- **Phase 4: Internet Access & MCP** - MCP server for web access
- 🚨 **Security Hardening** - CRITICAL: Firewall, fail2ban, SSH hardening (PR #28)
- [ ] **Phase 5: TAK Server** - Research, implementation, and validation
## Phase Details
@@ -133,8 +135,25 @@ Plans:
## Progress
**Merge Priority Order** (CRITICAL - merge in this order):
| Priority | PR | Description | Status | Notes |
|----------|-----|-------------|--------|-------|
| 🚨 1 | #28 | **Security hardening** (firewall, fail2ban, SSH) | Open | **MERGE FIRST** - protects all other services |
| 2 | #22 | Matrix bridge dependency fix | Open | Blocks Hermes functionality |
| 3 | #21 | Backup network creation fix | Open | Infrastructure fix |
| 4 | #25 | Hermes voice GPU support | Open | Feature enhancement |
| 5 | #24 | uConsole CM5 host | Open | New hardware support |
| 6 | #23 | NixOS deployment infrastructure | Open | Deployment tooling |
| 7 | #1 | AI worker restricted access | Open | Legacy PR (superseded by hardening) |
**Execution Order:**
Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7
Phases execute in numeric order: 1 → 2 → 3 → 4 → Security → 5 → 6 → 7
**Merge vs Phase Execution:**
- PRs can merge independently (no strict phase ordering for merges)
- **EXCEPTION:** Security hardening (#28) must merge before any new services are exposed
- After security merge, deploy with: `nh os switch --flake .#lazyworkhorse`
| Phase | Milestone | Plans Complete | Status | Completed |
|-------|-----------|----------------|--------|-----------|

View File

@@ -5,7 +5,6 @@ This document outlines the development conventions for this NixOS-based infrastr
## Build & Deployment
- **Build/Deploy:** Use `nixos-rebuild switch --flake .#<hostname>` to build and deploy the configuration for a specific host.
- **CRITICAL — Validate before pushing:** Always `nix build --no-link '.#nixosConfigurations.<hostname>.config.system.build.toplevel'` (or `nh os build`) and confirm it succeeds before pushing any changes. Never push untested NixOS configs.
- **Development Shell:** Activate the development environment with `nix develop`.
## Linting & Formatting

188
flake.lock generated
View File

@@ -23,38 +23,6 @@
"type": "github"
}
},
"argononed": {
"flake": false,
"locked": {
"lastModified": 1729566243,
"narHash": "sha256-DPNI0Dpk5aym3Baf5UbEe5GENDrSmmXVdriRSWE+rgk=",
"owner": "nvmd",
"repo": "argononed",
"rev": "16dbee54d49b66d5654d228d1061246b440ef7cf",
"type": "github"
},
"original": {
"owner": "nvmd",
"repo": "argononed",
"type": "github"
}
},
"argononed_2": {
"flake": false,
"locked": {
"lastModified": 1729566243,
"narHash": "sha256-DPNI0Dpk5aym3Baf5UbEe5GENDrSmmXVdriRSWE+rgk=",
"owner": "nvmd",
"repo": "argononed",
"rev": "16dbee54d49b66d5654d228d1061246b440ef7cf",
"type": "github"
},
"original": {
"owner": "nvmd",
"repo": "argononed",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
@@ -69,36 +37,6 @@
"url": "https://git.lix.systems/lix-project/flake-compat/archive/main.tar.gz"
}
},
"flake-compat_2": {
"locked": {
"lastModified": 1767039857,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-compat_3": {
"locked": {
"lastModified": 1767039857,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
@@ -206,130 +144,6 @@
"type": "github"
}
},
"nixos-images": {
"inputs": {
"nixos-stable": [
"nixos-raspberrypi",
"nixpkgs"
],
"nixos-unstable": [
"nixos-raspberrypi",
"nixpkgs"
]
},
"locked": {
"lastModified": 1747747741,
"narHash": "sha256-LUOH27unNWbGTvZFitHonraNx0JF/55h30r9WxqrznM=",
"owner": "nvmd",
"repo": "nixos-images",
"rev": "cbbd6db325775096680b65e2a32fb6187c09bbb4",
"type": "github"
},
"original": {
"owner": "nvmd",
"ref": "sdimage-installer",
"repo": "nixos-images",
"type": "github"
}
},
"nixos-images_2": {
"inputs": {
"nixos-stable": [
"nixos-uconsole",
"nixos-raspberrypi",
"nixpkgs"
],
"nixos-unstable": [
"nixos-uconsole",
"nixos-raspberrypi",
"nixpkgs"
]
},
"locked": {
"lastModified": 1747747741,
"narHash": "sha256-LUOH27unNWbGTvZFitHonraNx0JF/55h30r9WxqrznM=",
"owner": "nvmd",
"repo": "nixos-images",
"rev": "cbbd6db325775096680b65e2a32fb6187c09bbb4",
"type": "github"
},
"original": {
"owner": "nvmd",
"ref": "sdimage-installer",
"repo": "nixos-images",
"type": "github"
}
},
"nixos-raspberrypi": {
"inputs": {
"argononed": "argononed",
"flake-compat": "flake-compat_2",
"nixos-images": "nixos-images",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1779023229,
"narHash": "sha256-MInilg7B/06c34SwOuGSBho4l0H1EZcmvxTkSWCs5pE=",
"owner": "nvmd",
"repo": "nixos-raspberrypi",
"rev": "06c6e3513e1ee64b651913193fc6ac38aa4963f5",
"type": "github"
},
"original": {
"owner": "nvmd",
"ref": "v1.20260517.0",
"repo": "nixos-raspberrypi",
"type": "github"
}
},
"nixos-raspberrypi_2": {
"inputs": {
"argononed": "argononed_2",
"flake-compat": "flake-compat_3",
"nixos-images": "nixos-images_2",
"nixpkgs": [
"nixos-uconsole",
"nixpkgs"
]
},
"locked": {
"lastModified": 1779023229,
"narHash": "sha256-MInilg7B/06c34SwOuGSBho4l0H1EZcmvxTkSWCs5pE=",
"owner": "nvmd",
"repo": "nixos-raspberrypi",
"rev": "06c6e3513e1ee64b651913193fc6ac38aa4963f5",
"type": "github"
},
"original": {
"owner": "nvmd",
"ref": "v1.20260517.0",
"repo": "nixos-raspberrypi",
"type": "github"
}
},
"nixos-uconsole": {
"inputs": {
"nixos-raspberrypi": "nixos-raspberrypi_2",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1780333979,
"narHash": "sha256-yjp/gWRqWCxalYLe2+27gbxhttL+46Ozm7qvccpFbv4=",
"owner": "nixos-uconsole",
"repo": "nixos-uconsole",
"rev": "1e6a8dc51c7b26e15850688a0b01121c3fea4b30",
"type": "github"
},
"original": {
"owner": "nixos-uconsole",
"repo": "nixos-uconsole",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1705033721,
@@ -398,8 +212,6 @@
"inputs": {
"agenix": "agenix",
"lix": "lix",
"nixos-raspberrypi": "nixos-raspberrypi",
"nixos-uconsole": "nixos-uconsole",
"nixpkgs": "nixpkgs_2"
}
},

View File

@@ -12,18 +12,10 @@
url = "git+https://git.lix.systems/lix-project/lix?ref=main";
inputs.nixpkgs.follows = "nixpkgs";
};
nixos-uconsole = {
url = "github:nixos-uconsole/nixos-uconsole";
inputs.nixpkgs.follows = "nixpkgs";
};
nixos-raspberrypi = {
url = "github:nvmd/nixos-raspberrypi/v1.20260517.0";
inputs.nixpkgs.follows = "nixpkgs";
};
self.submodules = true;
};
outputs = { self, nixpkgs, agenix, lix, nixos-uconsole, nixos-raspberrypi, ... }@inputs:
outputs = { self, nixpkgs, agenix, lix, ... }@inputs:
let
system = "x86_64-linux";
keys = import ./lib/keys.nix;
@@ -34,7 +26,7 @@
"/etc/ssh/ssh_host_ed25519_key"
"/root/.age/bootstrap.key" ];
};
overlays = [ agenix.overlays.default (import ./overlays/reticulum.nix) ];
overlays = [ agenix.overlays.default ];
pkgs = import nixpkgs {
inherit system overlays;
config.allowUnfree = true;
@@ -69,7 +61,7 @@
./modules/nixos/services/open_code_server.nix
./modules/nixos/services/ollama_init_custom_models.nix
./modules/nixos/services/openclaw_node.nix
./modules/nixos/security/ai-worker-restricted.nix
./modules/nixos/services/hyperspace.nix
./users/gortium.nix
./users/ai-worker.nix
];
@@ -88,22 +80,6 @@
./hosts/cyt-pi/hardware-configuration.nix
];
};
uConsole = nixos-raspberrypi.lib.nixosSystem {
specialArgs = { inherit self keys paths inputs nixos-raspberrypi; };
modules = [
{
nixpkgs.overlays = overlays;
nixpkgs.config.allowUnfree = true;
nixpkgs.hostPlatform = "aarch64-linux";
nix.package = lix.packages."aarch64-linux".default;
}
nixos-raspberrypi.nixosModules.raspberry-pi-5.base
nixos-uconsole.nixosModules.uconsole-cm5
./hosts/uConsole/configuration.nix
./hosts/uConsole/hardware-configuration.nix
];
};
};
devShells.${system}.default = devShell;
};

View File

@@ -11,10 +11,6 @@
# Flakesss
nix.settings.experimental-features = [ "nix-command" "flakes" "flake-self-attrs" ];
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
nix.gc = {
@@ -40,7 +36,7 @@
"transparent_hugepage=always" # because mucho ram
];
# 2. Load the specific drivers found by sensors-detect
boot.kernelModules = [ "nct6775" "lm96163" "iptable_nat" "iptable_filter" ];
boot.kernelModules = [ "nct6775" "lm96163" ];
# 3. Force the nct6775 driver to recognize the chip if it's stubborn
boot.extraModprobeConfig = ''
options nct6775 force_id=0xd280
@@ -53,26 +49,6 @@
networking.networkmanager.enable = true; # Easiest to use and most distros use this by default.
networking.hostId = "deadbeef";
# WireGuard VPN client -- always up, connects to wg-easy server
# Create age-encrypted secrets before deploying (run on the host):
# echo -n "<private_key>" | agenix -e secrets/wireguard_private_key.age
# echo -n "<preshared_key>" | agenix -e secrets/wireguard_preshared_key.age
networking.wireguard.interfaces = {
wg0 = {
ips = [ "10.8.0.3/24" ];
privateKeyFile = config.age.secrets.wireguard_private_key.path;
peers = [
{
publicKey = "rY9zII3AOm8rog2rv02PyA3Bq7zdvTOGkZapfCV1DkE=";
presharedKeyFile = config.age.secrets.wireguard_preshared_key.path;
allowedIPs = [ "10.8.0.0/24" ];
endpoint = "vpn.lazyworkhorse.net:51820";
persistentKeepalive = 25;
}
];
};
};
# Set your time zone.
time.timeZone = "America/Montreal";
@@ -144,7 +120,6 @@
# You can use https://Search.nixos.org/ to find more packages (and options).
environment.systemPackages = with pkgs; [
neovim
tmux
docker-compose
wget
age
@@ -183,7 +158,7 @@
settings = {
PasswordAuthentication = false;
KbdInteractiveAuthentication = false;
# Additional hardening settings below in SERVER HARDENING section
PermitRootLogin = "prohibit-password";
};
hostKeys = [
{
@@ -212,7 +187,6 @@
ai = {
path = self + "/assets/compose/ai";
envFile = config.age.secrets.containers_env.path;
ports = [ 22000 ]; # Syncthing TCP sync
};
cloudstorage = {
@@ -247,16 +221,17 @@
path = self + "/assets/compose/homepage";
};
vpn = {
path = self + "/assets/compose/vpn";
envFile = config.age.secrets.containers_env.path;
};
# tak = {
# path = self + "/assets/compose/tak";
# };
};
services.opencode = {
enable = true;
port = 4099;
ollamaUrl = "http://127.0.0.1:11434/v1";
};
# Private host ssh key managed by agenix
age = {
identityPaths = paths.identities;
@@ -289,21 +264,27 @@
mode = "0440";
path = "/run/secrets/openclaw_gateway_token";
};
wireguard_private_key = {
file = ../../secrets/wireguard_private_key.age;
owner = "root";
group = "root";
mode = "0400";
path = "/run/secrets/wireguard_private_key";
};
wireguard_preshared_key = {
file = ../../secrets/wireguard_preshared_key.age;
owner = "root";
group = "root";
mode = "0400";
path = "/run/secrets/wireguard_preshared_key";
};
};
# 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";
};
# Hyperspace Pods — P2P mesh AI cluster (combine GPUs across machines)
services.hyperspace = {
enable = true;
user = "ai-worker";
apiPort = 8080;
profile = "auto";
openFirewall = true;
extraArgs = [ "--verbose" ];
};
# Public host ssh key (kept in sync with the private one)
@@ -337,203 +318,6 @@
# Or disable the firewall altogether.
# networking.firewall.enable = false;
# =============================================================================
# SERVER HARDENING - Firewall, Fail2ban, SSH, Kernel
# =============================================================================
# Firewall - default deny, explicit allow
networking.firewall = {
# Enable firewall with default deny policy (NixOS firewall denies all by default)
enable = true;
allowPing = true;
# Only essential ports exposed to internet
allowedTCPPorts = [
2424 # SSH (non-standard port)
2222 # Gitea (version control)
80 # HTTP (Traefik redirect)
443 # HTTPS (Traefik)
# 8000 # Portainer - REVIEW: internal only?
# 4242 # Coms - REVIEW: internal only?
# 5000 # TAK API - REVIEW: internal only?
# 8087 # TAK Connect - REVIEW: internal only?
# 8089 # TAK Management - REVIEW: internal only?
];
allowedUDPPorts = [
51820 # WireGuard VPN
];
# Rate limiting and attack prevention
extraCommands = ''
# 1. Wipe the INPUT chain clean at the start of every activation
iptables -F INPUT
# Rate limit SSH connections (max 20 new connections per 60 seconds)
iptables -A INPUT -p tcp --dport 2424 -m state --state NEW -m recent --set
iptables -A INPUT -p tcp --dport 2424 -m state --state NEW -m recent --update --seconds 60 --hitcount 20 -j DROP
# Rate limit HTTP/HTTPS (protects Traefik)
iptables -A INPUT -p tcp --dport 80 -m state --state NEW -m limit --limit 25/minute --limit-burst 100 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -m state --state NEW -m limit --limit 25/minute --limit-burst 100 -j ACCEPT
# Drop invalid packets
iptables -A INPUT -m state --state INVALID -j DROP
# Log dropped packets (rate limited)
iptables -A INPUT -m limit --limit 5/min -j LOG --log-prefix "IPTables-Dropped: " --log-level 4
# 3. CRITICAL: Re-link the NixOS default firewall chain
# Without this line, the 'allowedTCPPorts' in your Nix config will be ignored!
iptables -A INPUT -j nixos-fw
'';
};
# Fail2ban - automatic IP banning
services.fail2ban = {
enable = true;
maxretry = 3;
bantime = "1h";
banaction = "iptables-multiport";
jails = {
# SSH brute force protection (uses systemd journal backend)
sshd = {
enabled = true;
settings = {
filter = "sshd";
port = "2424";
maxretry = 3;
bantime = "1h";
};
};
# Recidive - ban repeat offenders for 1 week
recidive = {
enabled = true;
settings = {
filter = "recidive";
logpath = "/var/log/fail2ban.log";
bantime = "1w";
findtime = "1d";
maxretry = 3;
};
};
# HTTP authentication failures (Traefik)
http-auth = {
enabled = true;
settings = {
filter = "traefik-auth";
port = "80,443";
logpath = "/var/log/traefik/access.log";
maxretry = 5;
bantime = "1h";
};
};
# HTTP scanning/attacks (Traefik)
http-botsearch = {
enabled = true;
settings = {
filter = "traefik-botsearch";
port = "80,443";
logpath = "/var/log/traefik/access.log";
maxretry = 2;
bantime = "2h";
};
};
};
};
# Custom fail2ban filters for Traefik
environment.etc."fail2ban/filter.d/traefik-auth.conf".text = ''
[Definition]
failregex = ^<HOST> -.*"(GET|POST|HEAD|PUT|DELETE).*" (401|403) \d+.*$
ignoreregex =
'';
environment.etc."fail2ban/filter.d/traefik-botsearch.conf".text = ''
[Definition]
failregex = ^<HOST> -.*"(GET|POST|HEAD|PUT|DELETE).*" 404 \d+.*$
^<HOST> -.*"(GET|POST|HEAD|PUT|DELETE).*/(\.|wp-|php|admin|login|xmlrpc|\.env|\.git|\.aws|\.azure).*" \d+.*$
ignoreregex =
'';
# SSH hardening
services.openssh.settings = {
PermitRootLogin = "no";
MaxAuthTries = 3;
MaxSessions = 20;
LoginGraceTime = 30;
ClientAliveInterval = 300;
ClientAliveCountMax = 2;
PermitEmptyPasswords = "no";
ChallengeResponseAuthentication = "no";
UsePAM = true;
LogLevel = "VERBOSE";
X11Forwarding = false;
AllowTcpForwarding = "no";
AllowAgentForwarding = "no";
PermitTunnel = "no";
};
# Kernel network hardening
boot.kernel.sysctl = {
# IP Spoofing protection
"net.ipv4.conf.all.rp_filter" = 1;
"net.ipv4.conf.default.rp_filter" = 1;
# Ignore ICMP broadcasts
"net.ipv4.icmp_echo_ignore_broadcasts" = 1;
# Disable source routing
"net.ipv4.conf.all.accept_source_route" = 0;
"net.ipv4.conf.default.accept_source_route" = 0;
"net.ipv6.conf.all.accept_source_route" = 0;
"net.ipv6.conf.default.accept_source_route" = 0;
# Disable redirects
"net.ipv4.conf.all.send_redirects" = 0;
"net.ipv4.conf.default.send_redirects" = 0;
# SYN flood protection
"net.ipv4.tcp_syncookies" = 1;
"net.ipv4.tcp_max_syn_backlog" = 2048;
"net.ipv4.tcp_synack_retries" = 2;
"net.ipv4.tcp_syn_retries" = 5;
# Log martian packets
"net.ipv4.conf.all.log_martians" = 1;
"net.ipv4.conf.default.log_martians" = 1;
# Ignore redirects
"net.ipv4.conf.all.accept_redirects" = 0;
"net.ipv4.conf.default.accept_redirects" = 0;
"net.ipv4.conf.all.secure_redirects" = 0;
"net.ipv4.conf.default.secure_redirects" = 0;
"net.ipv6.conf.all.accept_redirects" = 0;
"net.ipv6.conf.default.accept_redirects" = 0;
# Connection tuning
"net.core.somaxconn" = 4096;
"net.core.netdev_max_backlog" = 65536;
"net.ipv4.tcp_max_orphans" = 65536;
"net.ipv4.tcp_fin_timeout" = 15;
"net.ipv4.tcp_keepalive_time" = 300;
"net.ipv4.tcp_keepalive_probes" = 5;
"net.ipv4.tcp_keepalive_intvl" = 15;
};
# Audit logging
security.auditd.enable = true;
# Fail2ban log directory
systemd.tmpfiles.rules = [
"d /var/log/fail2ban 0755 root root -"
"d /var/log/traefik 0755 root root -"
];
# Copy the NixOS configuration file and link it from the resulting system
# (/run/current-system/configuration.nix). This is useful in case you
# accidentally delete configuration.nix.

View File

@@ -1,171 +0,0 @@
{ config, lib, pkgs, paths, self, keys, ... }:
{
# Basic Host Info
networking.hostName = "uConsole";
time.timeZone = "America/Montreal";
i18n.defaultLocale = "en_CA.UTF-8";
# System State
system.stateVersion = "25.05";
# 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;
# Networking
networking.networkmanager.enable = true;
services.openssh = {
enable = true;
settings.PermitRootLogin = lib.mkForce "prohibit-password";
settings.PasswordAuthentication = lib.mkForce false;
};
# 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
# ===== 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 # Python CLI for Meshtastic devices
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 and radio
# ============================================================
boot.kernelModules = [
"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
];
boot.blacklistedKernelModules = [ ];
# ============================================================
# 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.
}

View File

@@ -1,26 +0,0 @@
{ 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 = [ ];
# uConsole CM5 uses NVMe or eMMC for boot storage
# The uconsole-cm5 module sets up /boot/firmware and default /
# Override device label here if using different storage
fileSystems."/" = lib.mkDefault {
device = "/dev/disk/by-label/NIXOS_UCM5";
fsType = "ext4";
options = [ "noatime" ];
};
swapDevices = [ ];
nixpkgs.hostPlatform = lib.mkDefault "aarch64-linux";
hardware.enableRedistributableFirmware = true;
powerManagement.cpuFreqGovernor = lib.mkDefault "ondemand";
}

View File

@@ -1,105 +0,0 @@
# AI Worker Restricted Access
This module provides SSH access for the AI worker (hermes-agent) to run ollama benchmarks on the host.
## Security Model
The `ai-worker` user has:
### Filesystem Access
- **Home directory**: `/home/ai-worker` (standard user home)
- **No bind mounts**: Cannot access `/home/gortium/infra` or other host files
- **Cannot access**: Any files outside standard system paths
### Sudo Access
- **NONE**: ai-worker has no sudo privileges
- Cannot run `nh`, `nixos-rebuild`, `nixpkgs-fmt`, or `nix` with elevated permissions
### 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
## 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
# From Hermes container, SSH to host
ssh -i /path/to/ssh/key ai-worker@host.docker.internal
# On host, run ollama benchmarks via docker
docker exec ollama ollama pull devstral-small-2:24b
# Create test modelfile
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'
# Create and test model
docker exec ollama ollama create test-model -f /root/.ollama/test.modelfile
docker exec ollama ollama run test-model "Write a Python async function"
# Check VRAM usage
docker exec --privileged ollama rocm-smi --showmeminfo vram
# Cleanup
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
Connect as:
```bash
ssh ai-worker@lazyworkhorse
```
The working directory will be `/home/ai-worker`. No infra repo access.
## Verification
Check ai-worker permissions:
```bash
# On the host, as root or gortium:
sudo -u ai-worker sudo -l
# Should show: no sudo access
# Check docker group membership
groups ai-worker
# Should show: ai-worker docker
```
## Troubleshooting
If ai-worker cannot run docker commands:
```bash
# Check docker group membership
groups ai-worker
# Verify ollama container is running
docker ps | grep ollama
# Test docker access
sudo -u ai-worker docker exec ollama ollama list
```
If SSH connection fails:
```bash
# Check SSH key is authorized
cat /home/ai-worker/.ssh/authorized_keys
# Check SSH service
systemctl status sshd
```

View File

@@ -1,17 +0,0 @@
{ config, pkgs, lib, ... }:
with lib;
{
options.services.aiWorkerAccess = mkOption {
type = types.bool;
default = false;
description = "Enable AI worker SSH access with docker group membership for ollama benchmarking";
};
config = mkIf config.services.aiWorkerAccess {
# ai-worker is member of docker group - can run docker commands via SSH
# No bind mounts, no sudo access - docker-only for ollama benchmarking
users.groups.docker.members = [ "ai-worker" ];
};
}

View File

@@ -0,0 +1,235 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.hyperspace;
# Hyperspace CLI release from github.com/hyperspaceai/aios-cli
# The binary bundles Node.js runtime + llama.cpp + sidecars (~914MB)
# It auto-updates via `hyperspace update` post-install
hyperspacePkg = pkgs.stdenv.mkDerivation rec {
pname = "hyperspace";
version = cfg.release;
src = pkgs.fetchurl {
url = "https://github.com/hyperspaceai/aios-cli/releases/download/v${version}/aios-cli-x86_64-unknown-linux-gnu.tar.gz";
hash = "sha256-f6fJ8t3exqtYwUD5j+WvD+Hm0oN/Eef0X+R9Rj23dE0=";
};
sourceRoot = ".";
installPhase = ''
mkdir -p $out/bin $out/lib/hyperspace
# Main CLI binary
cp aios-cli $out/bin/hyperspace
chmod +x $out/bin/hyperspace
# Sidecar binaries
for f in _aios-cli pod-raft hyperspace-*; do
[ -f "$f" ] && install -m755 "$f" $out/lib/hyperspace/ || true
done
# WASM, native modules, Python shards
cp -r *.wasm $out/lib/hyperspace/ 2>/dev/null || true
cp -r *.node $out/lib/hyperspace/ 2>/dev/null || true
mkdir -p $out/lib/hyperspace/python
cp -r python/* $out/lib/hyperspace/python/ 2>/dev/null || true
# Skills directory
mkdir -p $out/share/hyperspace
cp -r skills $out/share/hyperspace/ 2>/dev/null || true
# Set HYPERSPACE_PATH so the binary finds sidecars
wrapProgram $out/bin/hyperspace \
--set HYPERSPACE_PATH "$out/lib/hyperspace" \
--set HYPERSPACE_SKILLS_DIR "$out/share/hyperspace/skills"
'';
nativeBuildInputs = with pkgs; [ makeWrapper ];
meta = {
description = "Hyperspace CLI P2P mesh AI inference network (Pods)";
longDescription = ''
Hyperspace Pods let multiple machines pool their GPUs into one private
AI cluster. Install the CLI, create a pod, share an invite link your
machines form a P2P mesh and can run models split across all connected
GPUs. Exposes an OpenAI-compatible API for use with Cursor, Claude Code,
Aider, etc.
'';
homepage = "https://hyperspace.sh";
sourceProvenance = with lib; [ sourceTypes.binaryNativeCode ];
license = lib.licenses.unfree;
platforms = [ "x86_64-linux" ];
maintainers = [ ];
};
};
in {
options.services.hyperspace = {
enable = mkEnableOption "Hyperspace P2P AI agent (Pods)";
release = mkOption {
type = types.str;
default = "5.45.30";
description = "Hyperspace CLI release version (from GitHub releases).";
};
user = mkOption {
type = types.str;
default = "ai-worker";
description = "System user to run the Hyperspace agent.";
};
apiPort = mkOption {
type = types.port;
default = 8080;
description = "Port for the OpenAI-compatible API server.";
};
autoStart = mkOption {
type = types.bool;
default = true;
description = "Auto-start the Hyperspace agent on boot.";
};
openFirewall = mkOption {
type = types.bool;
default = true;
description = "Open firewall ports for P2P traffic (libp2p 4001, chain 30301, API).";
};
profile = mkOption {
type = types.enum [ "auto" "full" "inference" "embedding" "relay" "storage" ];
default = "auto";
description = ''
Agent profile:
- auto: auto-detect hardware
- full: all 9 capabilities
- inference: GPU inference only
- embedding: CPU embedding only
- relay: lightweight relay
- storage: storage + memory
'';
};
extraArgs = mkOption {
type = types.listOf types.str;
default = [ ];
description = "Extra arguments passed to `hyperspace start`.";
};
dataDir = mkOption {
type = types.str;
default = "/var/lib/hyperspace";
description = "Data directory for agent state (models, config, logs).";
};
};
config = mkIf cfg.enable {
# Ensure the service user exists
users.users.${cfg.user} = {
isSystemUser = true;
group = cfg.user;
home = "/home/${cfg.user}";
createHome = true;
shell = pkgs.bash;
};
users.groups.${cfg.user} = { };
# Install the hyperspace binary
environment.systemPackages = [ hyperspacePkg ];
# Data directories
systemd.tmpfiles.rules = [
"d ${cfg.dataDir} 0755 ${cfg.user} ${cfg.user} -"
"d ${cfg.dataDir}/models 0755 ${cfg.user} ${cfg.user} -"
"d ${cfg.dataDir}/data 0755 ${cfg.user} ${cfg.user} -"
];
# Systemd service: runs the Hyperspace agent as a system daemon
systemd.services.hyperspace = {
description = "Hyperspace P2P AI Agent Pods mesh cluster";
documentation = [ "https://hyperspace.sh" "https://github.com/hyperspaceai/aios-cli" ];
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = mkIf cfg.autoStart [ "multi-user.target" ];
environment = {
HYPERSPACE_HOME = cfg.dataDir;
HYPERSPACE_API_PORT = toString cfg.apiPort;
HYPERSPACE_PATH = "${hyperspacePkg}/lib/hyperspace";
};
path = with pkgs; [ bash curl nodejs ];
script = ''
# Wait for network connectivity before starting
${pkgs.bash}/bin/bash -c '
for i in $(seq 1 30); do
ping -c 1 -W 1 8.8.8.8 >/dev/null 2>&1 && break
sleep 2
done
' || true
exec ${hyperspacePkg}/bin/hyperspace start \
--profile ${cfg.profile} \
--api-port ${toString cfg.apiPort} \
${lib.escapeShellArgs cfg.extraArgs}
'';
serviceConfig = {
Type = "exec";
User = cfg.user;
Group = cfg.user;
WorkingDirectory = cfg.dataDir;
Restart = "always";
RestartSec = 10;
TimeoutStartSec = 180;
TimeoutStopSec = 30;
KillMode = "mixed";
# File limits for network-heavy P2P agent
LimitNOFILE = 65536;
LimitNPROC = 4096;
# GPU access — AMD MI50 (ROCm) through /dev/kfd and /dev/dri
DeviceAllow = [
"/dev/kfd" "rw"
"/dev/dri" "rw"
];
SupplementaryGroups = [ "video" "render" ];
# Security hardening
NoNewPrivileges = true;
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = false; # needs GPU access
ReadWritePaths = [
cfg.dataDir
"/tmp"
];
BindPaths = [
# GPU devices for AMD MI50
"/dev/kfd"
"/dev/dri"
];
};
};
# Firewall: open P2P ports for the mesh network
networking.firewall = mkIf cfg.openFirewall {
allowedTCPPorts = [
4001 # libp2p P2P (agent gossip, DHT, circuits)
30301 # Chain P2P (blockchain consensus)
cfg.apiPort # OpenAI-compatible API
];
allowedUDPPorts = [
4001 # libp2p QUIC transport
30301 # Chain UDP discovery
];
};
};
}

View File

@@ -1,70 +1,49 @@
{ pkgs, ... }: {
systemd.services.init-ollama-model = {
description = "Initialize LLM models with extra context in Ollama Docker";
# On s'assure que Docker tourne avant de lancer ce script
after = [ "docker.service" ];
after = [ "docker-ollama.service" ];
wantedBy = [ "multi-user.target" ];
script = ''
# Fonction de création asynchrone pour ne pas bloquer le démarrage
(
echo "Starting asynchronous Ollama initialization..."
# Attente d'Ollama (maximum 120 secondes pour éviter une boucle infinie)
TIMEOUT=60
COUNT=0
while ! ${pkgs.curl}/bin/curl -s -f http://127.0.0.1:11434/api/tags > /dev/null; do
if [ $COUNT -ge $TIMEOUT ]; then
echo "Ollama did not become ready in time. Exiting."
exit 1
fi
echo "Waiting for Ollama API to be reachable..."
sleep 5
COUNT=$((COUNT + 5))
# Wait for Ollama
while ! ${pkgs.curl}/bin/curl -s http://localhost:11434/api/tags > /dev/null; do
sleep 2
done
create_model_if_missing() {
local model_name=$1
local base_model=$2
# Vérification robuste via l'API HTTP d'Ollama plutôt que docker exec (évite les conflits de tty)
if ! ${pkgs.curl}/bin/curl -s http://127.0.0.1:11434/api/tags | ${pkgs.jq}/bin/jq -e ".models[] | select(.name == \"$model_name\")" > /dev/null; then
if ! ${pkgs.docker}/bin/docker exec ollama ollama list | grep -q "$model_name"; then
echo "$model_name not found, creating from $base_model..."
# Utilisation d'un fichier temporaire sur l'hôte pour l'injecter proprement dans Docker
TMP_FILE=$(mktemp)
cat <<EOF > "$TMP_FILE"
# We use a custom TEMPLATE block to strip the 'currentDate' function
# which is unsupported in Ollama 0.5.7 but present in Devstral's default manifest.
${pkgs.docker}/bin/docker exec ollama sh -c "cat <<EOF > /root/.ollama/$model_name.modelfile
FROM $base_model
TEMPLATE """{{- if .System }}
TEMPLATE \"\"\"{{- if .System }}
[SYSTEM_PROMPT]
{{ .System }}
[/SYSTEM_PROMPT]
{{- end }}
{{- range .Messages }}
{{- if eq .Role "user" }}
{{- if eq .Role \"user\" }}
[INST]
{{ .Content }}
[/INST]
{{- else if eq .Role "assistant" }}
{{- else if eq .Role \"assistant\" }}
{{ .Content }}
{{- end }}
{{- end }}"""
{{- end }}\"\"\"
PARAMETER num_ctx 131072
PARAMETER num_predict 4096
PARAMETER num_keep 1024
PARAMETER repeat_penalty 1.1
PARAMETER top_k 40
PARAMETER stop "[INST]"
PARAMETER stop "[/INST]"
PARAMETER stop "</s>"
EOF
# Copie et création dans le conteneur
${pkgs.docker}/bin/docker cp "$TMP_FILE" ollama:/tmp/model.modelfile
${pkgs.docker}/bin/docker exec ollama ollama create "$model_name" -f /tmp/model.modelfile
${pkgs.docker}/bin/docker exec ollama rm /tmp/model.modelfile
rm -f "$TMP_FILE"
PARAMETER stop \"[INST]\"
PARAMETER stop \"[/INST]\"
PARAMETER stop \"</s>\"
EOF"
${pkgs.docker}/bin/docker exec ollama ollama create "$model_name" -f "/root/.ollama/$model_name.modelfile"
${pkgs.docker}/bin/docker exec ollama rm "/root/.ollama/$model_name.modelfile"
else
echo "$model_name already exists, skipping."
fi
@@ -76,12 +55,13 @@ EOF
# Create Devstral
create_model_if_missing "devstral-small-2:24b-128k" "devstral-small-2:24b"
) &
'';
# create_model_if_missing "qwen2.5-coder:32b-128k" "qwen2.5-coder:32b"
# create_model_if_missing "mistral-large-planner:123b" "mistral-large:123b-instruct-v2407-q4_K_S"
'';
serviceConfig = {
Type = "forking"; # Permet à systemd de savoir que le script passe en arrière-plan via '&'
User = "root";
Type = "oneshot";
RemainAfterExit = true;
};
};
}

View File

@@ -1,81 +0,0 @@
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";
};
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;
};
};
}

View File

@@ -1,19 +0,0 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEdoTUQ4QSA2QUFR
RmlnY2pYUFBHZGN3dzNpNVc0UW14V2NaVWZHWDJmcU1iWStsSHpJCnRYQUtEalNt
SzFYWUF6VWRyYWF5VCtsSGduRU96MHA0WlI0b0lpUGFram8KLT4gM0stZ3JlYXNl
IC5DPExhJQpCMlZRR0E1NGdsYlhNRXZUTVJqTTNHZUZQVUJoYkhheWd3TktvVEFp
WnFqbS9xWUxJSmI0U0hqQkdzQU1EVy9BCkhWc3A0Mk45YVBxcnJFWU5qa01LRTVw
WlNoWm40cjgyb1RXS3hGdlpRSEs5V0x0cHNvcVBOek0KLS0tIFhiOFExNnRSbEgz
M3FmS1owQUdHSXpiWVhLQ0R6emtuK2hZSkkrSTFtTTAKi2hXxTslGLlneDJlvHUm
OFsvgsHmiADRaiwDC/iCfWyEFzHyupXKX79B6rjt4Up2yUWHLHy4pyCvJiezAh1J
uxvtVmUYrqYOUoUHCEMLjjVydh5tt+22yXezV0ecU2ePiAgb0gv9EwH8t+70An6Z
fgvtZZqJ1hK6dTyTf4oROUjPD8RgfjDnRTTvTM1COMvwgpK+cboYAvJ4BhUsn70V
B3Gveg7++1U2ECF8YHKZGfSuWn0ajT5RzdgRXTveGAINiW4G98oyXnOVhRHmXo9i
8T/41afy4xhUbNCRsaAApFjS6y8xrpGtrOcpw0u3y3T/bjYHq8VDi4ttYWIF8hJD
HPo44+SDj0g9lr/UPQDWXaiZjdD6xdioUqnHnaAdyOPPf5D6C5I7WIQPPIjnJ1yc
PmQkIV4rtb+e7cWrGbnd9stCBZ2F+HcORfjgnJzKI5asy9rmdWPIIHkcZoPFZ/K8
2ZWXUVgAZvwzcFxSjh5uXKvkOgYixdVLRfa88+KM++vwz0y3O5naq3d1SaLxV4dk
mRifLNhF9y2MKjX0bZ86Wm74NlhGGQSnUuUHpPLSA6McVOfleN/yJ9gMsosxUc9U
xIsKHYn8gN0lPVtyTSBTomK6kxoKJXxia50x/XAhJwI9X3ydLg==
-----END AGE ENCRYPTED FILE-----

View File

@@ -1,10 +0,0 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEdoTUQ4QSBaT3h1
K28zWlE3N3BMdHRFaldkZ0F2YVZjT3kxK2dpV1RxZUVOSElsbkdJCnF2M1ZkUWky
cGtPaXdzMmU1cllsWWtQK3R4Z0xhUExxSW9xUVh3eUo5RjQKLT4geHEtZ3JlYXNl
IEQ5JWBWM0QKS1N5WlpnbEtLTFNxQVFvbkVEa1BOTDhQRDV0SDhsdWFEdE5xMDRa
b3loMDZEeGZTZ1NzaFM5UlZYT0NjMHVvWAordjh4cFhnb2pOc3JPZlFLbFU0eC9O
WWFFVlUKLS0tIEJnMm9LM09KQ3VXNXpNNU1kS3V5VDNOTGozdDdGdWQxcFc3Mk5j
R0p0WVkKIX6p7MGDZOxS5qVgYs5K5sKC31IPIEMaWAqA5Gygegj5rVnKtN/jn7+9
L5CBbzaSXzNwRce0WPVi0mQp56lL12mRlXXamOx/E9Q+r2rznFYdQA==
-----END AGE ENCRYPTED FILE-----

View File

@@ -1,42 +1,34 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEdoTUQ4QSBMNTFJ
N1Q3eXhyZFliWXRhbStOQW1SZTR5UWlIcFVtS3FEUWFLWlpnbGwwCkoxUzZhckNV
aUlpYUZTYU1wd0dtMy9Malo1ZmxvTXFMeU4yemYydmtNKzAKLT4gJn53LDswXzQt
Z3JlYXNlIG4oRW0gPE5cWiBaaVpkWSUgO1xTSGkKenhuUGpxV0xpUTFZNlRqeWlF
N1JaSUd5a0hWVgotLS0gQnZ6U2RjZHcvWTRTYklYd0tjNEJjc2xUYXNOMEFodm9t
NmZFcXhLQmxmZwp99f6PvpVh5VdcZvY56M0W/wCT9yWAGwV5kAJo625aFKUEDiDL
gybuE9ycCJ6Yk708YMRuCo8wlUF5Gd8uJuaXl2BX4450yVG7CQQbfK+zMpawvvmN
5caZW+s3SjtlxR76qQP4UbrAmnrUnXSR3HGqwNSoYjWRg7vdLuUFam+un+B/auew
g26RbsTC3CtBu3m7ELj61atZ2xBmx8KNrBFyzpy2HN/gZcys9IYxd8dU+M98yK1s
BvavfZqr2nH30iYZ68RsCpO6Tu0Si8OIXFIOVF4EYZukZMkpfVlm8dwDgDF/qh9v
cOJ3DE4SS+/iweu7/3KmsaSf+1Pz1J2g517foisJsEJ2AeSdSCeLFBa+hPM1nJmr
uBuTzFv4UrCsT+WszLkrQwktyOGKMDD7EOmhqGZyB3qu9/Tt0uQAAOG8voXYxaIM
CtxcyUSTfv+D3scporNYtRsF1wt/kIiqnTKWD4x4rUvaA1ik74Dk8VRGd/vesPDM
MkaUadb8NhNbZRUcbaqWrM7Op/WhcwwnmL05FznAkjFf0ub9k1idbHRuLrtYq0eZ
m/GMINkKnrwkx3A0tYOSHZBpCg70p6U1uzXHUdatZ56K+0OrQMWRwqsMzosQGEpS
Yjw4Kta4iDkgI6lqNnpFWm8E0lfA2SOP3LL6R0MIj0Jj3MrEzOspXyJHjlfJapNN
3kV4iWpXfbsEYDSVMuk5K2BUCV+fXQ9Ns9XaCu3uySzFeIIFZ0YKJ1guGZ/hInPn
HRbyB2dIC6Nkrh2BozJxiS8Z5/hnWOPGP3EYP5U4DoKlNynUISZSDlUs9Be06Oc0
MATCFCt5SjfbfMOrZwvl8HQTHJVuF4r48pUS9XrkWD0JxYIpMJk+jicCRk6VJf2K
QbbMxOKgU3Dvl+OdIo8DogwITVI0kyDsWsmmFPs9UWMi5z+aq+FEwEN8BBXUCtKa
0IEmuO/kWBf8qMfo9CxUL6AgBxRKAw691IYgF+vTsBtneyKwJdUiYEOu6AVe87SY
gCEDxGct762aVE+ZDDEYkL4WBiKpT+JJTXvTe2S9T1lObU/25vX4FiWGOUDcBxT9
VgoByZ2Ld+FueUgSZ1VMWxd+zkA6IrQetcL/Puumx7kBIiS+1hjxc/DMOAUy/lA5
JmjjulYAOL0K8HbE/oEYP9JM9YyR2eIMJGocsEmonzECPPENqmQZV6vT1zjxegay
xrkjXXbGMun28z9CrLuE2Tv5VNiGJkdB2SQbk0n2MV8Pmt7CsvQS0pT+mYsvejV+
K+9U3+YnHXiVMK0YbRJWVpnSyEQAAjASb85N5yi8ygN+V4JfTMxZDGvDUF8d4HtT
TXaXlQ4FoIRtbsN90SZCkYYIKIiZDQiZiK5yZ/Tw4slyZgZvG+zEG7kjMkWmqY2j
lirzxRoRAc1tSFmb4KNST5ZEzZMdUVkoOZqBTXCxCPfhcJhMedtQP/cGJSz6kDBS
2Mpd9oZDeL0SiJMWKB28Qvb/OnZzmfRG/8iWP5P1Tv9NzMg3nvc8w4WzjVeSFz7j
J59wd+4BJFmHITNfklrgxTx6ODHBvJX61+6098DzBjIJetxd+Vtpgypp2lazh2b2
TUH0BeTO7qtS7tbunXk4fVGm/LfeoPvlNf+LP8Jof0v5BPoQWkG2xS1Yvzbiz4/q
kkm1NF8kYW4ldi16rCCeAfBr7md5jDtP7YJxykey882aCe4CZunhRaDDgnySg82g
4hiWRmXPeuonGIHhtDPSb5CEzF9Bq6/lpDCPb2hu1mtZW2wvJv5Zf8WyopEPvBKy
YrOzh2HNvp0e2/fKkYi7LwnPNn3Wrevp4+3aK+BztaJ7JWFFVdx8C30rt0hIMfDu
2uc08RLuGBlcbPgLFzj+60OG+M+P+Ib8uCcPGfPnovft2Y2OSUf6yS1GylPSORDP
wsNEwCMd/q1cUKy263SBqypPOwViFBItT8fkpXrm+aBhjse9NDQ3r0LLwaLtN0r+
+PCvnVa2pjx4EBudYdkahWlCBEHVYFpgR9It/rXyRxJbL3lB6UvrMv+CQeu78MYW
Mk1M0uPchpXvLG3AHVF5hReYOjsi//76Fw9Mvl9uRJwja3swKw3sjFlovnnREGbm
Lj0KqyiJ7rh0rDNywYyq2PsVk7LjyQXwFq5J/8azo06oXG8FvNGkJ5R5n2hiEshM
dBBnVsOXpRqK/EZnKjb/Rfg=
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEdoTUQ4QSBOL29w
eGk1N2xxTHJtaUEvWWZmbkh1bk11Tjk3anNnMDB1cCtPYUMzdTNJCkdhQ08vblNG
UlV1K2xVTGZVTzFWYXAzcjZaMWs0RTFWdStKSmlSTURvK1EKLT4gLC1zKU8zVkgt
Z3JlYXNlIFUiXFcpS302IHByVn5jOy0gRDMKQjV3SHpDWUIybGFyQUg3ZlR0R2hV
eWM3SFlCVW5mdlpBVUF3a0xpNlZCeGNUd1oxTTlkc1RkTXdZS0lFTmN3Ci0tLSA3
VlBqM1VLWllZc0JnOTMvUFRjMU13OTdzMmhsdGJubkk5eGpERVVLYUk4Cnzh5UbU
FlgqpM8jkJ6XlsaIDCw/G3D6uJ/GRJW4gIekuhAUxpZJrc8eOA8ZuHfGrBbH3acV
tVafX5F0Kr2oOblqZ6gduZOUS52KmWH8stiBJM+e5ZZ7zRQVE4PJUKUPCzi+WdcH
zr295T//FOdicrYHdsjfziKEHzBtUCFiATW05+O2zMjYjO6cPzePcCzPWinwiID6
V+f6ngfkkQaj3wBGkzaieQJzRcdSwky21aVhGCCX/bvqx61iW2d5QAKxGbtQ2RcG
X1okr+xunAM94nzDMv46vyN97KxY7cZd4pAaOxoICc2Tfhtw6F+iS6QkQh1odJzO
7ZH+sSQCvndG+8z9shXGiHalASF5tdguM+JlEvAGljcaiAUtsQWxr9CoWiEkC6c6
NCaECSYO8Il+SXBQnSZSGJSNDhuPYCYrsjXGSAONFixuyeslAkq9x2WUaUS4H063
1QvRF7XO2tBPtgCLsSjdiGp0h+ImUaGdu6fDR7zrDsGsaAFCSFeH/rGNNXRQ2vP2
CSfPfDDCqpUSCn0WuA30BtaPLxGmZT6OjFevKzYMNDmdeq9ia/q8K0hmjLUBdN3k
tdYWbwoaf4gYbUWxSleD768b0Jgxss9Vod+sFQ+NYRksdGIeyND+aQIc312XehfA
qHFBS8nlj7eUF5bdvCYQ64z741mH4cNlGxyjPBH1x8FHnEOocJXYt1l2AZSRJmJA
c3z0QGXyuCbsrLBXWK1EKa/Juo4PGGsEVoLRhwJAQy9+i1JN0yrfRvSPyzvD4px6
wRPzlZ80MQdb2lv84WS/zcOEZmZzlLntszTRRdIfAsuaavP2Rquh4rEXABYeTZwp
5dem79s8bdW2nFsGMNz1OQKQwocyjYu1jJMHu6Gp7Ngdl1xyW7xfg0dezE1c0cIh
xt1aLER9YJp4n5to5cOH16l3mjDHnAvABx38xE9loNL3399J/evw7LxpTYQ4v2Xv
x8xnDHcqJ+deFSwyuUnMS5DkUeYuHmUl0Q2WYcfY+ibCmcgCb2ObTtuN1/ZxNYrL
OKrnmfuSvBgyuIOj5e6uWW0+Zs8dHKXu2TgV8WignxOhl5zQgCpCBlqVfO0t+NCu
Gi26hU/fhGWQ/1oQa3VkpGsypZbJpgQvfWxfcGHP/MMhnl01zzlP8/aexSY3pAxf
fz9v0IVh6xxtu3zbiiVzUsXbfG7t+xY98jMphf4AS2mWva3GWVmhhu0lS3J3P+go
YEEP4rOFHeU0Y1/6kLydTXvz4jMH0H92XQIzshd7vzQnEJPUPAzqRmw3LKYGgCI+
wZEnxJ6ckqTkGBFnxTpy9LLllwmnz2Ky87nY3XAmqxlhb2Ap1XFAlfgszmGjc+Il
KkIgoWQHTUm6QM9ta++oUTIDneOvxGd0zZsqoEhiC/7E01BNNZ6E58TeJU3fDlA3
mX6n05XjwPRpgXZfayPoAgBlZc2H4KeiynxwNZ/dWu7qz7L6Ppk6Nvtly8giTbFx
CA+tto7vq+D+CAEJ4bgyq4BCH4GL4APrhPcWp98Mko1WCiRTIKgkZxQCYvlg/LZq
LNhMacP9T1qTvNC+yR1NEMiegE3APzk6CkDpVaO9+5f/sqifNPINCMothenI9ePw
zjQLI3Mo1m73bkomytUZ7i1VstP5sEZ5LF72Sq7BpR3oQ3Gp0CAN9w==
-----END AGE ENCRYPTED FILE-----

View File

@@ -1,19 +0,0 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEdoTUQ4QSBOSk9J
RDF0bk4zNmdWRXhFTU1xUlVKeDJkTU51UDRiNjcvWlVKT3cxNzBvCk00ZEtMdlI2
L2xiS3BnT1g0aEd3VmgydCtSVERrTjNraHNmSHZicENLODAKLT4gI0crX0A0Zmst
Z3JlYXNlIE8wNCBfPz5uSy4KSGxzcnJlM1FRamI0VWlZbTJvRnRuanlwSjN0eisy
bnlHSDZZbHpIbGNZUngvS1Fqb2dzd1p6UjZKZGowOUhONgoKLS0tIE1NMUV3QlpV
dEFXMUVOTFBGSlpVSjFzRlpjVTk2dkx1VURUaFl3R014a2cKTnWIRQg4txvZZ4A5
XdwnXZqmYzOX7l2UrjmP/TJA++EURZzYtTS+q3FV9QTIj+7k0DOLf9nKq7XHlGM8
Ptkaf/s1Ly7ke+v9l9C2uf9oNI6b+m3d7QqZC5WWvRXWvl9ImaJ3xVi9AM5f5NWH
Likn6GZDir4ihAec8pAi3H2SCCjkhqF/TlLe99tp+7AAUiOjNuNlej1OUOmJNxEr
GnHePwZZqa6/F9YnYQ0tKKRsZH5whxUPYUwhHSuWa/NidBK7lqKqdaPBevfdqWLg
59gkaL9BeGG5FUXg1cPEWAvOQ5Ajae/kVH6aRi5LXJPZfA/DetWgm9mmxJ2qPpFD
/n/JCsQUOnpk66j38bWMW5uok51s61oG1OZWGpPYstoLtivKOlVpG7xgioGqIIuc
c2ZsKK/iCauwX3sGQtpX9DbILrtOnzxU2B/dclcXe2EDfSJH0u2RrefxcqPG87pI
BAosRdq7GUUISSCTccaswwG555a7/Z5gzXOwXeQYNlK1HRnkF9oBsKTUoAI2b+VO
Hdv1uw9u5JLcZt+5qSB6AxJuk8yDF0KXIqkaCiwl2fV3bBkehPru7oCJFTtVMFVZ
u0A0h+n8KJCvyRKe/Tl+lT+JHM6Mk7SkFrmsaepZPnAtfLht+bP6b0oHrUsVx/iI
r7o=
-----END AGE ENCRYPTED FILE-----

View File

@@ -1,12 +0,0 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEdoTUQ4QSBVMEhO
RWdybFg4VWJaMjRWYm1ac2pzZ3R5aFZEM1o4dUtFZ0M4QnVKRzIwCnNGRFRjb01X
QU5mQTRSb21MV3lMMERIMGI1RHFQVFFZNlVlMnk4aTdna1kKLT4gM24xLWdyZWFz
ZSA/bVhubQplcXNVN3kxOEpqY2hUbkV3MG5PNVJtRGhORVVLdFVCRnFFVjRneEFI
VWFyZTRNS280RXlnckJpdFR4V1NBSTUzCjlxZkZ3UllpZXUwczR0T3JGRzUzMEhV
U1hobC84eWtEWWZOYyt6M2c3OUZIUTlKTm41dVp5UEZVSmVwWHBmR00KCi0tLSBF
ZnJ5QzN2VXpsY3JQQ3lmbklxcldydm1MSlFlejJ2S3hVVUtka0hTM044CvqhtnFn
pZsgP6w5uKh9CtVQpcyST8hA9P24e1opfu1JN3m/8g9XiJrMT5qVYtUXkV+pr4s6
pVGsp9KScaS0T8TpLxvZoUAAdvVtdHh6GSbbszR6gZbStbWXmb6THYKHOghpK9HD
cDEoRmzcmFdwi3m26Xppmi2R6NmeKI4S6pbY95bfh2E6jn+7
-----END AGE ENCRYPTED FILE-----

View File

@@ -11,8 +11,4 @@ in
"lazyworkhorse_host_ssh_key.age".publicKeys = authorizedKeys;
"n8n_ssh_key.age".publicKeys = authorizedKeys;
"openclaw_gateway_token.age".publicKeys = authorizedKeys;
"ai.age".publicKeys = authorizedKeys;
"cloudstorege.age".publicKeys = authorizedKeys;
"homeautomation.age".age".publicKeys = authorizedKeys;
"network.age".age".publicKeys = authorizedKeys;
}

View File

@@ -1,12 +0,0 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEdoTUQ4QSBsYXVu
Sk5CSjNsbHBRZkNXUGZVeGNva3FKMUdHV0RiQzJLNHVvSHZzd0dRClNnbzZvUFFR
Ty96OXZ4R2lCMHpnclpVaEZ1TEYvb0tTSVRwdWV1amdIL0UKLT4gYTtvOmAtZ3Jl
YXNlICk5WkE7USUgSyRLCjBmTWRTdEJJV2lNbVRlL3VidmZza1NTTWhIcG1vVFY5
OUUxcG4ycTMraUUKLS0tIEVlaUdlcUFPMkZSMUpJVEFzTksrVC9lZGlMNEJ1SFBv
SFNtc0wwQ05nQ0EKYlRGYGeKlfW0lTPdpCtcTfh2NWUuyZnDFe6VAWLYL3PW42od
a+bhlgxTi9KF5FLjVA3IIdQSsKiMya+PI0VfOLWkA24ZN+4uilsQkicxnnRR1z8o
hv9G8N3eYqBsLylg8rW2Xkn4RzT/S5TyJZPFG4HsUYWtHS+iomg4HbQrgIrpIde9
Th3V8WQB1HfKs9ilg8Q/DvDckFjevWYz2OXMYx4ZHWS53huQfE/oWPZfQid6Dd06
NXcvCHPVDUszhWvcoOsSpK6vaOnuY1B32/qExw==
-----END AGE ENCRYPTED FILE-----

View File

@@ -1,9 +0,0 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEdoTUQ4QSA3VG9Z
MVFPVFc2VVJ3d0h0dmtBUnI3WHl2SzUxTkRZbjFCaGloWmV3dnd3ClcxdnVPeGd6
SU4zR0Q0K1dtVjRRVHd0VW5XSFI0dVFpTjZnYk1DNjRxTVEKLT4gQzlgRy1ncmVh
c2UKeUozOWgyUytSTVF0NjY2STBEb2VadwotLS0gblI3bmJCUWxxU3QrYTEyVFBI
Snc4NC9rTkh0NnZYbUtxUE9hRWRkelpmMAq58fmH6cK13GeD7wGLxKmx10hmJeW4
b7KqnCD1ZP7uG85s32xzVRwRG8RrG4xZo5nR9Mrtg1CoTSFfUGeFnf5xveN+Ej0X
wDVB1LwC+Q==
-----END AGE ENCRYPTED FILE-----

View File

@@ -1,11 +0,0 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEdoTUQ4QSA5dzVG
WUNvT3NlRmcrWS81bzJqSWlTekVYaDFFTE10SkI2dEgzaGpxcUI4Cmk5Y0FGYTRZ
K0NGYzY3VUp4aS9ZZGRmWTgybDJFUURva2pZNmVOS3QxdEUKLT4gPnVRTCtldGMt
Z3JlYXNlCk04OTJZeFRNeDI5aGpMVTk1ZTE0Y2FMMnFEMjlJalJpMHRlaTE4ZWIx
d2lCRGQ5RHVjcktOMGJCb1VERlNWcTYKaSt0L1Z6dVJ0QWIyZkhsYzFEVjZSQWUr
ZWpwVlo1TmhoUFJZdkEvR0gxNlVhcXF2ZTRnCi0tLSBLcmM2MThNVkdWclpHUXRr
VTF6QVk2WUZlTXpZMVNLMlpBOFc3M1o5WjZzCs9xbPlIX+u5vRSQ/z9utu+I9S2c
02DOsIb1kzxzb1OK91b8Kh4JucQSq3qkyEvRucsNn5QW8hIHDnRuND6EbPyN7p4S
YB/F0dxSqgnq
-----END AGE ENCRYPTED FILE-----

View File

@@ -9,85 +9,6 @@
openssh.authorizedKeys.keys = [
keys.users.ai-worker.main
];
# No password login - SSH key only
hashedPassword = "!";
};
users.groups.ai-worker = {};
# Enable restricted AI worker SSH access for ollama benchmarking
# SECURITY: ai-worker can only:
# - SSH into host from Hermes container
# - Run docker commands (docker exec ollama ...) via docker group
# - Run specific security audit commands
# - NO access to infra repo (no bind mount)
# - NO sudo access (no nh, nixos-rebuild, nixpkgs-fmt, nix)
# WORKFLOW: SSH from Hermes container, run docker benchmarks, return and save results to /opt/data/ai-optimizer/
services.aiWorkerAccess = true;
# Restricted sudo for ai-worker - security checks only
security.sudo.extraRules = [
{
users = [ "ai-worker" ];
commands = [
# Firewall checks
{
command = "/run/wrappers/bin/sudo iptables -L -n -v";
options = [ "NOPASSWD" ];
}
{
command = "/run/wrappers/bin/sudo iptables -S";
options = [ "NOPASSWD" ];
}
# Fail2ban status
{
command = "/run/current-system/sw/bin/fail2ban-client status";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/fail2ban-client status *";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/fail2ban-client get * banned";
options = [ "NOPASSWD" ];
}
# Log inspection
{
command = "/run/current-system/sw/bin/journalctl -t kernel -n 100";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/journalctl -u fail2ban -n 50";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/journalctl -u firewall -n 50";
options = [ "NOPASSWD" ];
}
# SSH config verification
{
command = "/run/current-system/sw/bin/sshd -T";
options = [ "NOPASSWD" ];
}
# Docker service checks
{
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";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/cat /proc/net/tcp";
options = [ "NOPASSWD" ];
}
];
}
];
}