Compare commits

..

1 Commits

Author SHA1 Message Date
9459839d74 fix: restrict docker commands for ai-worker user
Remove ai-worker from docker group and enforce sudo whitelist.

SECURITY: Being in the docker group gives unrestricted access to the
Docker daemon socket (/var/run/docker.sock), allowing any docker command:
docker exec, docker cp, docker run -v /:/host, docker commit, etc.

Changes:
- Remove extraGroups = ["docker"] from ai-worker user definition
- Add comprehensive sudo NOPASSWD whitelist for safe docker subcommands
  ALLOWED: ps, inspect, logs, images, info, version, stats, start, stop,
  restart, rm, rmi, wait, pull, build, run, compose, system,
  network ls, volume ls
  BLOCKED (implicitly): exec, cp, commit, diff, export, import, load,
  save, attach, push, tag, create, plugin, network create, volume create
- Update ai-worker-restricted.nix module to reflect new approach
- Update README-ai-worker.md with new security model and examples

All docker commands must now be prefixed with sudo.
The Hermes agent's host_run tool needs to be updated to prepend sudo.
2026-05-20 20:34:19 -04:00
34 changed files with 561 additions and 2253 deletions

4
.gitmodules vendored
View File

@@ -1,7 +1,3 @@
[submodule "assets/compose"]
path = assets/compose
url = ssh://git@code.lazyworkhorse.net:2222/gortium/compose.git
[submodule "assets/dotfiles"]
path = assets/dotfiles
url = ssh://git@code.lazyworkhorse.net:2222/gortium/dotfiles.git
branch = master

Submodule assets/dotfiles deleted from 3c06c3938c

106
assets/ollama/Dockerfile Normal file
View File

