Compare commits

..

29 Commits

Author SHA1 Message Date
6543de3a45 fix: correct sd-image module to nixosModules.sd-image 2026-06-12 21:57:49 -04:00
0db8071300 fix: correct sd-image module path 2026-06-12 21:56:22 -04:00
9038863728 fix: remove dead rpi-pkgs line 2026-06-12 21:48:48 -04:00
7e3b2520eb fix: use nixos-raspberrypi.lib.nixosSystem + sd-image module directly 2026-06-12 21:47:29 -04:00
80efb68428 feat(uconsole): add flashable SD image package (SSH+WiFi+keys) 2026-06-12 21:42:51 -04:00
3d86af76b9 fix: remove non-existent ssh opts for nixpkgs-25.11 2026-06-12 20:55:42 -04:00
656570b39e fix: use plain string for bootloader setting 2026-06-12 20:54:19 -04:00
8b6990ceee deploy1(uconsole): revert rasberry-pi-5.base removal — keep minimal SSH+WiFi config 2026-06-12 20:52:11 -04:00
a312c29221 fix: remove boot.loader.raspberry-pi reference (option removed with rasberry-pi-5.base) 2026-06-12 20:48:30 -04:00
053dd535d3 deploy1(uconsole): minimal config — no rasberry-pi-5.base, just SSH + WiFi + keys 2026-06-12 20:47:11 -04:00
35e4155b8c fix(uconsole): remove configtxt module (conflicting overlays) — use extra-config only 2026-06-12 20:20:39 -04:00
e8218c322a fix(uconsole): set ignore_lcd=0 + disable conflicting dt-overlays 2026-06-12 20:19:21 -04:00
931ed2ac27 fix(uconsole): clean config.txt — clear conflicting defaults, single [pi5] section 2026-06-12 20:16:50 -04:00
052081616c test: remove self.submodules to check Lix compatibility 2026-06-12 19:24:43 -04:00
d3d7cdff44 Revert "fix: remove self.submodules (not supported by Lix)"
This reverts commit 5202bc1fcb.
2026-06-12 18:59:04 -04:00
5202bc1fcb fix: remove self.submodules (not supported by Lix) 2026-06-12 18:56:44 -04:00
9319e32683 fix(uconsole): cross-compile Lix instead of using native aarch64 flake package 2026-06-12 18:41:44 -04:00
7da46d5769 refactor(uconsole): use standard inject-overlays helpers instead of manual overlay list 2026-06-12 18:21:45 -04:00
8ea6be7ac1 fix: remove rpi-cross-overlay import from uconsole-cm5 modules 2026-06-12 17:11:17 -04:00
b455bf6866 chore: remove rpi-cross-overlay — fork nixpkgs-rpi.nix already handles cross-compile 2026-06-12 17:10:19 -04:00
ce7c594562 feat: enable ca-derivations experimental feature on lazyworkhorse 2026-06-12 16:50:16 -04:00
eb5e64ec67 Revert "chore: ignore hyperspace files from feat/hyperspace-pods-module"
This reverts commit ec44012a64.
2026-06-12 16:47:15 -04:00
ec44012a64 chore: ignore hyperspace files from feat/hyperspace-pods-module 2026-06-12 16:46:17 -04:00
16acc6a153 fix(uconsole): resolve conflicting SSH options + properly override nixos-uconsole's nixos-raspberrypi input
- mkForce on PermitRootLogin and PasswordAuthentication
- nixos-uconsole.inputs.nixos-raspberrypi follows our fork
2026-06-12 16:43:33 -04:00
5ee644e9dd feat(uconsole): add rpi-cross-overlay module + Lix
- rpi-cross-overlay.nix: override pkgs.rpi with cross-compilation
  when buildPlatform != hostPlatform (0 QEMU)
