Compare commits

..

1 Commits

Author SHA1 Message Date
993b9c559c fix: restrict docker commands for ai-worker (wrapper blacklist)
SECURITY CHANGE: Keep ai-worker in docker group but block dangerous
docker subcommands via a wrapper script.

Approach:
- docker group membership preserved (ps, start, stop, compose still work)
- Docker binary wrapped with a script that blocks dangerous subcommands
- BLOCKED: exec, cp, commit, diff, export, import, load, save, attach, push, tag
- ALLOWED: ps, images, inspect, logs, start, stop, restart, rm, rmi,
  pull, build, run, compose, system, network ls, volume ls

The wrapper is installed in both system packages and ai-worker's
personal profile to ensure it takes precedence over the real docker.
This is effective for the LLM agent threat model — the agent uses CLI
commands and blocked subcommands simply return an error.

Files modified:
- users/ai-worker.nix — restored docker group, kept sudo audit rules
- modules/nixos/security/ai-worker-restricted.nix — added docker wrapper
  script with blacklist logic and NixOS module integration
- modules/nixos/security/README-ai-worker.md — documentation update
2026-05-20 20:42:32 -04:00
12 changed files with 205 additions and 387 deletions

View File

@@ -12,23 +12,10 @@
url = "git+https://git.lix.systems/lix-project/lix?ref=main"; url = "git+https://git.lix.systems/lix-project/lix?ref=main";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
self.submodules = true;
# 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 outputs = { self, nixpkgs, agenix, lix, ... }@inputs:
, nixpkgs-uconsole, nixos-uconsole, nixos-raspberrypi
, ... }@inputs:
let let
system = "x86_64-linux"; system = "x86_64-linux";
keys = import ./lib/keys.nix; keys = import ./lib/keys.nix;
@@ -93,78 +80,7 @@
./hosts/cyt-pi/hardware-configuration.nix ./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; 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; 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" ];
# Garbage collection # Garbage collection

View File

@@ -1,12 +0,0 @@
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

@@ -1,134 +0,0 @@
{ 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

@@ -1,28 +0,0 @@
{ 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

@@ -1,30 +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 = [ ];
# 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,64 +1,74 @@
# AI Worker Restricted Access # AI Worker Restricted Access
This module provides SSH access for the AI worker (hermes-agent) to run ollama benchmarks on the host. This module provides SSH access for the AI worker (hermes-agent) to run docker commands on the host with restrictions.
## Security Model ## Security Model
The `ai-worker` user has: ### 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.
### Filesystem Access ### Filesystem Access
- **Home directory**: `/home/ai-worker` (standard user home) - **Home directory**: `/home/ai-worker` (standard user home)
- **No bind mounts**: Cannot access `/home/gortium/infra` or other host files - **No bind mounts**: Cannot access `/home/gortium/infra` or other host files
- **Cannot access**: Any files outside standard system paths - **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 ## SSH Access
Connect as: Connect as:
@@ -70,32 +80,42 @@ The working directory will be `/home/ai-worker`. No infra repo access.
## Verification ## Verification
Check ai-worker permissions:
```bash ```bash
# On the host, as root or gortium: # Verify wrapper is in PATH
sudo -u ai-worker sudo -l sudo -u ai-worker which docker
# Should show: no sudo access # Should show: /home/ai-worker/.nix-profile/bin/docker (wrapped version)
# Check docker group membership # 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
groups ai-worker groups ai-worker
# Should show: ai-worker docker # Should show: ai-worker docker
``` ```
## Troubleshooting ## Troubleshooting
If ai-worker cannot run docker commands: If docker commands fail unexpectedly:
```bash ```bash
# Check docker group membership # Check which docker binary is being used
groups ai-worker which docker
# If this shows /run/current-system/sw/bin/docker, the wrapper is not in PATH
# Verify ollama container is running # Check if the wrapper is installed
docker ps | grep ollama ls -la $(which docker)
# Test docker access # Verify you're running as the right user
sudo -u ai-worker docker exec ollama ollama list whoami
``` ```
If SSH connection fails: If SSH connection fails:
```bash ```bash
# Check SSH key is authorized # Check SSH key is authorized
cat /home/ai-worker/.ssh/authorized_keys cat /home/ai-worker/.ssh/authorized_keys

View File

@@ -2,16 +2,123 @@
with lib; 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 { options.services.aiWorkerAccess = mkOption {
type = types.bool; type = types.bool;
default = false; default = false;
description = "Enable AI worker SSH access with docker group membership for ollama benchmarking"; description = "Enable AI worker SSH access with restricted docker commands";
}; };
config = mkIf config.services.aiWorkerAccess { config = mkIf config.services.aiWorkerAccess {
# ai-worker is member of docker group - can run docker commands via SSH # ai-worker is in docker group for normal docker operations
# No bind mounts, no sudo access - docker-only for ollama benchmarking
users.groups.docker.members = [ "ai-worker" ]; 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 ];
}; };
} }

View File

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

View File

@@ -11,5 +11,4 @@ in
"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; "openclaw_gateway_token.age".publicKeys = authorizedKeys;
"home_wifi.age".publicKeys = authorizedKeys;
} }

View File

@@ -4,6 +4,8 @@
group = "ai-worker"; group = "ai-worker";
home = "/home/ai-worker"; home = "/home/ai-worker";
createHome = true; 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" ]; extraGroups = [ "docker" ];
shell = pkgs.bashInteractive; shell = pkgs.bashInteractive;
openssh.authorizedKeys.keys = [ openssh.authorizedKeys.keys = [
@@ -14,17 +16,14 @@
}; };
users.groups.ai-worker = {}; users.groups.ai-worker = {};
# Enable restricted AI worker SSH access for ollama benchmarking # Enable restricted AI worker SSH access
# SECURITY: ai-worker can only: # SECURITY: ai-worker is in docker group but docker commands are filtered:
# - SSH into host from Hermes container # ALLOWED: ps, images, logs, start, stop, restart, rm, rmi, pull, build, run, compose
# - Run docker commands (docker exec ollama ...) via docker group # BLOCKED: exec, cp, commit, diff, export, import, load, save, attach, push
# - Run specific security audit commands # The filtering is done by a docker wrapper in ai-worker's PATH.
# - 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; services.aiWorkerAccess = true;
# Restricted sudo for ai-worker - security checks only # Restricted sudo for ai-worker - security checks only (not for docker)
security.sudo.extraRules = [ security.sudo.extraRules = [
{ {
users = [ "ai-worker" ]; users = [ "ai-worker" ];
@@ -69,15 +68,6 @@
command = "/run/current-system/sw/bin/sshd -T"; command = "/run/current-system/sw/bin/sshd -T";
options = [ "NOPASSWD" ]; 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 # Network diagnostics
{ {
command = "/run/current-system/sw/bin/ss -tlnp"; command = "/run/current-system/sw/bin/ss -tlnp";