@@ -0,0 +1,106 @@
# ollama-gfx906/Dockerfile
#
# Custom ollama image with ROCm 6.1 + gfx906 (MI50) support.
# The official ollama/rocm image ships ROCm 7.2 which dropped gfx906.
# This uses v0.23.2's native CMake build system with AMDGPU_TARGETS including gfx906.
#
# Build: docker build -t ollama/ollama:rocm-gfx906 ai/ollama
FROM rocm/dev-ubuntu-22.04:6.1.2-complete AS builder
# Build dependencies (CMake, Ninja, Go)
ARG CMAKEVERSION=3.31.2
ARG NINJAVERSION=1.12.1
ARG GOLANG_VERSION=1.22.0
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
curl git ccache build-essential pkg-config unzip \
&& rm -rf /var/lib/apt/lists/*
# Install CMake from official binaries
RUN curl -fsSL https://github.com/Kitware/CMake/releases/download/v${CMAKEVERSION}/cmake-${CMAKEVERSION}-linux-x86_64.tar.gz \
| tar xz -C /usr/local --strip-components 1
# Install Ninja
RUN curl -fsSL -o /tmp/ninja.zip \
https://github.com/ninja-build/ninja/releases/download/v${NINJAVERSION}/ninja-linux.zip \
&& unzip /tmp/ninja.zip -d /usr/local/bin && rm /tmp/ninja.zip
# Install Go
RUN curl -fsSL https://go.dev/dl/go${GOLANG_VERSION}.linux-amd64.tar.gz \
| tar xz -C /usr/local
ENV PATH=/usr/local/go/bin:$PATH
ARG OLLAMA_VERSION=v0.23.2
RUN git clone --depth 1 --branch ${OLLAMA_VERSION} https://github.com/ollama/ollama.git /build
WORKDIR /build
# ROCm paths
ENV HIP_PATH=/opt/rocm
ENV ROCM_PATH=/opt/rocm
ENV CMAKE_GENERATOR=Ninja
ENV LDFLAGS=-s
# Step 1: Build CPU backends with GCC (no ROCm preset)
# Pre-set CMAKE_HIP_COMPILER="" to prevent check_language(HIP) from
# finding a HIP compiler (it searches /opt/rocm even without PATH).
# Remove /opt/rocm from PATH to prevent find_program from finding hipcc.
RUN mkdir -p build-cpu && \
PATH=/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
cmake -B build-cpu -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_HIP_COMPILER="" \
-DCMAKE_INSTALL_PREFIX=/build/dist && \
cmake --build build-cpu --target ggml-cpu -- -l $(nproc) && \
cmake --install build-cpu --component CPU --strip && \
echo "=== CPU install ===" && \
(find /build/dist/lib/ollama -type f -o -type l 2>&1 | head -20 || echo "empty")
# Step 2: Build HIP backend with ROCm preset + gfx906 target only
# The ROCm 6 preset enables HIP language detection (enable_language(HIP))
# which ensures GPU kernels are properly compiled for gfx906.
# OLLAMA_RUNNER_DIR=rocm from the preset, so HIP goes to lib/ollama/rocm/
# Need CMAKE_PREFIX_PATH so find_package(hip) finds hip-config.cmake
# at /opt/rocm/lib/cmake/hip/hip-config.cmake.
RUN mkdir -p build-hip && \
cmake -B build-hip \
--preset 'ROCm 6' \
-DAMDGPU_TARGETS="gfx906:xnack-" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH="/opt/rocm" && \
cmake --build build-hip --target ggml-hip -- -l $(nproc) && \
cmake --install build-hip --component HIP --strip && \
echo "=== HIP install ===" && \
find /build/dist/lib/ollama -type f -o -type l | head -20
# Step 3: Build Go binary (GCC for CGo linking)
ENV CGO_ENABLED=1
RUN go build -trimpath -ldflags="-X=github.com/ollama/ollama/version.Version=${OLLAMA_VERSION}" -o /build/dist/ollama .
# ---------- Runtime image ----------
FROM ubuntu:24.04
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
ca-certificates curl libstdc++6 libgomp1 libvulkan1 libopenblas0 \
&& rm -rf /var/lib/apt/lists/*
# Copy ROCm 6.1 runtime libraries
# These are needed at runtime by ggml-hip via LD_LIBRARY_PATH
COPY --from=builder /opt/rocm/lib/ /opt/rocm/lib/
COPY --from=builder /opt/rocm/share/ /opt/rocm/share/
# Copy ollama binary + all backends (CPU + HIP)
# CPU install: /build/dist/lib/ollama/libggml-*.so
# HIP install: /build/dist/lib/ollama/rocm/libggml-hip.so
COPY --from=builder /build/dist/ollama /usr/bin/ollama
COPY --from=builder /build/dist/lib/ollama/ /usr/lib/ollama/
RUN ldconfig
ENV LD_LIBRARY_PATH=/opt/rocm/lib:/usr/lib/ollama/rocm:/usr/lib/ollama
ENV HSA_OVERRIDE_GFX_VERSION=9.0.6
ENV HCC_AMDGPU_TARGET=gfx906
ENV HSA_ENABLE_SDMA=0
EXPOSE 11434
ENTRYPOINT ["/bin/ollama"]
CMD ["serve"]

301
flake.lock generated
View File

@@ -23,84 +23,7 @@
"type": "github"
}
},
"agenix-rekey": {
"inputs": {
"devshell": "devshell",
"flake-parts": "flake-parts",
"nixpkgs": [
"nixpkgs"
],
"pre-commit-hooks": "pre-commit-hooks",
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1774522439,
"narHash": "sha256-GvINrdGznE7mGlDNjW0/PMgOJlC+Nl9MkfxALB4QvWs=",
"owner": "oddlama",
"repo": "agenix-rekey",
"rev": "8b9c179bc1300ab130c90f2d25426bf0e7a2b58d",
"type": "github"
},
"original": {
"owner": "oddlama",
"repo": "agenix-rekey",
"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"
}
},
"devshell": {
"inputs": {
"nixpkgs": [
"agenix-rekey",
"nixpkgs"
]
},
"locked": {
"lastModified": 1728330715,
"narHash": "sha256-xRJ2nPOXb//u1jaBnDP56M7v5ldavjbtR6lfGqSvcKg=",
"owner": "numtide",
"repo": "devshell",
"rev": "dd6b80932022cea34a019e2bb32f6fa9e494dfef",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "devshell",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-compat_2": {
"flake": false,
"locked": {
"lastModified": 1751685974,
@@ -114,64 +37,6 @@
"url": "https://git.lix.systems/lix-project/flake-compat/archive/main.tar.gz"
}
},
"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"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
"agenix-rekey",
"nixpkgs"
]
},
"locked": {
"lastModified": 1733312601,
"narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"agenix-rekey",
"pre-commit-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
@@ -193,37 +58,16 @@
"type": "github"
}
},
"home-manager_2": {
"inputs": {
"nixpkgs": [
"nixpkgs-uconsole"
]
},
"locked": {
"lastModified": 1779506708,
"narHash": "sha256-QOD/CNm196nCJRheux/URi4/HE66fthdOMqCJoPP1Y0=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "3ee51fbdac8c8bdfe1e7e1fcaba6520a563f394f",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "release-25.11",
"repo": "home-manager",
"type": "github"
}
},
"lix": {
"inputs": {
"flake-compat": "flake-compat_2",
"flake-compat": "flake-compat",
"nix2container": "nix2container",
"nix_2_18": "nix_2_18",
"nixpkgs": [
"nixpkgs"
],
"nixpkgs-regression": "nixpkgs-regression",
"pre-commit-hooks": "pre-commit-hooks_2"
"pre-commit-hooks": "pre-commit-hooks"
},
"locked": {
"lastModified": 1774721317,
@@ -300,80 +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-raspberrypi": {
"inputs": {
"argononed": "argononed",
"flake-compat": "flake-compat_3",
"nixos-images": "nixos-images",
"nixpkgs": [
"nixpkgs-uconsole"
]
},
"locked": {
"lastModified": 1781324200,
"narHash": "sha256-JWqxN2Yle86+4Q+GFh12SvB92ZyLeqalVsN9lfMh6eQ=",
"owner": "gortium",
"repo": "nixos-raspberrypi",
"rev": "721a6e9e67dca3a23133db650b87018646bca3e6",
"type": "github"
},
"original": {
"owner": "gortium",
"ref": "cm5-cross-v1",
"repo": "nixos-raspberrypi",
"type": "github"
}
},
"nixos-uconsole": {
"inputs": {
"nixos-raspberrypi": [
"nixos-raspberrypi"
],
"nixpkgs": [
"nixpkgs-uconsole"
]
},
"locked": {
"lastModified": 1781476310,
"narHash": "sha256-jY6ujqLXNAWJGvt+pAuw1Wg/OiHRGd1B1Z7Czhiq7Q4=",
"owner": "gortium",
"repo": "nixos-uconsole",
"rev": "38a7fcbffbf2d2e122bc1e1c634fe25f66ecda13",
"type": "github"
},
"original": {
"owner": "gortium",
"ref": "pr/dcs-panel-detection",
"repo": "nixos-uconsole",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1705033721,
@@ -406,22 +176,6 @@
"type": "github"
}
},
"nixpkgs-uconsole": {
"locked": {
"lastModified": 1780952837,
"narHash": "sha256-Fwd1+spDtQ0hDyBwme6ufG3n4mY0UrjjFdYHv+G/Hds=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e820eb4a444b46a19b2e03e8dfd2359439ff30fe",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1774386573,
@@ -439,29 +193,6 @@
}
},
"pre-commit-hooks": {
"inputs": {
"flake-compat": "flake-compat",
"gitignore": "gitignore",
"nixpkgs": [
"agenix-rekey",
"nixpkgs"
]
},
"locked": {
"lastModified": 1735882644,
"narHash": "sha256-3FZAG+pGt3OElQjesCAWeMkQ7C/nB1oTHLRQ8ceP110=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "a5a961387e75ae44cc20f0a57ae463da5e959656",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"type": "github"
}
},
"pre-commit-hooks_2": {
"flake": false,
"locked": {
"lastModified": 1769939035,
@@ -480,13 +211,8 @@
"root": {
"inputs": {
"agenix": "agenix",
"agenix-rekey": "agenix-rekey",
"home-manager": "home-manager_2",
"lix": "lix",
"nixos-raspberrypi": "nixos-raspberrypi",
"nixos-uconsole": "nixos-uconsole",
"nixpkgs": "nixpkgs_2",
"nixpkgs-uconsole": "nixpkgs-uconsole"
"nixpkgs": "nixpkgs_2"
}
},
"systems": {
@@ -503,27 +229,6 @@
"repo": "default",
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"agenix-rekey",
"nixpkgs"
]
},
"locked": {
"lastModified": 1735135567,
"narHash": "sha256-8T3K5amndEavxnludPyfj3Z1IkcFdRpR23q+T0BVeZE=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "9e09d30a644c57257715902efbb3adc56c79cf28",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
}
},
"root": "root",

340
flake.nix
View File

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

View File

@@ -21,7 +21,14 @@
settings.PermitRootLogin = "prohibit-password";
};
# User (defined in users/gortium/gortium.nix)
# User
users.users.gortium = {
isNormalUser = true;
extraGroups = [ "wheel" "networkmanager" "kismet" ];
openssh.authorizedKeys.keys = [
# Populate with your public key
];
};
# CYT Project Dependencies (Headless)
environment.systemPackages = with pkgs; [
@@ -43,7 +50,7 @@
User = "gortium";
Group = "kismet";
ExecStart = ''
${pkgs.kismet}/bin/kismet -c panda --log-prefix=/home/gortium/kismet_logs --no-nc-ui
${pkgs.kismet}/bin/kismet -c panda --log-base=/home/gortium/kismet_logs --no-nc-ui
'';
Restart = "always";
RestartSec = "10s";
@@ -70,12 +77,6 @@
};
};
# Age secrets
age.secrets.gortium_password = {
file = ../../secrets/gortium_password.age;
};
# CYT Application Service
systemd.services.cyt-app = {
description = "Chasing Your Tail - Target Detector";

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 = {
@@ -53,12 +49,24 @@
networking.networkmanager.enable = true; # Easiest to use and most distros use this by default.
networking.hostId = "deadbeef";
# WireGuard VPN client -- module, always up, connects to wg-easy server
gortium.wireguard-client = {
enable = true;
vpnIp = "10.8.0.3/24";
privateKeyFile = config.age.secrets.wireguard_private_key.path;
presharedKeyFile = config.age.secrets.wireguard_preshared_key.path;
# 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.
@@ -170,6 +178,7 @@
settings = {
PasswordAuthentication = false;
KbdInteractiveAuthentication = false;
# Additional hardening settings below in SERVER HARDENING section
};
hostKeys = [
{
@@ -251,9 +260,7 @@
# Private host ssh key managed by agenix
age = {
identityPaths = paths.identities ++ [
"/etc/ssh/ssh_host_ed25519_key"
];
identityPaths = paths.identities;
secrets = {
containers_env = {
file = ../../secrets/containers.env.age;
@@ -276,6 +283,13 @@
mode = "0600";
path = "/home/ai-worker/.ssh/ai_ssh_key";
};
openclaw_gateway_token = {
file = ../../secrets/openclaw_gateway_token.age;
owner = "root";
group = "ai-worker";
mode = "0440";
path = "/run/secrets/openclaw_gateway_token";
};
wireguard_private_key = {
file = ../../secrets/wireguard_private_key.age;
owner = "root";
@@ -289,24 +303,20 @@
group = "root";
mode = "0400";
path = "/run/secrets/wireguard_preshared_key";
};
builder_key = {
file = ../../secrets/builder_key.age;
owner = "root";
group = "root";
mode = "0600";
path = "/etc/ssh/builder_key";
};
gortium_password = {
file = ../../secrets/gortium_password.age;
owner = "gortium";
group = "gortium";
mode = "0400";
path = "/run/secrets/gortium_password";
};
};
};
# OpenClaw Node service (host-side execution for Docker gateway)
services.openclaw-node = {
enable = true;
user = "ai-worker";
gatewayHost = "127.0.0.1";
gatewayPort = 18789;
gatewayTokenFile = "/run/secrets/openclaw_gateway_token";
displayName = "lazyworkhorse-host";
};
# Public host ssh key (kept in sync with the private one)
environment.etc."ssh/ssh_host_ed25519_key.pub".text =
"${keys.hosts.lazyworkhorse.main}";
@@ -339,16 +349,6 @@
# networking.firewall.enable = false;
# =============================================================================
# ============================================================
# ClamAV antivirus — daemon, hourly updates, daily scan, on-access
# ============================================================
gortium.clamav = {
enable = true;
enableDaemon = true;
onAccessScanning = true;
dailyScanTime = "03:00";
};
# SERVER HARDENING - Firewall, Fail2ban, SSH, Kernel
# =============================================================================
@@ -569,29 +569,5 @@
# For more information, see `man configuration.nix` or https://nixos.org/manual/nixos/stable/options#opt-system.stateVersion .
system.stateVersion = "25.05"; # Did you read the comment?
# ============================================================
# Remote builder — dispatches aarch64-linux builds to uConsole
# ============================================================
services.remoteBuilder = {
enable = true;
machines = [
{
hostName = "192.168.1.120";
port = 22;
sshUser = "builder";
sshKey = config.age.secrets.builder_key.path;
systems = [ "aarch64-linux" ];
maxJobs = 4;
}
];
};
# agenix-rekey — automatic re-encryption for each target host
age.rekey = {
masterIdentities = [
"/home/gortium/.ssh/gortium_ssh_key"
];
};
}

View File

@@ -1,240 +0,0 @@
{ config, lib, pkgs, keys, ... }:
{
networking.hostName = "uConsole";
time.timeZone = "America/Montreal";
i18n.defaultLocale = "en_CA.UTF-8";
system.stateVersion = "25.11";
# Boot & Hardware
boot.loader.raspberry-pi.bootloader = "kernel";
# 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
];
# Age secret for gortium password (file created by user)
age.secrets.gortium_password = {
file = ../../secrets/gortium_password.age;
};
# WiFi via NetworkManager
networking.networkmanager.enable = true;
# Firmware
hardware.enableRedistributableFirmware = true;
# RealtimeKit for PipeWire audio
security.rtkit.enable = true;
services.pipewire = {
enable = true;
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;
};
# Hyprland Wayland compositor (manual start — no SDDM)
programs.hyprland = {
enable = true;
xwayland.enable = true;
};
# HackerGadgets AIO v2 board
hardware.uconsole-cm5-aio-v2 = {
enable = true;
bootRails = {
GPS = false;
LORA = false;
SDR = false;
USB = false;
};
enableGPS = false;
};
# User
users.users.gortium = {
isNormalUser = true;
extraGroups = [ "wheel" "networkmanager" "video" "dialout" "kismet" ];
hashedPasswordFile = config.age.secrets.gortium_password.path;
openssh.authorizedKeys.keys = [
keys.users.gortium.main
keys.users.gortium.gitea
];
};
security.sudo.extraRules = [
{
users = [ "gortium" ];
commands = [{
command = "ALL";
options = [ "NOPASSWD" ];
}];
}
];
# ============================================================
# Package groups
# ============================================================
# ============================================================
# CROSS-COMPILE REMOVALS — packages removed for aarch64 bootstrap
# ============================================================
# These packages fail to cross-compile for aarch64.
# Install them natively AFTER the first successful switch.
#
# Removed: Reason:
# inspectrum — Qt5 cross-compile cascade fails (qtsvg mismatched qtbase deps)
# hashcat — Makefile calls gcc directly (cross-compiler not used)
# neovim — Same as hashcat: Makefile calls gcc directly (cross-compiler not used)
# clamav — cmake try_run + Rust proc-macro linker for aarch64
# sdrpp — glfw/wxPython cross-compile fails
# gqrx — Qt5 cross-compile cascade fails
# emacs-pgtk → emacs-nox — GTK3 + mailutils → gss → shishi chain
# viking — GTK3 GPS map editor
# foxtrotgps — GTK2 GPS app
# js8call — QtQuick3D dep
# wsjtx — qtbase/Qt5 linker fails (collect2: ld returned 1)
# fldigi — same: qtbase/Qt5 linker fails
# gpsbabel — qmake can't find cross-compiler g++
# john — configure script needs python (not in PATH during cross-compile)
# trustedqsl — needs wxWidgets (unavailable in cross-compile)
# chirp — depends on wxPython (fails cross-compile: GTK3 + wx build)
# ============================================================
environment.systemPackages = with pkgs; [
# ===== Base =====
# emacs-pgtk — removed for bootstrap (GTK3 cross-compile fails)
# emacs-nox — removed for bootstrap (depends on mailutils -> gss -> shishi, cross-compile fails)
git
ripgrep
fd
htop
tmux
# ===== HAM Radio =====
# wsjtx # removed for bootstrap - now native (native-only, needs remote build)
# fldigi # removed for bootstrap - now native (native-only, needs remote build)
pat # Winlink client
direwolf # AX.25 packet modem
# chirp # Radio programming tool - now native (native-only, needs remote build)
hamlib # Ham radio control libraries
# trustedqsl # Logbook of the World (LoTW) - now native (native-only, needs remote build)
# ===== SDR / RF =====
# sdrpp # removed for bootstrap - now native (native-only, needs remote build)
# gqrx # removed for bootstrap - now native (native-only, needs remote build)
rtl-sdr # RTL-SDR drivers & utilities
# inspectrum # removed for bootstrap - now native (native-only, needs remote build)
soapysdr-with-plugins # SoapySDR + hardware support plugins
# ===== Mesh / LoRa =====
reticulumStack # Reticulum Network Stack
lxmf # LXMF messaging protocol
nomadnet # Nomad Network client
# ===== Security =====
nmap
aircrack-ng
kismet # Wi-Fi monitor / IDS
bettercap # MITM/network attack framework
wireshark-cli # Packet analyzer
john # John the Ripper - now native
sqlmap # SQL injection tool
# ===== GPS / Maps =====
#foxtrotgps # removed for bootstrap - now native (native-only, needs remote build)
#viking # removed for bootstrap - now native (native-only, needs remote build)
#gpsbabel # GPS data conversion - now native (native-only, needs remote build)
];
# ============================================================
# 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-prefix=/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
"rtl8xxxu" # RTL8188/8192/8723 USB WiFi
"rtl2832_sdr" # RTL-SDR kernel module
"dvb_usb_rtl28xxu" # RTL-SDR DVB-T
];
# ============================================================
# 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
# ============================================================
networking.firewall.allowedTCPPorts = [ 22 ];
networking.firewall.allowedUDPPorts = [ ];
# ============================================================
# Remote builder — dispatches x86_64-linux builds to lazyworkhorse
# ============================================================
services.remoteBuilder = {
enable = true;
machines = [
{
hostName = "lazyworkhorse.net";
port = 2424;
sshUser = "builder";
sshKey = config.age.secrets.builder_key.path;
systems = [ "x86_64-linux" ];
maxJobs = 36;
supportedFeatures = [ "benchmark" "big-parallel" "nixos-test" ];
}
];
};
# Age secret for builder SSH key (remote building)
age.secrets.builder_key = {
file = ../../secrets/builder_key.age;
};
# ============================================================
age.identityPaths = [
"/etc/ssh/ssh_host_ed25519_key"
];
# agenix-rekey — automatic secret re-encryption at deploy time
# ============================================================
age.rekey = {
# Master identities for encrypting secrets (on Thierry's laptop)
masterIdentities = [
"/home/gortium/.ssh/gortium_ssh_key"
];
# uConsole SSH host pubkey — for automatic rekey at build time
# Once uConsole is deployed, replace with actual pubkey from:
# ssh-keyscan uConsole.local | ssh-to-age
hostPubkey = "age1un8td4jzkhg5fm4jja7z3aznskc8hlcm8ky0j2f70tv74rulwgaqtpemn2"; # uConsole CM5 SSH host key
};
# Pipewire overlay: drop libcamera (fixes aarch64 cross-compile — rpi-pisp blocks)
nixpkgs.overlays = [
(final: prev: {
pipewire = prev.pipewire.override { libcamera = null; };
})
];
}

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

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

View File

@@ -1,121 +0,0 @@
{ config, lib, pkgs, ... }:
let
cfg = config.gortium.poup16t;
luksName = cfg.luksName;
in
with lib;
{
options.gortium.poup16t = {
enable = mkEnableOption "Poup_16T storage disk (btrfs + LUKS + btrbk snapshots)";
luksUuid = mkOption {
type = types.str;
description = ''
UUID of the LUKS partition on the 16TB disk (WD Red Pro).
Find this by running as root when the disk is connected:
blkid /dev/sdb # or wherever the disk appears
lsblk -o NAME,SIZE,FSTYPE,UUID
Since btrfs is inside LUKS, the FS UUID is hidden use the
LUKS partition UUID from blkid (it'll show TYPE=\"crypto_LUKS\").
'';
example = "00000000-0000-0000-0000-000000000000";
};
luksName = mkOption {
type = types.str;
default = "poup_16t";
description = "Name for the LUKS /dev/mapper/ mapping";
};
mountPoint = mkOption {
type = types.str;
default = "/mnt/Poup_16T";
description = "Mount point for the 16TB data disk";
};
btrfsOptions = mkOption {
type = types.listOf types.str;
default = [ "defaults" "noatime" "compress=zstd:3" "nofail" ];
description = "Mount options for the btrfs filesystem. 'nofail' ensures boot succeeds when disk is disconnected.";
};
btrbk = {
enable = mkOption {
type = types.bool;
default = true;
description = "Enable btrbk snapshot management on this volume";
};
schedule = mkOption {
type = types.str;
default = "daily";
description = "systemd calendar event for btrbk (e.g. 'daily', 'hourly', '*-*-* 00:00:00')";
};
preserveMin = mkOption {
type = types.str;
default = "2d";
description = "btrbk snapshot_preserve_min minimum age before pruning";
};
preserve = mkOption {
type = types.str;
default = "14d 4w 3m";
description = "btrbk snapshot_preserve retention policy (daily, weekly, monthly)";
};
snapshotDir = mkOption {
type = types.str;
default = ".snapshots";
description = "Directory name for snapshots relative to volume root";
};
};
};
config = mkIf cfg.enable {
# Enable btrfs kernel support (no DKMS needed — it's in-tree)
boot.supportedFilesystems = [ "btrfs" ];
# Install btrfs administration tools
environment.systemPackages = with pkgs; [
btrfs-progs # mkfs.btrfs, btrfs, fsck, balance, scrub
btrbk # Snapshot management + rotation
];
# LUKS2 unlock at boot (uses keyfile or prompts if unavailable)
# Since the disk may be disconnected, initrd times out gracefully (~30s)
boot.initrd.luks.devices.${luksName} = {
device = "/dev/disk/by-uuid/${cfg.luksUuid}";
preLVM = false;
allowDiscards = true;
};
# Mount the unlocked mapper device as btrfs
fileSystems.${cfg.mountPoint} = {
device = "/dev/mapper/${luksName}";
fsType = "btrfs";
options = cfg.btrfsOptions;
};
# btrbk — automated snapshot creation and rotation
services.btrbk = mkIf cfg.btrbk.enable {
instances.poup16t = {
onCalendar = cfg.btrbk.schedule;
settings = {
snapshot_preserve_min = cfg.btrbk.preserveMin;
snapshot_preserve = cfg.btrbk.preserve;
volume.${cfg.mountPoint} = {
snapshot_create = "always";
snapshot_dir = cfg.btrbk.snapshotDir;
subvolume = ".";
};
};
};
};
};
}

View File

@@ -1,170 +0,0 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.hardware.uconsole-cm5-aio-v2;
# GPIO pin map matching the AIO v2 board hardware
# SDR (RTL-SDR): GPIO 7
# LoRa (SX1262) : GPIO 16
# USB Hub Interne: GPIO 23
# GPS (GNSS) : GPIO 27
gpioMap = {
GPS = 27;
LORA = 16;
SDR = 7;
USB = 23;
};
# Generate a script that applies boot rail states via pinctrl
applyRailsScript = pkgs.writeShellScript "apply-aio-v2-rails" (
''
set -e
PINCTRL=${pkgs.raspberrypi-utils}/bin/pinctrl
''
+ concatStringsSep "" (mapAttrsToList (name: pin: ''
if [ "${if cfg.bootRails.${name} then "1" else "0"}" = "1" ]; then
echo "AIO v2: ${name} (GPIO${toString pin}) -> ON"
$PINCTRL set ${toString pin} op dh
else
echo "AIO v2: ${name} (GPIO${toString pin}) -> OFF"
$PINCTRL set ${toString pin} op dl
fi
'') gpioMap)
);
# aiov2_ctl CLI tool -- fetched from GitHub, available as `aiov2_ctl`
aiov2CtlPkg = pkgs.stdenv.mkDerivation rec {
pname = "aiov2_ctl";
version = "0-unstable-2026-06-16";
src = pkgs.fetchFromGitHub {
owner = "hackergadgets";
repo = "aiov2_ctl";
rev = "main";
hash = "sha256-hqOvS1K5pDVXAroUE50i5R9YqRgC2U3fzby6uuB67K0=";
};
dontUnpack = true;
buildInputs = [ pkgs.python3 ];
installPhase = ''
mkdir -p $out/bin $out/share/aiov2_ctl/img
cp $src/aiov2_ctl.py $out/bin/aiov2_ctl
chmod +x $out/bin/aiov2_ctl
patchShebangs $out/bin/aiov2_ctl
substituteInPlace $out/bin/aiov2_ctl \
--replace-fail '"/usr/local/share/aiov2_ctl/img/' '"'$out'/share/aiov2_ctl/img/'
cp -r $src/img/* $out/share/aiov2_ctl/img/
'';
meta = {
description = "HackerGadgets uConsole AIO v2 GPIO control and telemetry tool";
homepage = "https://github.com/hackergadgets/aiov2_ctl";
license = lib.licenses.mit;
maintainers = with lib.maintainers; [ ];
platforms = [ "aarch64-linux" ];
};
};
in {
options.hardware.uconsole-cm5-aio-v2 = {
enable = mkEnableOption "HackerGadgets uConsole AIO v2 board support";
bootRails = {
GPS = mkOption {
type = types.bool;
default = false;
description = "Enable GPS module at boot (GPIO 27)";
};
LORA = mkOption {
type = types.bool;
default = false;
description = "Enable LoRa module at boot (GPIO 16)";
};
SDR = mkOption {
type = types.bool;
default = false;
description = "Enable SDR module at boot (GPIO 7)";
};
USB = mkOption {
type = types.bool;
default = false;
description = "Enable internal USB hub at boot (GPIO 23)";
};
};
package = mkOption {
type = types.package;
default = aiov2CtlPkg;
defaultText = literalExpression "aiov2CtlPkg";
description = "aiov2_ctl package to use";
};
enableGPS = mkOption {
type = types.bool;
default = false;
description = ''
Enable GPS UART (/dev/ttyAMA0 at 9600 baud).
Requires enabling UART on the CM5 via boot.kernelParams.
'';
};
enableGUI = mkOption {
type = types.bool;
default = false;
description = ''
Enable the system tray GUI for aiov2_ctl.
Requires a desktop environment with system tray support.
'';
};
};
config = mkIf cfg.enable {
# Package the aiov2_ctl tool + pinctrl
environment.systemPackages = with pkgs; [
cfg.package
raspberrypi-utils # provides pinctrl
];
# Boot rail systemd oneshot service
systemd.services.aiov2-rails-boot = {
description = "Apply AIO v2 GPIO rail boot states";
after = [ "local-fs.target" ];
wants = [ "local-fs.target" ];
before = [ "multi-user.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${applyRailsScript}";
RemainAfterExit = true;
};
};
# GPS configuration
boot.kernelParams = mkIf cfg.enableGPS [ "uart0=on" ];
users.users = mkIf cfg.enableGPS {
gortium = {
extraGroups = [ "dialout" ];
};
};
# GUI autostart (XDG)
systemd.user.services.aiov2-ctl-gui = mkIf cfg.enableGUI {
description = "AIO v2 System Tray Controller";
after = [ "graphical-session.target" ];
wants = [ "graphical-session.target" ];
wantedBy = [ "graphical-session.target" ];
serviceConfig = {
Type = "simple";
ExecStart = "${cfg.package}/bin/aiov2_ctl --gui";
Restart = "on-failure";
RestartSec = 5;
};
environment = {
AIOV2_CTL_DEBUG = "0";
};
};
};
}

View File

@@ -1,10 +1,44 @@
# 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.
## Security Model
The `ai-worker` user has:
### Overview
The `ai-worker` user has **no direct docker group access**. All docker commands must go through `sudo`, and only specific subcommands are whitelisted:
- **Container lifecycle**: `docker ps`, `docker inspect`, `docker logs`, `docker images`, `docker info`, `docker version`, `docker stats`
- **Control**: `docker start`, `docker stop`, `docker restart`, `docker rm`, `docker rmi`, `docker wait`
- **Image management**: `docker pull`, `docker build`, `docker run`, `docker compose`
- **Disk cleanup**: `docker system`
- **Network/Volume**: `docker network ls`, `docker volume ls` (read-only)
### EXPLICITLY BLOCKED (not in sudo whitelist)
| Command | Risk | Result |
|---------|------|--------|
| `docker exec` | Execute arbitrary commands inside containers (FILE MODIFICATION) | Blocked by sudo |
| `docker cp` | Copy files between containers and host | Blocked by sudo |
| `docker commit` | Create images from running containers (data exfil) | Blocked by sudo |
| `docker diff` | Inspect filesystem changes | Blocked by sudo |
| `docker export` | Export container filesystem | Blocked by sudo |
| `docker import` | Import filesystem archives | Blocked by sudo |
| `docker load` | Load docker images | Blocked by sudo |
| `docker save` | Save docker images to tar | Blocked by sudo |
| `docker attach` | Interactive access to containers | Blocked by sudo |
| `docker push` | Push images to registries | Blocked by sudo |
| `docker tag` | Rename images | Blocked by sudo |
### Why This Approach?
Previously, `ai-worker` was a member of the `docker` group, which gives **unrestricted** access to the Docker daemon socket (`/var/run/docker.sock`). Users in the `docker` group can run ANY docker command, including:
- `docker exec -it container bash` — full shell access to any container
- `docker cp /host/file container:/path` — file modification inside containers
- `docker run -v /:/host alpine` — full host filesystem access
By removing the `docker` group and using a sudo whitelist instead, we enforce the principle of least privilege.
### Filesystem Access
- **Home directory**: `/home/ai-worker` (standard user home)
@@ -12,51 +46,33 @@ The `ai-worker` user has:
- **Cannot access**: Any files outside standard system paths
### Sudo Access
- **NONE**: ai-worker has no sudo privileges
- **Restricted**: ai-worker has `NOPASSWD` access only to whitelisted commands
- 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 + Restricted Docker
## 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
All docker commands must be prefixed with `sudo`:
```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
# Check container status (works)
sudo docker ps
# 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'
# Restart a container (works)
sudo docker restart ollama
# 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"
# Run benchmark (works - docker run is allowed)
sudo docker run --rm alpine echo "test"
# Check VRAM usage
docker exec --privileged ollama rocm-smi --showmeminfo vram
# ANY of these will FAIL (not in whitelist):
sudo docker exec ollama ollama list # FAILS - docker exec blocked
sudo docker cp file.txt container:/path/ # FAILS - docker cp blocked
sudo docker commit container new-image # FAILS - docker commit blocked
# 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
# For ollama operations, use the HTTP API instead of docker exec:
curl http://ollama:11434/api/tags
```
## SSH Access
@@ -74,25 +90,30 @@ Check ai-worker permissions:
```bash
# On the host, as root or gortium:
sudo -u ai-worker sudo -l
# Should show: no sudo access
# Should show the whitelisted commands only (no docker exec/cp/commit)
# Check docker group membership
# Verify NOT in docker group
groups ai-worker
# Should show: ai-worker docker
# Should show: ai-worker (NO docker group)
```
## Troubleshooting
If ai-worker cannot run docker commands:
If docker commands fail:
```bash
# Check docker group membership
# Check sudo permissions
sudo -u ai-worker sudo -l | grep docker
# Verify group membership
groups ai-worker
# Verify ollama container is running
docker ps | grep ollama
# Test allowed command
sudo -u ai-worker sudo docker ps
# Test docker access
sudo -u ai-worker docker exec ollama ollama list
# Test blocked command (should fail)
sudo -u ai-worker sudo docker exec ollama ollama list
# Expected: "Sorry, user ai-worker is not allowed to execute"
```
If SSH connection fails:

View File

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

View File

@@ -1,240 +0,0 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.gortium.clamav;
clamavPkg = pkgs.clamav;
clamdConfig = pkgs.writeText "clamd.conf" ''
LogFile /var/log/clamav/clamd.log
LogTime yes
LogVerbose yes
LogSyslog yes
LocalSocket /run/clamav/clamd.sock
TCPSocket 3310
TCPAddr 127.0.0.1
User clamav
AllowSupplementaryGroups yes
${cfg.clamdExtraConfig}
'';
freshclamConfig = pkgs.writeText "freshclam.conf" ''
DatabaseDirectory /var/lib/clamav
LogFile /var/log/clamav/freshclam.log
LogTime yes
LogVerbose yes
LogSyslog yes
User clamav
AllowSupplementaryGroups yes
${cfg.freshclamExtraConfig}
'';
# Daily scan — logging only, no auto-quarantine/delete
scanScript = pkgs.writeShellScript "clamav-daily-scan" ''
set -e
PATHS="${concatStringsSep " " cfg.scanPaths}"
if [ -z "$PATHS" ]; then
echo "No paths configured for daily scan"
exit 0
fi
echo "=== ClamAV daily scan started: $(date) ==="
${clamavPkg}/bin/clamdscan --fdpass --log=/var/log/clamav/daily-scan.log --no-summary $PATHS
echo "=== ClamAV daily scan finished: $(date) ==="
'';
in
{
##### Options #####
options.gortium.clamav = {
enable = mkEnableOption "ClamAV antivirus installs clamav CLI tools";
enableDaemon = mkOption {
type = types.bool;
default = true;
description = ''
Run clamd daemon + freshclam updater + daily scheduled scan.
Set to false on machines where you only want the CLI tools
(clamscan, clamdscan) for manual on-demand scanning.
'';
};
onAccessScanning = mkOption {
type = types.bool;
default = false;
description = ''
Enable on-access scanning via clamonacc (fanotify-based).
Resource-heavy; server use only. Requires enableDaemon = true.
'';
};
scanPaths = mkOption {
type = types.listOf types.str;
default = [
"/home"
"/nix/store"
"/var/lib"
"/etc"
"/tmp"
"/var/tmp"
];
description = "Paths for the daily scheduled scan.";
};
dailyScanTime = mkOption {
type = types.str;
default = "daily";
description = ''
When to run the daily scan. systemd calendar expression
or shortcuts like "daily", "weekly", "04:00".
'';
};
clamdExtraConfig = mkOption {
type = types.lines;
default = "";
description = "Extra lines appended to clamd.conf";
};
freshclamExtraConfig = mkOption {
type = types.lines;
default = "";
description = "Extra lines appended to freshclam.conf";
};
};
##### Implementation #####
config = mkIf cfg.enable {
# 1. Package — always installed when enable = true
environment.systemPackages = [ clamavPkg ];
# Everything below uses mkIf cfg.enableDaemon — conditionalized per attribute
# 2. Users/groups (only if daemon runs)
users.users.clamav = mkIf cfg.enableDaemon {
isSystemUser = true;
group = "clamav";
home = "/var/lib/clamav";
createHome = true;
description = "ClamAV daemon user";
};
users.groups.clamav = mkIf cfg.enableDaemon {};
# 3. Directories (only if daemon runs)
systemd.tmpfiles.rules = mkIf cfg.enableDaemon [
"d /var/lib/clamav 0750 clamav clamav -"
"d /var/log/clamav 0750 clamav clamav -"
"d /run/clamav 0755 clamav clamav -"
];
# 4. ClamAV daemon (clamd)
systemd.services.clamav-daemon = mkIf cfg.enableDaemon {
description = "ClamAV Anti-Virus Daemon";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
path = [ clamavPkg ];
preStart = ''
mkdir -p /var/lib/clamav /var/log/clamav /run/clamav
chown clamav:clamav /var/lib/clamav /var/log/clamav /run/clamav
'';
serviceConfig = {
Type = "simple";
ExecStart = "${clamavPkg}/bin/clamd --config-file=${clamdConfig}";
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
Restart = "on-failure";
RestartSec = "10s";
User = "clamav";
Group = "clamav";
RuntimeDirectory = "clamav";
RuntimeDirectoryMode = "0755";
StateDirectory = "clamav";
StateDirectoryMode = "0750";
LogsDirectory = "clamav";
LogsDirectoryMode = "0750";
PrivateTmp = true;
ProtectSystem = "strict";
ProtectHome = true;
ReadWritePaths = [
"/var/lib/clamav"
"/var/log/clamav"
"/run/clamav"
];
NoNewPrivileges = true;
};
};
# 5. freshclam (database updater) — hourly via timer
systemd.services.clamav-freshclam = mkIf cfg.enableDaemon {
description = "ClamAV Virus Database Updater";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
path = [ clamavPkg pkgs.curl ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${clamavPkg}/bin/freshclam --config-file=${freshclamConfig} --daemon-notify=${clamdConfig}";
User = "clamav";
Group = "clamav";
PrivateTmp = true;
ProtectSystem = "full";
NoNewPrivileges = true;
};
};
systemd.timers.clamav-freshclam = mkIf cfg.enableDaemon {
description = "ClamAV database update timer";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "hourly";
Persistent = true;
RandomizedDelaySec = "1800";
};
};
# 6. Daily scan — logging only, no auto-quarantine
systemd.services.clamav-daily-scan = mkIf cfg.enableDaemon {
description = "ClamAV Daily Scheduled Scan";
after = [ "clamav-daemon.service" ];
requires = [ "clamav-daemon.service" ];
path = [ clamavPkg ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${scanScript}";
User = "clamav";
Group = "clamav";
PrivateTmp = true;
ProtectSystem = "strict";
ReadWritePaths = [ "/var/log/clamav" ];
};
};
systemd.timers.clamav-daily-scan = mkIf cfg.enableDaemon {
description = "ClamAV daily scan timer";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = cfg.dailyScanTime;
Persistent = true;
};
};
# 7. On-access scanning (clamonacc) — needs enableDaemon
systemd.services.clamav-onaccess = mkIf (cfg.enableDaemon && cfg.onAccessScanning) {
description = "ClamAV On-Access Scanner (clamonacc)";
after = [ "clamav-daemon.service" ];
requires = [ "clamav-daemon.service" ];
wantedBy = [ "multi-user.target" ];
path = [ clamavPkg ];
serviceConfig = {
Type = "simple";
ExecStart = "${clamavPkg}/bin/clamonacc --config-file=${clamdConfig} --fdpass --log=/var/log/clamav/clamonacc.log";
Restart = "on-failure";
RestartSec = "10s";
User = "root"; # clamonacc needs root for fanotify
Group = "root";
PrivateTmp = true;
NoNewPrivileges = true;
};
};
};
}

View File

@@ -1,5 +0,0 @@
{
imports = [
./systemd
];
}

View File

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

View File

@@ -1,73 +0,0 @@
{ config, lib, pkgs, ... }:
let
cfg = config.services.remoteBuilder;
in {
options.services.remoteBuilder = {
enable = lib.mkEnableOption "remote Nix build machine";
machines = lib.mkOption {
type = lib.types.listOf (lib.types.submodule {
options = {
hostName = lib.mkOption {
type = lib.types.str;
description = "Hostname or IP of the remote build machine.";
};
port = lib.mkOption {
type = lib.types.port;
default = 22;
description = "SSH port.";
};
sshUser = lib.mkOption {
type = lib.types.str;
default = "builder";
description = "SSH user on the remote build machine.";
};
sshKey = lib.mkOption {
type = lib.types.str;
description = "Path to SSH private key for the builder.";
};
systems = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "aarch64-linux" ];
description = "System types the remote builder can build for.";
};
maxJobs = lib.mkOption {
type = lib.types.int;
default = 4;
description = "Max parallel jobs on the remote builder.";
};
supportedFeatures = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "benchmark" "big-parallel" "nixos-test" ];
description = "Features the remote builder supports.";
};
};
});
default = [];
description = "List of remote Nix build machines.";
};
};
config = lib.mkIf cfg.enable {
nix.distributedBuilds = true;
nix.buildMachines = map (m: {
hostName = m.hostName;
sshUser = m.sshUser;
sshKey = m.sshKey;
systems = m.systems;
maxJobs = m.maxJobs;
supportedFeatures = m.supportedFeatures;
}) cfg.machines;
# SSH config for port + key (nix.buildMachines has no port option)
programs.ssh.extraConfig = lib.concatStringsSep "\n" (map (m: ''
Host ${m.hostName}
HostName ${m.hostName}
Port ${toString m.port}
User ${m.sshUser}
IdentityFile ${m.sshKey}
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
'') cfg.machines);
};
}

View File

@@ -1,275 +0,0 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.stagingVm;
in
{
options.services.stagingVm = {
enable = mkOption {
type = types.bool;
default = false;
description = "Enable KVM/libvirt staging VM for compose PR testing";
};
vmName = mkOption {
type = types.str;
default = "compose-test-vm";
description = "Name of the staging VM";
};
memory = mkOption {
type = types.str;
default = "4096";
description = "RAM allocated to the staging VM (MB)";
};
vcpus = mkOption {
type = types.int;
default = 2;
description = "Number of vCPUs for the staging VM";
};
storagePath = mkOption {
type = types.str;
default = "/var/lib/libvirt/images";
description = "Path for libvirt storage pool";
};
dataPath = mkOption {
type = types.str;
default = "/var/lib/staging-vm";
description = "Path for compose test data (PR checkouts, test results)";
};
};
config = mkIf cfg.enable {
# Enable libvirt daemon
virtualisation.libvirtd = {
enable = true;
qemu = {
package = pkgs.qemu_kvm;
runAsRoot = true;
swtpm.enable = true;
ovmf = {
enable = true;
packages = [ pkgs.OVMFFull.fd ];
};
};
};
# Kernel modules + groups already handled in configuration.nix
# libvirt NAT network (192.168.122.0/24)
environment.etc."libvirt/qemu/networks/default.xml" = {
text = ''
<network>
<name>default</name>
<uuid>2b8f7a3c-9e5d-4a1f-bc3d-6e7a8f9b0c1d</uuid>
<forward mode='nat'>
<nat>
<port start='1024' end='65535'/>
</nat>
</forward>
<bridge name='virbr0' stp='on' delay='0'/>
<mac address='52:54:00:12:34:56'/>
<ip address='192.168.122.1' netmask='255.255.255.0'>
<dhcp>
<range start='192.168.122.2' end='192.168.122.254'/>
</dhcp>
</ip>
</network>
'';
# Autostart the network so it comes up on boot
mode = "0644";
};
# Ensure the default network is defined and autostarted
systemd.services.libvirtd = {
postStart = ''
${pkgs.libvirt}/bin/virsh net-define /etc/libvirt/qemu/networks/default.xml 2>/dev/null || true
${pkgs.libvirt}/bin/virsh net-autostart default 2>/dev/null || true
${pkgs.libvirt}/bin/virsh net-start default 2>/dev/null || true
'';
};
# Storage directory for VM images
systemd.tmpfiles.rules = [
"d ${cfg.storagePath} 0755 root root -"
"d ${cfg.dataPath} 0755 root root -"
];
# Ensure storage pool exists in libvirt
systemd.services.libvirtd.postStart = mkAfter ''
${pkgs.libvirt}/bin/virsh pool-define-as default dir --target "${cfg.storagePath}" 2>/dev/null || true
${pkgs.libvirt}/bin/virsh pool-autostart default 2>/dev/null || true
${pkgs.libvirt}/bin/virsh pool-start default 2>/dev/null || true
'';
# Firewall: allow traffic from virbr0 to host and outbound NAT
networking.firewall = {
extraCommands = ''
# Allow inbound DHCP/DNS from libvirt guests
iptables -I INPUT -i virbr0 -p udp --dport 67:68 -j ACCEPT
iptables -I INPUT -i virbr0 -p tcp --dport 53 -j ACCEPT
iptables -I INPUT -i virbr0 -p udp --dport 53 -j ACCEPT
# Allow established/related traffic back to guests
iptables -I FORWARD -i virbr0 -o virbr0 -j ACCEPT
iptables -I FORWARD -o virbr0 -j ACCEPT
iptables -I FORWARD -i virbr0 -j ACCEPT
'';
};
# Packages needed for VM management
environment.systemPackages = with pkgs; [
libvirt
qemu_kvm
virt-manager # optional GUI for manual management
OVMFFull
swtpm
];
# Enable docker in the host (already enabled, but ensure for compose testing)
virtualisation.docker.enable = true;
# Helper script: pr-test-vm
# Usage:
# pr-test-vm build — build the staging VM derivation
# pr-test-vm start — boot the VM with a compose PR branch
# pr-test-vm stop — graceful shutdown
# pr-test-vm destroy — force stop + delete VM
# pr-test-vm ssh — SSH into the running VM
systemd.tmpfiles.rules = mkAfter [
"d ${cfg.dataPath}/scripts 0755 root root -"
];
environment.systemPackages = [ (pkgs.writeShellScriptBin "pr-test-vm" ''
set -euo pipefail
DATA="${cfg.dataPath}"
VM_NAME="${cfg.vmName}"
VM_IMAGE="''${DATA}/''${VM_NAME}.qcow2"
VM_PORT=2223
build_vm() {
echo "==> Building NixOS staging VM for compose testing..."
# Build the VM config inline a minimal NixOS with Docker + SSH
cat > /tmp/staging-vm-config.nix << 'NIXEOF'
{ config, pkgs, lib, ... }: {
boot.loader.grub.devices = [ "/dev/vda" ];
boot.loader.timeout = 0;
# Minimal kernel
boot.kernelParams = [ "console=ttyS0" ];
boot.initrd.kernelModules = [ "virtio_blk" "virtio_net" "virtio_pci" ];
# SSH access
services.openssh = {
enable = true;
settings.PasswordAuthentication = false;
settings.PermitRootLogin = "prohibit-password";
};
# Docker for compose testing
virtualisation.docker.enable = true;
# Network (DHCP via virbr0)
networking.useDHCP = true;
networking.firewall.enable = false;
# Users
users.users.root.openssh.authorizedKeys.keys = [
"$(cat /root/.ssh/authorized_keys 2>/dev/null || echo 'ssh-ed25519 AAAAC3... placeholder')"
];
users.users.testrunner = {
isNormalUser = true;
extraGroups = [ "docker" ];
openssh.authorizedKeys.keys = [
"$(cat /root/.ssh/authorized_keys 2>/dev/null || echo 'ssh-ed25519 AAAAC3... placeholder')"
];
};
# Git + compose tools
environment.systemPackages = with pkgs; [ git docker-compose curl ];
system.stateVersion = "24.11";
}
NIXEOF
nixos-rebuild build-vm -I nixpkgs=channel:nixos-unstable \
--arg configuration 'import /tmp/staging-vm-config.nix' \
--out-link "''${DATA}/vm-result"
echo "==> VM built. Run 'pr-test-vm start' to boot."
}
start_vm() {
if [ -f "''${VM_IMAGE}" ]; then
echo "==> Booting existing VM..."
else
echo "==> Creating VM image..."
${pkgs.qemu_kvm}/bin/qemu-img create -f qcow2 "''${VM_IMAGE}" 20G
fi
# Check if already running
if ${pkgs.libvirt}/bin/virsh list --name 2>/dev/null | grep -q "''${VM_NAME}"; then
echo "==> VM already running."
exit 0
fi
${pkgs.qemu_kvm}/bin/qemu-system-x86_64 \
-name "''${VM_NAME}" \
-machine q35,accel=kvm \
-cpu host \
-smp ${toString cfg.vcpus} \
-m ${cfg.memory} \
-drive file="''${VM_IMAGE}",if=virtio,format=qcow2 \
-netdev user,id=net0,hostfwd=tcp::''${VM_PORT}-:22 \
-device virtio-net-pci,netdev=net0 \
-nographic \
-serial mon:stdio \
-pidfile "''${DATA}/''${VM_NAME}.pid" \
-daemonize
echo "==> VM booting... SSH on port ''${VM_PORT}"
echo "==> Wait for it: ssh -p ''${VM_PORT} testrunner@localhost"
}
stop_vm() {
PIDFILE="''${DATA}/''${VM_NAME}.pid"
if [ -f "''${PIDFILE}" ]; then
PID=$(cat "''${PIDFILE}")
kill "''${PID}" 2>/dev/null || true
rm -f "''${PIDFILE}"
echo "==> VM stopped."
else
${pkgs.libvirt}/bin/virsh destroy "''${VM_NAME}" 2>/dev/null || true
echo "==> VM destroyed."
fi
}
ssh_vm() {
exec ssh -p "''${VM_PORT}" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "testrunner@localhost" "$@"
}
# Main dispatch
case "''${1:-help}" in
build) build_vm ;;
start) start_vm ;;
stop) stop_vm ;;
destroy) stop_vm; rm -f "''${VM_IMAGE}"; echo "==> VM deleted." ;;
ssh) shift; ssh_vm "$@" ;;
*)
echo "Usage: pr-test-vm {build|start|stop|destroy|ssh}"
echo ""
echo " build build the NixOS VM derivation"
echo " start boot the VM (create image if needed)"
echo " stop graceful VM shutdown"
echo " destroy stop + delete VM image"
echo " ssh SSH into the running VM"
;;
esac
'') ];
};
}

View File

@@ -1,54 +0,0 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.gortium.wireguard-client;
in
{
##### Options #####
options.gortium.wireguard-client = {
enable = mkEnableOption "WireGuard VPN client to lazyworkhorse VPN server";
vpnIp = mkOption {
type = types.str;
description = "Assigned VPN IP with CIDR, e.g. \"10.8.0.4/24\"";
example = "10.8.0.4/24";
};
privateKeyFile = mkOption {
type = types.path;
description = "Path to the WireGuard private key (age-encrypted, via agenix)";
};
presharedKeyFile = mkOption {
type = types.nullOr types.path;
default = null;
description = "Path to the WireGuard preshared key (optional, age-encrypted)";
};
};
##### Config #####
config = mkIf cfg.enable {
networking.wireguard.interfaces = {
wg0 = {
ips = [ cfg.vpnIp ];
privateKeyFile = cfg.privateKeyFile;
peers = [
{
# Server public key (lazyworkhorse wg-easy)
publicKey = "rY9zII3AOm8rog2rv02PyA3Bq7zdvTOGkZapfCV1DkE=";
presharedKeyFile = cfg.presharedKeyFile;
# Split-tunnel: only route the VPN subnet
allowedIPs = [ "10.8.0.0/24" ];
endpoint = "vpn.lazyworkhorse.net:51820";
persistentKeepalive = 25;
}
];
};
};
environment.systemPackages = with pkgs; [ wireguard-tools ];
};
}

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 @@
--- a/drivers/gpu/drm/panel/panel-cwu50.c
+++ b/drivers/gpu/drm/panel/panel-cwu50.c
@@ -58,5 +58,8 @@
dcs_write_seq(0x72,0x06);
dcs_write_seq(0x75,0x03);
+ /* DSI_INIT0: set 4 lanes (bits[1:0]=11) */
+ dcs_write_seq(0x80,0x03);
dcs_write_seq(0xE0,0x01);
+
dcs_write_seq(0x00,0x00);
dcs_write_seq(0x01,0x47);//VCOM0x47
@@ -721,6 +723,6 @@
dsi->lanes = 4;
dsi->format = MIPI_DSI_FMT_RGB888;
- dsi->mode_flags = MIPI_DSI_MODE_VIDEO | MIPI_DSI_MODE_VIDEO_BURST | MIPI_DSI_MODE_VIDEO_SYNC_PULSE;
+ dsi->mode_flags = MIPI_DSI_MODE_VIDEO | MIPI_DSI_MODE_VIDEO_SYNC_PULSE;
ctx->id_gpio = devm_gpiod_get_optional(dev, "reset", GPIOD_IN);
if (IS_ERR(ctx->id_gpio)) {

View File

@@ -1 +0,0 @@
/nix/store/7y7rfksqcf5smz59jjixyl56bxq50j9g-nixos-system-uConsole-25.11.20260608.e820eb4

Binary file not shown.

Binary file not shown.

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

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

View File

@@ -8,11 +8,7 @@ let
in
{
"containers.env.age".publicKeys = authorizedKeys;
"gortium_password.age".publicKeys = authorizedKeys;
"home_wifi.age".publicKeys = authorizedKeys;
"lazyworkhorse_host_ssh_key.age".publicKeys = authorizedKeys;
"n8n_ssh_key.age".publicKeys = authorizedKeys;
"builder_key.age".publicKeys = authorizedKeys ++ [
keys.hosts.uConsole.bootstrap
];
"openclaw_gateway_token.age".publicKeys = authorizedKeys;
}

205
users/ai-worker.nix Normal file
View File

@@ -0,0 +1,205 @@
{ pkgs, inputs, config, keys, ... }: {
users.users.ai-worker = {
isSystemUser = true;
group = "ai-worker";
home = "/home/ai-worker";
createHome = true;
# SECURITY: ai-worker is NOT in the docker group.
# Docker access is restricted via sudo whitelist — only specific subcommands allowed.
# extraGroups = [ "docker" ]; — REMOVED: docker group gives unrestricted docker daemon access
shell = pkgs.bashInteractive;
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 via sudo (whitelist below — no exec/cp/commit)
# - Run specific security audit commands
# - NO access to infra repo (no bind mount)
# - NO nix/nixos-rebuild/nh commands
# WORKFLOW: SSH from Hermes container, run docker commands via sudo, return and save results
services.aiWorkerAccess = true;
# Restricted sudo for ai-worker
# IMPORTANT: ai-worker is NOT in docker group. All docker access goes through sudo.
# Only the subcommands listed below are allowed — everything else is denied.
# This prevents: docker exec, docker cp, docker commit, and other file-modifying operations.
security.sudo.extraRules = [
{
users = [ "ai-worker" ];
commands = [
# === Docker commands: lifecycle management (NO file modification) ===
# ps/inspect/logs — read-only status checks
{
command = "/run/current-system/sw/bin/docker ps";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/docker inspect *";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/docker logs *";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/docker images";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/docker info";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/docker version";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/docker stats *";
options = [ "NOPASSWD" ];
}
# start/stop/restart — container lifecycle
{
command = "/run/current-system/sw/bin/docker start *";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/docker stop *";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/docker restart *";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/docker rm *";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/docker rmi *";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/docker wait *";
options = [ "NOPASSWD" ];
}
# pull/build/run — image management and container creation
{
command = "/run/current-system/sw/bin/docker pull *";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/docker build *";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/docker run *";
options = [ "NOPASSWD" ];
}
# compose — orchestration
{
command = "/run/current-system/sw/bin/docker compose *";
options = [ "NOPASSWD" ];
}
# system — disk cleanup
{
command = "/run/current-system/sw/bin/docker system *";
options = [ "NOPASSWD" ];
}
# network — list only (create/modify not needed)
{
command = "/run/current-system/sw/bin/docker network ls";
options = [ "NOPASSWD" ];
}
# volume — list only (create/modify not needed)
{
command = "/run/current-system/sw/bin/docker volume ls";
options = [ "NOPASSWD" ];
}
# === EXPLICITLY DENIED docker commands (not in whitelist — sudo rejects them) ===
# docker exec — executes arbitrary commands inside running containers (FILE MODIFICATION)
# docker cp — copies files between containers and host (FILE ACCESS)
# docker commit — creates images from running containers (DATA EXFIL)
# docker diff — inspects filesystem changes (INFO LEAK)
# docker export — exports container filesystem (DATA EXFIL)
# docker import — imports filesystem archives
# docker load — loads docker images
# docker save — saves docker images to tar (DATA EXFIL)
# docker attach — attaches to running containers (INTERACTIVE ACCESS)
# docker push — pushes images to registries (DATA EXFIL)
# docker tag — renames images
# docker create — creates containers (use 'docker run' instead)
# docker plugin — manages plugins
# docker network create/rm — network management
# docker volume create/rm — volume management
# === Firewall checks ===
{
command = "/run/wrappers/bin/sudo iptables -L -n -v";
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" ];
}
# === Network diagnostics ===
{
command = "/run/current-system/sw/bin/ss -tlnp";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/cat /proc/net/tcp";
options = [ "NOPASSWD" ];
}
];
}
];
}

View File

@@ -1,92 +0,0 @@
{ pkgs, inputs, config, keys, ... }: {
users.users.ai-worker = {
isSystemUser = true;
group = "ai-worker";
home = "/home/ai-worker";
createHome = true;
extraGroups = [ "docker" ];
shell = pkgs.bashInteractive;
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/
# 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" ];
}
];
}
];
}