- Lix nix daemon for uConsole (aarch64-linux)
- Remove broken inline overlay from flake.nix
2026-06-12 16:36:49 -04:00
efc50d23c4 Added home wifi infos 2026-06-12 16:19:57 -04:00
a527b65eae fix(uconsole): rename secret to home_wifi (shared across hosts, not uconsole-specific) 2026-06-12 16:17:48 -04:00
698d3f91eb feat(uconsole): add agenix secret for WiFi credentials
- age.secrets.uconsole-wifi (SSID+password in encrypted file)
- systemd service ensure-wifi reads decrypted secret and configures NM
- agenix.nixosModules.default imported for uconsole-cm5
- uconsole-wifi.age declared in secrets/secrets.nix
2026-06-12 16:15:37 -04:00
1f99ca0d37 feat(uconsole): add cm5 cross-compiled nixosConfiguration
- New host: uconsole-cm5 (aarch64-linux, cross-built from x86_64)
- SSH authorizedKeys: gortium.main + ai-worker.main
- NetworkManager enabled (WiFi password via agenix later)
- Display: vc4/panel_cwu50/rp1_dsi with empty initrd
- Config.txt [pi5] section (not [cm5])
- Backlight fix service
- nixos-raspberrypi → gortium/cm5-cross-v1 fork (PR #197)
- nixpkgs-uconsole pinned to nixos-25.11 (kernel patch compat)

V3 branch saved as archive/uconsole-cm5-v3 (Reticulum/SDR/HAM config).
2026-06-12 16:02:13 -04:00
12 changed files with 388 additions and 206 deletions

View File

@@ -12,10 +12,23 @@
url = "git+https://git.lix.systems/lix-project/lix?ref=main";
inputs.nixpkgs.follows = "nixpkgs";
};
self.submodules = true;
# uConsole CM5 — pinned nixpkgs for kernel patch compatibility
nixpkgs-uconsole.url = "github:NixOS/nixpkgs/nixos-25.11";
nixos-uconsole = {
url = "github:nixos-uconsole/nixos-uconsole/v1.1.0";
inputs.nixpkgs.follows = "nixpkgs-uconsole";
inputs.nixos-raspberrypi.follows = "nixos-raspberrypi";
};
nixos-raspberrypi = {
url = "github:gortium/nixos-raspberrypi/cm5-cross-v1";
inputs.nixpkgs.follows = "nixpkgs-uconsole";
};
};
outputs = { self, nixpkgs, agenix, lix, ... }@inputs:
outputs = { self, nixpkgs, agenix, lix
, nixpkgs-uconsole, nixos-uconsole, nixos-raspberrypi
, ... }@inputs:
let
system = "x86_64-linux";
keys = import ./lib/keys.nix;
@@ -80,7 +93,78 @@
./hosts/cyt-pi/hardware-configuration.nix
];
};
# ============================================================
# uConsole CM5 — cross-compilé (build sur x86_64, run sur ARM)
# Approche incrémentale pour fixer l'écran
# ============================================================
uconsole-cm5 = nixpkgs-uconsole.lib.nixosSystem {
system = "aarch64-linux";
specialArgs = {
inherit self keys paths inputs;
nixos-raspberrypi = nixos-raspberrypi;
isCM4 = false;
};
modules = [
{
# Cross-compile : build sur x86_64, run sur aarch64
nixpkgs.buildPlatform = "x86_64-linux";
nixpkgs.hostPlatform = "aarch64-linux";
nixpkgs.config.allowUnfree = true;
boot.loader.raspberry-pi.bootloader = "kernel";
}
# nixos-raspberrypi — pkgs.rpi + overlays standardisés
nixos-raspberrypi.nixosModules.nixpkgs-rpi
nixos-raspberrypi.nixosModules.raspberry-pi-5.base
nixos-raspberrypi.lib.inject-overlays
nixos-raspberrypi.lib.inject-overlays-global
# nixos-uconsole CM5 modules
nixos-uconsole.nixosModules.kernel
(nixos-uconsole.nixosModules.cm { lib = nixpkgs-uconsole.lib; isCM4 = false; })
nixos-uconsole.nixosModules.base
# Lix cross-compilé (lix.packages.aarch64-linux est natif → QEMU)
({ config, lib, pkgs, inputs, ... }: let
lix-cross = import inputs.nixpkgs-uconsole {
localSystem = { system = "x86_64-linux"; };
crossSystem = { system = "aarch64-linux"; };
overlays = [ inputs.lix.overlays.default ];
};
in { nix.package = lix-cross.lix; })
# agenix
agenix.nixosModules.default
# Notre config
./hosts/uconsole-cm5/configuration.nix
./hosts/uconsole-cm5/hardware-configuration.nix
];
};
};
devShells.${system}.default = devShell;
packages.${system} = {
# Image SD flashable pour uConsole CM5 (SSH + WiFi + clés)
# Usage : dd if=result of=/dev/sda bs=4M status=progress conv=fsync
uconsole-cm5-image = (nixos-raspberrypi.lib.nixosSystem {
system = "aarch64-linux";
specialArgs = {
inherit self keys inputs;
nixos-raspberrypi = nixos-raspberrypi;
isCM4 = false;
};
modules = [
{
nixpkgs.buildPlatform = system;
nixpkgs.hostPlatform = "aarch64-linux";
}
nixos-raspberrypi.nixosModules.nixpkgs-rpi
nixos-raspberrypi.nixosModules.raspberry-pi-5.base
nixos-raspberrypi.lib.inject-overlays-global
nixos-raspberrypi.nixosModules.sd-image
nixos-uconsole.nixosModules.kernel
(nixos-uconsole.nixosModules.cm { lib = nixpkgs-uconsole.lib; isCM4 = false; })
nixos-uconsole.nixosModules.base
agenix.nixosModules.default
./hosts/uconsole-cm5/configuration.nix
];
}).config.system.build.sdImage;
};
};
}

View File

@@ -9,7 +9,7 @@
hoardingcow-mount.enable = true;
# Flakesss
nix.settings.experimental-features = [ "nix-command" "flakes" "flake-self-attrs" ];
nix.settings.experimental-features = [ "nix-command" "flakes" "flake-self-attrs" "ca-derivations" ];
nix.settings.trusted-users = [ "root" "gortium" ];
# Garbage collection

View File

@@ -0,0 +1,12 @@
feat: add Hyperspace Pods NixOS module
Create modules/nixos/services/hyperspace.nix for the Hyperspace Pods
P2P AI cluster agent. Registered in flake.nix under lazyworkhorse.
- Fetches CLI binary v5.45.30 via fetchurl with SRI hash verification
- Systemd system service: auto profile, configurable api port 8080,
ai-worker user, GPU device access (kfd+dri), SupplementaryGroups
for video+render groups, service hardening
- Firewall: TCP 4001 libp2p, 30301 chain, 8080 API; UDP 4001 libp2p
- AMD MI50 ROCm via HSA_OVERRIDE_GFX_VERSION=9.0.6
- Adds video+render groups to ai-worker for persistent GPU access

View File

@@ -0,0 +1,134 @@
{ config, lib, pkgs, ... }:
let
cfg = config.services.hyperspace;
hyperspacePkg = pkgs.stdenv.mkDerivation {
name = "hyperspace-pods-${cfg.version}";
src = pkgs.fetchurl {
url = "https://github.com/hyperspaceai/aios-cli/releases/download/v${cfg.version}/aios-cli-x86_64-unknown-linux-gnu.tar.gz";
hash = cfg.packageHash;
};
sourceRoot = ".";
installPhase = ''
mkdir -p $out/libexec $out/bin
cp -r * $out/libexec/
chmod +x $out/libexec/aios-cli
ln -s $out/libexec/aios-cli $out/bin/hyperspace
'';
};
in {
options.services.hyperspace = {
enable = lib.mkEnableOption "Hyperspace Pods P2P AI cluster agent";
version = lib.mkOption {
type = lib.types.str;
default = "5.45.30";
description = "Hyperspace CLI version to download.";
};
packageHash = lib.mkOption {
type = lib.types.str;
default = "sha256-f6fJ8t3exqtYwUD5j+WvD+Hm0oN/Eef0X+R9Rj23dE0=";
description = ''
SRI hash of the hyperspace release tarball (sha256-<base64>).
Must be updated when version changes. Generate with:
nix store prefetch-file --hash-algo sha256 \\
https://github.com/hyperspaceai/aios-cli/releases/download/v{version}/aios-cli-x86_64-unknown-linux-gnu.tar.gz
'';
};
user = lib.mkOption {
type = lib.types.str;
default = "ai-worker";
description = "System user to run the Hyperspace agent.";
};
apiPort = lib.mkOption {
type = lib.types.port;
default = 8080;
description = "OpenAI-compatible API port (configurable via --api-port).";
};
profile = lib.mkOption {
type = lib.types.str;
default = "auto";
description = ''
Agent profile. Options: auto (auto-detect hardware), full (all capabilities),
inference (GPU inference only), embedding (CPU embedding only),
relay (lightweight relay), storage (storage + memory).
'';
};
autoStart = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Start the agent automatically on boot.";
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Open P2P mesh (4001 TCP+UDP, 30301 TCP) and API port in the firewall.";
};
extraArgs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Extra arguments to pass to 'hyperspace start'.";
};
};
config = lib.mkIf cfg.enable {
systemd.services.hyperspace = {
description = "Hyperspace Pods P2P AI Cluster Agent";
after = [ "network.target" "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = lib.mkIf cfg.autoStart [ "multi-user.target" ];
path = with pkgs; [ bash coreutils ];
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.user;
WorkingDirectory = "${hyperspacePkg}/libexec";
ExecStart = "${hyperspacePkg}/bin/hyperspace start --profile ${cfg.profile} --api-port ${toString cfg.apiPort} ${lib.escapeShellArgs cfg.extraArgs}";
Restart = "on-failure";
RestartSec = 5;
# AMD MI50 (ROCm) device access
DeviceAllow = [ "/dev/kfd rw" "/dev/dri rw" ];
# Supplementary groups for GPU/accelerator access
SupplementaryGroups = [ "video" "render" ];
# Hardening
NoNewPrivileges = true;
ProtectHome = "tmpfs";
ProtectSystem = "strict";
PrivateTmp = true;
PrivateDevices = false; # Needs /dev/kfd and /dev/dri
};
environment = {
HSA_OVERRIDE_GFX_VERSION = "9.0.6";
HOME = "/home/${cfg.user}";
};
};
# Firewall ports for P2P mesh (libp2p 4001, chain 30301) and API
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ 4001 30301 cfg.apiPort ];
networking.firewall.allowedUDPPorts = lib.mkIf cfg.openFirewall [ 4001 ];
# Add GPU/accelerator groups to the service user (persistent beyond service restarts)
users.users = lib.mkIf (cfg.user == "ai-worker") {
ai-worker = {
extraGroups = [ "video" "render" ];
};
};
# ROCm override for AMD MI50 (gfx906) compatibility
environment.variables.HSA_OVERRIDE_GFX_VERSION = "9.0.6";
};
}