View File

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

View File

@@ -1,8 +1,4 @@
{ pkgs, inputs, config, keys, ... }: {
home-manager.extraSpecialArgs = {
inherit (config.networking) hostName;
};
home-manager.users.gortium = import ./home.nix;
users.users.gortium = {
isNormalUser = true;
extraGroups = [ "wheel" "docker" "video" "render"];
@@ -10,10 +6,9 @@
packages = with pkgs; [
tree
btop
nh
];
shell = pkgs.zsh;
passwordFile = config.age.secrets.gortium_password.path;
ignoreShellProgramCheck = true;
openssh.authorizedKeys.keys = [
keys.users.gortium.main
];

View File

@@ -1,91 +0,0 @@
{ pkgs, lib, config, inputs, hostName, ... }:
let
isUconsole = hostName == "uConsole";
dotfiles = ../../assets/dotfiles;
in {
home.username = "gortium";
home.homeDirectory = "/home/gortium";
home.stateVersion = "23.11";
programs.home-manager.enable = true;
home.file = {
# tmux
".tmux.conf".source = "${dotfiles}/tmux/.tmux.conf";
# kitty
".config/kitty/kitty.conf".source = "${dotfiles}/kitty/.config/kitty/kitty.conf";
# nvim
".config/nvim/init.lua".source = "${dotfiles}/nvim/.config/nvim/init.lua";
# starship
".config/starship.toml".source = "${dotfiles}/starship/.config/starship.toml";
# btop
".config/btop/btop.conf".source = "${dotfiles}/btop/.config/btop/btop.conf";
# waybar
".config/waybar/style.css".source = "${dotfiles}/waybar/.config/waybar/style.css";
".config/waybar/config.jsonc".source = "${dotfiles}/waybar/.config/waybar/config.jsonc";
# wofi
".config/wofi/style.css".source = "${dotfiles}/wofi/.config/wofi/style.css";
".config/wofi/config".source = "${dotfiles}/wofi/.config/wofi/config";
# yazi
# wallpapers
".config/wallpapers".source = "${dotfiles}/wallpapers/.config/wallpapers";
".config/yazi/yazi.toml".source = "${dotfiles}/yazi/.config/yazi/yazi.toml";
# hyprland — common config
".config/hypr/hyprland.conf".source = "${dotfiles}/hypr/.config/hypr/hyprland.conf";
".config/hypr/hypridle.conf".source = "${dotfiles}/hypr/.config/hypr/hypridle.conf";
".config/hypr/hyprlock.conf".source = "${dotfiles}/hypr/.config/hypr/hyprlock.conf";
".config/hypr/hyprpaper.conf".source = "${dotfiles}/hypr/.config/hypr/hyprpaper.conf";
".config/hypr/mocha.conf".source = "${dotfiles}/hypr/.config/hypr/mocha.conf";
# hyprland — host-specific monitor config
".config/hypr/hosts/monitors.conf".source =
if isUconsole
then "${dotfiles}/hypr/.config/hypr/hosts/uconsole.conf"
else "${dotfiles}/hypr/.config/hypr/hosts/laptop.conf";
};
programs.bash.enable = true;
programs.zsh = {
enable = true;
initExtra = builtins.readFile "${dotfiles}/zsh/.zshrc";
};
home.packages = with pkgs; [
git zsh tmux starship
neovim kitty
btop yazi ripgrep fd fzf
htop unzip wget jq
hyprland hyprlock hypridle hyprpaper
waybar wofi dunst
libnotify mako
swaynotificationcenter
swww
emacs
udiskie
hyprshade
networkmanagerapplet
pavucontrol
];
xdg.userDirs = {
enable = true;
createDirectories = true;
desktop = "$HOME/desktop";
documents = "$HOME/documents";
download = "$HOME/downloads";
music = "$HOME/music";
pictures = "$HOME/pictures";
publicShare = "$HOME/public";
templates = "$HOME/templates";
videos = "$HOME/videos";
};
}