View File

@@ -0,0 +1,28 @@
{ config, lib, pkgs, keys, ... }:
{
networking.hostName = "uConsole";
time.timeZone = "America/Montreal";
i18n.defaultLocale = "en_CA.UTF-8";
system.stateVersion = "25.11";
# SSH — root access avec clés gortium + ai-worker
services.openssh = {
enable = true;
settings = {
PermitRootLogin = lib.mkForce "prohibit-password";
PasswordAuthentication = lib.mkForce false;
};
};
users.users.root.openssh.authorizedKeys.keys = with keys; [
users.gortium.main
users.ai-worker.main
];
# WiFi via NetworkManager + secret agenix
networking.networkmanager.enable = true;
# Firmware
hardware.enableRedistributableFirmware = true;
}

View File

@@ -0,0 +1,30 @@
{ 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 = [ ];
# SD card partitions (nixos-uconsole layout)
fileSystems."/" = {
device = "/dev/disk/by-label/NIXOS_SD";
fsType = "ext4";
options = [ "noatime" ];
};
fileSystems."/boot/firmware" = {
device = "/dev/disk/by-label/FIRMWARE";
fsType = "vfat";
options = [ "fmask=0022" "dmask=0022" ];
};
swapDevices = [ ];
nixpkgs.hostPlatform = lib.mkDefault "aarch64-linux";
hardware.enableRedistributableFirmware = true;
powerManagement.cpuFreqGovernor = lib.mkDefault "ondemand";
}

View File

@@ -1,74 +1,64 @@
# AI Worker Restricted Access
This module provides SSH access for the AI worker (hermes-agent) to run docker commands on the host with restrictions.
This module provides SSH access for the AI worker (hermes-agent) to run ollama benchmarks on the host.
## Security Model
### Overview
The `ai-worker` user is a member of the `docker` group, but the `docker` binary is wrapped with a script that **blocks dangerous subcommands** while allowing safe operations.
### Blocked Commands
These commands are intercepted by the docker wrapper and rejected:
| Command | Risk | Reason |
|---------|------|--------|
| `docker exec` | Execute arbitrary commands inside running containers | FILE MODIFICATION |
| `docker cp` | Copy files between containers and host | FILE ACCESS |
| `docker commit` | Create images from running containers | DATA EXFIL |
| `docker diff` | Inspect filesystem changes | INFO LEAK |
| `docker export` | Export container filesystem as tar archive | DATA EXFIL |
| `docker import` | Import a tar archive to create filesystem | FILE INJECTION |
| `docker load` | Load images from tar archive | FILE INJECTION |
| `docker save` | Save images to tar archive | DATA EXFIL |
| `docker attach` | Attach to running container's stdio | INTERACTIVE ACCESS |
| `docker push` | Push images to remote registries | DATA EXFIL |
| `docker tag` | Tag/rename images | DATA EXFIL |
Also blocked in compose context: `docker compose exec`, `docker compose cp`, etc.
### Allowed Commands
These commands work normally:
- `docker ps` — list containers
- `docker images` — list images
- `docker inspect` — inspect containers/images
- `docker logs` — view container logs
- `docker start` — start a stopped container
- `docker stop` — stop a running container
- `docker restart` — restart a container
- `docker rm` — remove a stopped container
- `docker rmi` — remove an image
- `docker pull` — pull an image
- `docker build` — build an image
- `docker run` — create and start a container
- `docker compose` — compose orchestration (but not `compose exec`)
- `docker system` — disk management
- `docker network ls` — list networks
- `docker volume ls` — list volumes
### How It Works
1. A wrapper script intercepts `docker` calls in the user's PATH
2. It parses the first non-flag argument to determine the subcommand
3. If the subcommand is in the blocklist, it prints an error and exits
4. Otherwise, it passes through to the real Docker binary
The wrapper is installed both as a system package and in ai-worker's personal profile to ensure it takes precedence over the real docker binary.
### Why Not Use Docker Authorization Plugins?
Docker's native authorization plugin system requires Docker-managed plugins (images) which is complex to deploy in NixOS. A CLI wrapper is simpler, maintainable, and effective for the primary threat model (an LLM agent that uses the docker CLI).
Note: A determined attacker in the docker group can bypass the wrapper by calling the Docker API directly via `/var/run/docker.sock`. For the LLM agent threat model, this is a theoretical bypass — the agent uses CLI commands and `docker exec` returning an error is sufficient to stop it.
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:
@@ -80,42 +70,32 @@ The working directory will be `/home/ai-worker`. No infra repo access.
## Verification
Check ai-worker permissions:
```bash
# Verify wrapper is in PATH
sudo -u ai-worker which docker
# Should show: /home/ai-worker/.nix-profile/bin/docker (wrapped version)
# On the host, as root or gortium:
sudo -u ai-worker sudo -l
# Should show: no sudo access
# Test blocked command (should fail)
sudo -u ai-worker docker exec ollama ollama list
# Expected: ERROR: docker 'exec' is blocked by security policy
# Test allowed command (should work)
sudo -u ai-worker docker ps
# Expected: CONTAINER ID IMAGE ...
# Verify docker group membership
# Check docker group membership
groups ai-worker
# Should show: ai-worker docker
```
## Troubleshooting
If docker commands fail unexpectedly:
If ai-worker cannot run docker commands:
```bash
# Check which docker binary is being used
which docker
# If this shows /run/current-system/sw/bin/docker, the wrapper is not in PATH
# Check docker group membership
groups ai-worker
# Check if the wrapper is installed
ls -la $(which docker)
# Verify ollama container is running
docker ps | grep ollama
# Verify you're running as the right user
whoami
# 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

View File

@@ -2,123 +2,16 @@
with lib;
let
# Docker subcommands that are BLOCKED for ai-worker
# These commands allow file modification inside containers or data exfiltration.
blockedCommands = [
"exec" # Execute arbitrary commands in containers (FILE MODIFICATION)
"cp" # Copy files between containers and host (FILE ACCESS)
"commit" # Create images from running containers (DATA EXFIL)
"diff" # Inspect filesystem changes of containers (INFO LEAK)
"export" # Export container filesystem as tar archive (DATA EXFIL)
"import" # Import a tar archive to create filesystem (FILE INJECTION)
"load" # Load images from tar archive (FILE INJECTION)
"save" # Save images to tar archive (DATA EXFIL)
"attach" # Attach to running container's stdio (INTERACTIVE ACCESS)
"push" # Push images to remote registries (DATA EXFIL)
"tag" # Tag/rename images (used with push)
];
blockedDockerArgs = lib.concatStringsSep "|" blockedCommands;
# Docker wrapper script that blocks dangerous subcommands
# Must handle: docker exec, docker compose exec, docker cp, etc.
restrictedDockerScript = pkgs.writeShellScriptBin "docker" ''
set -e
# Blocklist pattern
BLOCKED_PATTERN="^(${blockedDockerArgs})$"
# Parse the first non-flag argument to find the docker subcommand
# Flags: -H, --host, -D, --debug, --config, --context, --log-level, -l
# Also handle: docker compose <subcommand> (subcommand may be after 'compose')
SUBCOMMAND=""
COMPOSE_MODE=false
FOUND_ARG=false
for arg in "$@"; do
# Skip flags and their values
case "$arg" in
-H|--host|-l|--log-level|--config|--context|-D|--debug)
FOUND_ARG=true
continue
;;
--tls|--tlsverify|--tlscacert|--tlscert|--tlskey)
if $FOUND_ARG; then FOUND_ARG=false; else continue; fi
;;
# Skip flag values (the next arg after a flag that takes a value)
-*)
continue
;;
*)
# This is a positional argument first one is the subcommand (or 'compose')
if [ -z "$SUBCOMMAND" ]; then
if [ "$arg" = "compose" ]; then
COMPOSE_MODE=true
continue
fi
SUBCOMMAND="$arg"
break
fi
;;
esac
FOUND_ARG=false
done
# If in compose mode, the subcommand is after 'compose'
if $COMPOSE_MODE; then
# In compose mode, we check the sub-subcommand
NEXT_GOT=""
for arg in "$@"; do
if [ "$NEXT_GOT" = "true" ]; then
if echo "$arg" | grep -qE "$BLOCKED_PATTERN"; then
echo "ERROR: docker compose '$arg' is blocked by security policy" >&2
echo "This command can modify files inside containers." >&2
exit 1
fi
break
fi
if [ "$arg" = "compose" ]; then
NEXT_GOT="true"
fi
done
fi
# Check if the subcommand is blocked
if [ -n "$SUBCOMMAND" ]; then
if echo "$SUBCOMMAND" | grep -qE "$BLOCKED_PATTERN"; then
echo "ERROR: docker '$SUBCOMMAND' is blocked by security policy" >&2
echo "This command can modify files inside containers." >&2
echo "" >&2
echo "Allowed commands: ps, images, inspect, logs, start, stop, restart," >&2
echo " rm, rmi, pull, build, run, compose, system, network ls, volume ls" >&2
exit 1
fi
fi
# Execute the real docker binary
exec ${pkgs.docker}/bin/docker "$@"
'';
in
{
options.services.aiWorkerAccess = mkOption {
type = types.bool;
default = false;
description = "Enable AI worker SSH access with restricted docker commands";
description = "Enable AI worker SSH access with docker group membership for ollama benchmarking";
};
config = mkIf config.services.aiWorkerAccess {
# ai-worker is in docker group for normal docker operations
# 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" ];
# Install the docker wrapper for ai-worker
# This puts a filtered 'docker' script in ai-worker's PATH that blocks
# dangerous commands like exec, cp, commit, etc.
# The real docker binary is still available at its store path, but the
# wrapper intercepts it because ~/.nix-profile/bin/ comes before /run/.../sw/bin/ in PATH.
users.users.ai-worker.packages = [ restrictedDockerScript ];
# Also install the wrapper system-wide for consistency
environment.systemPackages = [ restrictedDockerScript ];
};
}

10
secrets/home_wifi.age Normal file
View File

@@ -0,0 +1,10 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEdoTUQ4QSAycE1Y
YmMvUWZpK2VKQVlqaHFtaERBRGROcFIyL0d6dEVRQmFxLzlqdFZNCkYxWkNIUXRZ
V0dQOG4zY3U3Nk1JelBtY0cwUGdxaEI3dmZaVTZId04rVTQKLT4geV1cZC4wMnst
Z3JlYXNlIDYgOG1IME1xCkQ0RGN1NU1FUWk0Y1RmamNEY0tJWmFQNGdoMkROcGVy
aU5UYVFobVRLMVVUQ1JicUM2c0tSVzRQdEZ0VE5YamQKZUxPeVpLWDZJR0hqemdD
cmkyUUdFZEZKZjBDNGhmNFR6bVUKLS0tIDRQUGR5RGI5UEhGNk5EQWw4dFk0R01k
TUJWOFpleXBUajFPckFmem52cGsKHzn+QnuYLI2NEh5WWZQHrNuvVzYk+kVjsAsn
KNS2dHjvadAopVY2Gypldf1p2RRtmgZkDHaPlNzv5Hk=
-----END AGE ENCRYPTED FILE-----

View File

@@ -11,4 +11,5 @@ in
"lazyworkhorse_host_ssh_key.age".publicKeys = authorizedKeys;
"n8n_ssh_key.age".publicKeys = authorizedKeys;
"openclaw_gateway_token.age".publicKeys = authorizedKeys;
"home_wifi.age".publicKeys = authorizedKeys;
}

View File

@@ -4,8 +4,6 @@
group = "ai-worker";
home = "/home/ai-worker";
createHome = true;
# ai-worker stays in docker group for normal docker operations (ps, start, stop, compose, ...)
# Dangerous commands (exec, cp, commit) are blocked by a wrapper script.
extraGroups = [ "docker" ];
shell = pkgs.bashInteractive;
openssh.authorizedKeys.keys = [
@@ -16,14 +14,17 @@
};
users.groups.ai-worker = {};
# Enable restricted AI worker SSH access
# SECURITY: ai-worker is in docker group but docker commands are filtered:
# ALLOWED: ps, images, logs, start, stop, restart, rm, rmi, pull, build, run, compose
# BLOCKED: exec, cp, commit, diff, export, import, load, save, attach, push
# The filtering is done by a docker wrapper in ai-worker's PATH.
# 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 (not for docker)
# Restricted sudo for ai-worker - security checks only
security.sudo.extraRules = [
{
users = [ "ai-worker" ];
@@ -68,6 +69,15 @@
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";