Compare commits

..

67 Commits

Author SHA1 Message Date
15f70019d5 feat: integrate rollback sentinel as NixOS module
Add rollback-sentinel NixOS module that:
- Deploys sentinel-check.sh (inline) and nixos-rollback.sh (from file) as
  system packages
- Runs a boot-time systemd oneshot service after multi-user.target with
  configurable delay — checks Tier-1 services, triggers rollback on failure
- Runs a post-rebuild service via activation script after every
  nixos-rebuild switch
- Exposes options for tier1Services, tier2Services, tier3InfoServices,
  bootDelay, rollbackMode (set-default/rollback-now/dry-run), and
  enablePostRebuild

Module wired into flake.nix for lazyworkhorse and enabled in
configuration.nix with standard Tier-1/2 service lists and 120s delay.
2026-05-25 00:08:39 -04:00
e216c8edac fix: update compose submodule - honcho embedding config fix + deriver in CMD 2026-05-23 18:54:54 -04:00
2b4b2e2216 fix: update compose submodule - honcho config fixes 2026-05-23 16:51:24 -04:00
6be4076372 fix: update compose submodule to fix honcho config corrupted values 2026-05-23 16:43:22 -04:00
690873d0e4 fix: update compose submodule for honcho auth + LLM config 2026-05-23 08:34:46 -04:00
cd817c7fd2 chore: update compose submodule (fix nginx permissions - run as root) 2026-05-23 01:21:31 -04:00
33d1d860fb chore: update compose submodule (fix nginx permissions) 2026-05-23 00:31:46 -04:00
8d7afecb6e chore: update compose submodule (merge Honcho+OpenConcho into single container) 2026-05-23 00:00:27 -04:00
6f18071198 chore: update compose submodule (expose Honcho via Traefik + fix VECTOR_STORE_DIMENSIONS) 2026-05-22 23:54:35 -04:00
b43c6794b3 chore: update compose submodule (fix VECTOR_STORE_DIMENSIONS empty value) 2026-05-22 23:52:24 -04:00
8b9a144254 fix: update compose submodule for Matrix bridge deps + persistent venv
Updates assets/compose submodule to 8adbbf0 (compose fix/matrix-bridge-deps).

Compose commit adds:
- uv pip install openai mautrix[encryption] to hermes entrypoint
- Persistent venv volume at /opt/hermes/.venv
- Empty-volume first-boot handling (venv recreation)

This ensures Matrix bridge dependencies survive container recreation.
2026-05-20 14:36:42 -04:00
db2bd1d157 feat: add uConsole CM5 host configuration with Reticulum mesh stack
- New NixOS host 'uConsole' for ClockworkPi CM5 portable terminal
- flake.nix: add nixos-uconsole and nixos-raspberrypi inputs
- Imports: nixos-uconsole.nixosModules.uconsole-cm5,
  nixos-raspberrypi.nixosModules.raspberry-pi-5.base
- Full package list: base tools, HAM radio, SDR/RF, mesh/LoRa,
  security tools, GPS/maps
- Reticulum stack (rns 1.2.9, lxmf 0.9.8, nomadnet 1.1.1) built
  from PyPI via overlays/reticulum.nix
- systemd services: rnsd (Reticulum daemon), kismet (Wi-Fi IDS)
- Kernel modules for SDR (rtl-sdr, dvb) and USB WiFi
- Follows existing host config conventions (cyt-pi as template)
2026-05-20 14:34:15 -04:00
36359de6aa Merge pull request 'feat: add Syncthing firewall port and update compose submodule' (#47) from feat/syncthing-org-sync into master
Reviewed-on: #47
2026-05-19 00:34:42 +00:00
Robert
10b8565fd6 Merge branch 'master' into feat/syncthing-org-sync 2026-05-18 20:33:29 -04:00
Robert
f672696b8e Update submodule for syncthing 2026-05-18 20:31:07 -04:00
0980dca455 fix: update compose submodule to Traefik-routed Syncthing 2026-05-14 21:40:12 -04:00
96bc20ab70 feat: add Syncthing firewall port and update compose submodule 2026-05-14 21:36:26 -04:00
670ae4f002 Merge pull request 'fix: update compose submodule — use ln -sf for iptables-nft' (#46) from fix/vpn-iptables-nft-v3 into master
Reviewed-on: #46
2026-05-13 17:00:16 +00:00
f785abfd49 fix: update compose submodule — use ln -sf for iptables-nft 2026-05-13 12:59:04 -04:00
6f44aa7f76 Merge pull request 'fix: update compose submodule — remove apk add iptables-nft' (#45) from fix/vpn-iptables-nft-v2 into master
Reviewed-on: #45
2026-05-13 16:49:39 +00:00
8d40f1691f fix: update compose submodule — remove apk add iptables-nft 2026-05-13 12:49:14 -04:00
Robert
2dd2e64986 Merge remote-tracking branch 'origin/master' 2026-05-13 12:42:54 -04:00
Robert
23fc5e0597 Give a little more ssh room for tramp 2026-05-13 12:41:09 -04:00
0c9c33d735 Merge pull request 'fix: update wg-easy to official ghcr image with iptables-nft' (#44) from fix/vpn-iptables-nft-upstream into master
Reviewed-on: #44
2026-05-13 16:39:56 +00:00
0bb6890f1c chore: merge master into branch 2026-05-13 12:39:05 -04:00
9d5434425f fix: update compose submodule for wg-easy iptables-nft fix
Updates the assets/compose submodule to point to the fix/vpn-iptables-nft-upstream
branch which contains:
- Switch FROM weejewel/wg-easy:latest (Alpine 3.11, stale 4yr) to
  ghcr.io/wg-easy/wg-easy:latest (actively maintained, Alpine krypton)
- Use update-alternatives instead of raw ln -sf to flip iptables
  from legacy to nftables backend
- Fix compose build context: ./vpn -> . (Dockerfile is at same level)
2026-05-13 12:30:47 -04:00
1fb4320dd1 Merge pull request 'feat: update compose submodule for custom tools startup' (#43) from feat/update-compose-submodule-custom-tools into master
Reviewed-on: #43
2026-05-13 13:58:27 +00:00
51e9f47fd4 feat: update compose submodule for custom tools startup 2026-05-13 09:56:24 -04:00
06b3eb840f fix: update compose submodule for wg-easy iptables-nft fix 2026-05-12 16:29:51 -04:00
28ab52209c Merge pull request 'Add restricted AI worker access with deployment capabilities' (#1) from ai-worker-restricted-access into master
Reviewed-on: #1
2026-05-11 00:48:29 +00:00
Robert
e6f7f0c263 Merge branch 'ai-worker-restricted-access' of ssh://code.lazyworkhorse.net:2222/gortium/infra into ai-worker-restricted-access 2026-05-10 18:06:46 -04:00
Robert
5c136e0765 Merge remote-tracking branch 'origin/master' into ai-worker-restricted-access 2026-05-10 17:06:58 -04:00
Robert
f722af7803 New ollama model creator module version 2026-05-10 16:56:09 -04:00
Robert
c07debf088 Added wireguard keys 2026-05-10 16:51:32 -04:00
9e42f5d2cc Merge pull request 'feat(hermes): update compose submodule for Piper TTS' (#34) from feat/voice-support-v2 into master
Reviewed-on: #34
2026-05-09 19:40:05 +00:00
614883f3c3 fix: update compose submodule - startup permission fix for data volume 2026-05-09 16:04:39 +00:00
374d022593 fix: update compose submodule - permission fix for atomic writes 2026-05-09 15:50:42 +00:00
9679846cdb feat: update compose submodule - Ryan high voice 2026-05-09 15:21:59 +00:00
4056f91ec6 fix: update compose submodule - remove patch step 2026-05-09 14:28:44 +00:00
1ba7d31d2f fix: update compose submodule - patch path fix 2026-05-09 14:27:16 +00:00
c7e9f8a1e0 feat: update compose submodule for Norman voice 2026-05-09 14:20:55 +00:00
bbe1a4a850 fix: update compose submodule - ca-certificates 2026-05-09 14:15:01 +00:00
2b8316060c fix: update compose submodule - COPY path fix 2026-05-09 14:12:15 +00:00
cc2c62faf7 fix: update compose submodule - Dockerfile heredoc fix 2026-05-09 14:09:59 +00:00
47f1ba6cf2 fix: update compose submodule - clean patch script 2026-05-09 13:59:17 +00:00
db89881d75 fix: update compose submodule for full OPENROUTER_API_KEY 2026-05-09 13:55:47 +00:00
0bb0a270e6 fix: update compose submodule for clean Piper Dockerfile 2026-05-09 13:42:02 +00:00
41256ccbde fix: update compose submodule for Piper TTS (replaces Coqui/ROCm) 2026-05-09 13:24:17 +00:00
e551f0e5c5 feat: update compose submodule for ROCm + Coqui TTS Dockerfile 2026-05-09 04:10:05 +00:00
b11d599f37 fix: update compose submodule for simplified Dockerfile 2026-05-09 02:38:41 +00:00
782f2fa9ed feat(hermes): update compose submodule for ROCm GPU voice STT support 2026-05-09 00:22:08 +00:00
2e14069584 Merge pull request 'feat: add WireGuard VPN stack' (#33) from feat/wireguard-vpn into master
Reviewed-on: #33
2026-05-09 00:13:36 +00:00
c53460c400 fix: remove dns option from wireguard config (not a valid nixos option) 2026-05-05 03:26:44 +00:00
Robert
ee96593e3d Merge branch 'feat/wireguard-vpn' of ssh://code.lazyworkhorse.net:2222/gortium/infra into feat/wireguard-vpn 2026-05-04 23:22:35 -04:00
Robert
030125ab01 Added wireguard pass 2026-05-04 23:21:36 -04:00
Robert
5935747902 Security fixes 2026-05-04 23:20:57 -04:00
Robert
9ae0f6ad62 Submodule update 2026-05-04 23:20:03 -04:00
5c481d664a fix: split tunnel on host VPN - only route 10.8.0.0/24 2026-05-05 02:41:29 +00:00
94a7c7195a fix: remove exposed keys from comments 2026-05-05 02:12:55 +00:00
cf279c4fb0 feat: add host-level WireGuard client via networking.wireguard
- Add wg0 interface config with agenix-managed secrets
- Revert compose submodule to remove NET_ADMIN from Hermes
- WireGuard runs at host level, all containers inherit the tunnel
2026-05-05 02:11:41 +00:00
b9289a149d chore: update compose submodule for Hermes NET_ADMIN + WireGuard Dockerfile 2026-05-05 01:48:24 +00:00
e0068260cb chore: move Hermes Dockerfile to compose repo, add WireGuard tools
- Move Dockerfile.full from infra/docker/hermes to compose/ai/Dockerfile
- Add wireguard-tools and openresolv to Hermes image
- Remove stray docker/hermes directory from infra
2026-05-05 01:43:42 +00:00
a42b2ff65d chore: update compose submodule to wireguard-vpn (fix ref) 2026-05-05 01:21:34 +00:00
92bcf1cc04 chore: update compose submodule to wireguard-vpn 2026-05-05 01:21:19 +00:00
7d0b72a513 chore: update compose submodule to linuxserver/wireguard 2026-05-05 01:18:13 +00:00
48245518a1 fix: load iptables kernel modules for WireGuard NAT
wg-easy needs iptable_nat and iptable_filter to set up
masquerading for VPN traffic. These modules must be loaded
at boot for the container to access iptables.
2026-05-05 01:17:14 +00:00
1673a56439 feat: add WireGuard VPN stack
- Add vpn stack to services.dockerStacks
- Open UDP port 51820 for WireGuard protocol
- Update compose submodule to include vpn stack
2026-05-04 22:49:06 +00:00
13 changed files with 1076 additions and 149 deletions

View File

@@ -1,71 +0,0 @@
FROM ghcr.io/astral-sh/uv:0.11.6-python3.13-trixie@sha256:b3c543b6c4f23a5f2df22866bd7857e5d304b67a564f4feab6ac22044dde719b AS uv_source
FROM tianon/gosu:1.19-trixie@sha256:3b176695959c71e123eb390d427efc665eeb561b1540e82679c15e992006b8b9 AS gosu_source
FROM debian:13.4
# Disable Python stdout buffering to ensure logs are printed immediately
ENV PYTHONUNBUFFERED=1
# Store Playwright browsers outside the volume mount so the build-time
# install survives the /opt/data volume overlay at runtime.
ENV PLAYWRIGHT_BROWSERS_PATH=/opt/hermes/.playwright
# Install system dependencies in one layer, clear APT cache
# tini reaps orphaned zombie processes (MCP stdio subprocesses, git, bun, etc.)
# that would otherwise accumulate when hermes runs as PID 1. See #15012.
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential nodejs npm python3 ripgrep ffmpeg gcc python3-dev libffi-dev procps git openssh-client docker-cli tini \
curl poppler-utils imagemagick \
chromium xvfb fonts-noto-color-emoji fonts-unifont fonts-liberation fonts-ipafont-gothic fonts-wqy-zenhei fonts-tlwg-loma-otf fonts-freefont-ttf \
libasound2t64 libatk-bridge2.0-0t64 libatk1.0-0t64 libatspi2.0-0t64 libcairo2 libcups2t64 libdbus-1-3 libdrm2 libgbm1 libglib2.0-0t64 libnspr4 libnss3 libpango-1.0-0 libx11-6 libxcb1 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxkbcommon0 libxrandr2 \
texlive-latex-base texlive-latex-extra texlive-fonts-recommended texlive-xetex texlive-science \
qemu-user-static binfmt-support qemu-user-binfmt \
emacs-nox \
libportaudio2 && \
rm -rf /var/lib/apt/lists/*
# Non-root user for runtime; UID can be overridden via HERMES_UID at runtime
RUN useradd -u 10000 -m -d /opt/data hermes
COPY --chmod=0755 --from=gosu_source /gosu /usr/local/bin/
COPY --chmod=0755 --from=uv_source /usr/local/bin/uv /usr/local/bin/uvx /usr/local/bin/
WORKDIR /opt/hermes
# ---------- Layer-cached dependency install ----------
# Copy only package manifests first so npm install + Playwright are cached
# unless the lockfiles themselves change.
COPY package.json package-lock.json ./
COPY web/package.json web/package-lock.json web/
RUN npm install --prefer-offline --no-audit && \
npx playwright install --with-deps chromium --only-shell && \
(cd web && npm install --prefer-offline --no-audit) && \
npm cache clean --force
# ---------- Source code ----------
# .dockerignore excludes node_modules, so the installs above survive.
COPY --chown=hermes:hermes . .
# Build web dashboard (Vite outputs to hermes_cli/web_dist/)
RUN cd web && npm run build
# ---------- Permissions ----------
# Make install dir world-readable so any HERMES_UID can read it at runtime.
# The venv needs to be traversable too.
USER root
RUN chmod -R a+rX /opt/hermes
# Start as root so the entrypoint can usermod/groupmod + gosu.
# If HERMES_UID is unset, the entrypoint drops to the default hermes user (10000).
# ---------- Python virtualenv ----------
RUN uv venv && \
uv pip install --no-cache-dir -e ".[all]" && \
uv pip install --no-cache-dir sounddevice numpy faster-whisper
# ---------- Runtime ----------
ENV HERMES_WEB_DIST=/opt/hermes/hermes_cli/web_dist
ENV HERMES_HOME=/opt/data
ENV PATH="/opt/data/.local/bin:${PATH}"
VOLUME [ "/opt/data" ]
ENTRYPOINT [ "/usr/bin/tini", "-g", "--", "/opt/hermes/docker/entrypoint.sh" ]

View File

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

View File

@@ -36,7 +36,7 @@
"transparent_hugepage=always" # because mucho ram
];
# 2. Load the specific drivers found by sensors-detect
boot.kernelModules = [ "nct6775" "lm96163" ];
boot.kernelModules = [ "nct6775" "lm96163" "iptable_nat" "iptable_filter" ];
# 3. Force the nct6775 driver to recognize the chip if it's stubborn
boot.extraModprobeConfig = ''
options nct6775 force_id=0xd280
@@ -49,6 +49,26 @@
networking.networkmanager.enable = true; # Easiest to use and most distros use this by default.
networking.hostId = "deadbeef";
# WireGuard VPN client -- always up, connects to wg-easy server
# Create age-encrypted secrets before deploying (run on the host):
# echo -n "<private_key>" | agenix -e secrets/wireguard_private_key.age
# echo -n "<preshared_key>" | agenix -e secrets/wireguard_preshared_key.age
networking.wireguard.interfaces = {
wg0 = {
ips = [ "10.8.0.3/24" ];
privateKeyFile = config.age.secrets.wireguard_private_key.path;
peers = [
{
publicKey = "rY9zII3AOm8rog2rv02PyA3Bq7zdvTOGkZapfCV1DkE=";
presharedKeyFile = config.age.secrets.wireguard_preshared_key.path;
allowedIPs = [ "10.8.0.0/24" ];
endpoint = "vpn.lazyworkhorse.net:51820";
persistentKeepalive = 25;
}
];
};
};
# Set your time zone.
time.timeZone = "America/Montreal";
@@ -187,6 +207,7 @@
ai = {
path = self + "/assets/compose/ai";
envFile = config.age.secrets.containers_env.path;
ports = [ 22000 ]; # Syncthing TCP sync
};
cloudstorage = {
@@ -221,6 +242,11 @@
path = self + "/assets/compose/homepage";
};
vpn = {
path = self + "/assets/compose/vpn";
envFile = config.age.secrets.containers_env.path;
};
# tak = {
# path = self + "/assets/compose/tak";
# };
@@ -264,6 +290,20 @@
mode = "0440";
path = "/run/secrets/openclaw_gateway_token";
};
wireguard_private_key = {
file = ../../secrets/wireguard_private_key.age;
owner = "root";
group = "root";
mode = "0400";
path = "/run/secrets/wireguard_private_key";
};
wireguard_preshared_key = {
file = ../../secrets/wireguard_preshared_key.age;
owner = "root";
group = "root";
mode = "0400";
path = "/run/secrets/wireguard_preshared_key";
};
};
};
@@ -281,10 +321,40 @@
environment.etc."ssh/ssh_host_ed25519_key.pub".text =
"${keys.hosts.lazyworkhorse.main}";
# ── Boot sentinel: auto-rollback on critical service failure ───────────────
services.rollbackSentinel.enable = true;
# Tier-1: failure triggers rollback
services.rollbackSentinel.tier1Services = [
"sshd" "docker" "traefik" "authelia"
];
# Tier-2: warn only
services.rollbackSentinel.tier2Services = [
"gitea" "hermes" "ollama" "synapse" "nextcloud"
"vaultwarden" "wireguard" "homeassistant" "fail2ban"
];
# Wait 2 minutes after boot before checking (lets services initialize)
services.rollbackSentinel.bootDelay = "120";
# Change boot default only (not --rollback-now) for safety
services.rollbackSentinel.rollbackMode = "set-default";
services.fstrim.enable = true;
services.zfs.autoSnapshot.enable = true;
services.zfs.autoScrub.enable = true;
# Ensure com.sun:auto-snapshot is set on ZFS datasets so auto-snapshots actually run
systemd.services."zfs-set-auto-snapshot" = {
description = "Set com.sun:auto-snapshot=true on ZFS datasets";
after = [ "zfs-import.target" ];
wants = [ "zfs-import.target" ];
wantedBy = [ "multi-user.target" ];
path = with pkgs; [ zfs ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${pkgs.zfs}/bin/zfs set -r com.sun:auto-snapshot=true rpool";
};
};
# Mi50 config
hardware.graphics = {
@@ -332,14 +402,17 @@
];
allowedUDPPorts = [
# Add UDP ports if required
51820 # WireGuard VPN
];
# Rate limiting and attack prevention
extraCommands = ''
# Rate limit SSH connections (max 4 new connections per 60 seconds)
# 1. Wipe the INPUT chain clean at the start of every activation
iptables -F INPUT
# Rate limit SSH connections (max 20 new connections per 60 seconds)
iptables -A INPUT -p tcp --dport 2424 -m state --state NEW -m recent --set
iptables -A INPUT -p tcp --dport 2424 -m state --state NEW -m recent --update --seconds 60 --hitcount 4 -j DROP
iptables -A INPUT -p tcp --dport 2424 -m state --state NEW -m recent --update --seconds 60 --hitcount 20 -j DROP
# Rate limit HTTP/HTTPS (protects Traefik)
iptables -A INPUT -p tcp --dport 80 -m state --state NEW -m limit --limit 25/minute --limit-burst 100 -j ACCEPT
@@ -350,6 +423,10 @@
# Log dropped packets (rate limited)
iptables -A INPUT -m limit --limit 5/min -j LOG --log-prefix "IPTables-Dropped: " --log-level 4
# 3. CRITICAL: Re-link the NixOS default firewall chain
# Without this line, the 'allowedTCPPorts' in your Nix config will be ignored!
iptables -A INPUT -j nixos-fw
'';
};
@@ -428,7 +505,7 @@
services.openssh.settings = {
PermitRootLogin = "no";
MaxAuthTries = 3;
MaxSessions = 5;
MaxSessions = 20;
LoginGraceTime = 30;
ClientAliveInterval = 300;
ClientAliveCountMax = 2;

View File

@@ -0,0 +1,167 @@
{ config, lib, pkgs, paths, self, ... }:
{
# Basic Host Info
networking.hostName = "uConsole";
time.timeZone = "America/Montreal";
i18n.defaultLocale = "en_CA.UTF-8";
# System State
system.stateVersion = "25.05";
# Boot & Hardware (uconsole-cm5 module handles boot.loader)
boot.kernelPackages = pkgs.linuxPackages_latest;
# Networking
networking.networkmanager.enable = true;
services.openssh = {
enable = true;
settings.PermitRootLogin = "prohibit-password";
settings.PasswordAuthentication = false;
};
# User
users.users.gortium = {
isNormalUser = true;
extraGroups = [ "wheel" "networkmanager" "video" "dialout" "kismet" ];
openssh.authorizedKeys.keys = [
keys.users.gortium.main
keys.users.gortium.gitea
];
};
security.sudo.extraRules = [
{
users = [ "gortium" ];
commands = [
{
command = "ALL";
options = [ "NOPASSWD" ];
}
];
}
];
# ============================================================
# Package groups
# ============================================================
environment.systemPackages = with pkgs; [
# ===== Base =====
emacs-pgtk
git
ripgrep
fd
htop
tmux
neovim
# ===== HAM Radio =====
js8call
wsjtx
fldigi
pat # Winlink client
direwolf # AX.25 packet modem
chirp # Radio programming tool
hamlib # Ham radio control libraries
trustedqsl # Logbook of the World (LoTW)
# ===== SDR / RF =====
sdrpp # SDR++ spectrum analyzer
gqrx # SDR receiver GUI
rtl-sdr # RTL-SDR drivers & utilities
inspectrum # Offline signal analysis
soapysdr-with-plugins # SoapySDR + hardware support plugins
# ===== Mesh / LoRa =====
meshtastic # Python CLI for Meshtastic devices
reticulumStack # Reticulum Network Stack (rnsd, rnsh, rncp, rnx, rnpath, etc.)
lxmf # LXMF messaging protocol
nomadnet # Nomad Network client
# ===== Security =====
nmap
aircrack-ng
kismet # Wi-Fi monitor / IDS
bettercap # MITM/network attack framework
wireshark # Packet analyzer
hashcat # GPU password cracker
john # John the Ripper
sqlmap # SQL injection tool
# ===== GPS / Maps =====
foxtrotgps
viking # GPS map editor
gpsbabel # GPS data conversion
];
# Packages noted but not in unstable nixpkgs:
# - metasploit: unfree; install manually via Git clone
# - burpsuite: unfree Java app (Community Edition available for download)
# - sidechannel: not a distinct PyPI package; functionality covered by
# the Reticulum stack. For LXMF GUI client, install Sideband manually
# from github.com/markqvist/Sideband
# ============================================================
# Reticulum Service (rnsd)
# ============================================================
systemd.services.rnsd = {
description = "Reticulum Network Stack Daemon";
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";
after = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = "gortium";
Group = "kismet";
ExecStart = "${pkgs.kismet}/bin/kismet -c wlan0 --log-base=/home/gortium/kismet_logs --no-nc-ui";
Restart = "always";
RestartSec = "10s";
};
};
# ============================================================
# Kernel modules for SDR and radio
# ============================================================
boot.kernelModules = [
"88x2bu" # Realtek 8812/8821BU USB WiFi (common adapter)
"rtl8xxxu" # RTL8188/8192/8723 USB WiFi
"rtl2832_sdr" # RTL-SDR kernel module
"dvb_usb_rtl28xxu" # RTL-SDR DVB-T
];
boot.blacklistedKernelModules = [ ];
# ============================================================
# Extra udev rules for SDR and HAM radio devices
# ============================================================
services.udev.packages = with pkgs; [ rtl-sdr ];
# ============================================================
# Enable IPv6 for Reticulum mesh
# ============================================================
networking.enableIPv6 = true;
# ============================================================
# Firewall: open ports for Reticulum (optional)
# ============================================================
networking.firewall.allowedTCPPorts = [ 22 ]; # SSH only
networking.firewall.allowedUDPPorts = [ ];
# Reticulum uses its own encryption and doesn't need open ports
# for basic mesh operations (peer-to-peer discovery).
# For TCP interfaces, open additional ports as needed.
}

View File

@@ -0,0 +1,26 @@
{ config, lib, pkgs, modulesPath, ... }:
{
imports =
[ (modulesPath + "/installer/scan/not-detected.nix")
];
boot.initrd.availableKernelModules = [ "xhci_pci" "usbhid" "usb_storage" "sdhci_pci" "nvme" ];
boot.initrd.kernelModules = [ ];
boot.extraModulePackages = [ ];
# uConsole CM5 uses NVMe or eMMC for boot storage
# The uconsole-cm5 module sets up /boot/firmware and default /
# Override device label here if using different storage
fileSystems."/" = lib.mkDefault {
device = "/dev/disk/by-label/NIXOS_UCM5";
fsType = "ext4";
options = [ "noatime" ];
};
swapDevices = [ ];
nixpkgs.hostPlatform = lib.mkDefault "aarch64-linux";
hardware.enableRedistributableFirmware = true;
powerManagement.cpuFreqGovernor = lib.mkDefault "ondemand";
}

View File

@@ -0,0 +1,400 @@
#!/usr/bin/env bash
# =============================================================================
# nixos-rollback.sh — NixOS systemd-boot Rollback Script
#
# Detects a failed NixOS generation (critical services not starting) and sets
# the previous generation as the default boot option for systemd-boot.
# Logs all actions to syslog/journald and a local logfile. Fails safely when
# no previous generation exists or required files are missing.
#
# Integration with the boot sentinel:
# sentinel-check.sh → detects Tier-1 service failures (sshd, docker,
# traefik, authelia) after a boot
# nixos-rollback.sh ← called when sentinel exits nonzero; sets previous
# generation as default for next boot
#
# Usage:
# nixos-rollback.sh # auto-detect & set previous gen
# nixos-rollback.sh --dry-run # show what would be done
# nixos-rollback.sh --rollback-now # also run nixos-rebuild switch
# # --rollback for immediate fix
# nixos-rollback.sh --help # full help text
#
# Exit codes:
# 0 — rollback applied (or dry-run would apply)
# 1 — preflight failure (missing files, permissions)
# 2 — no previous generation available
# 3 — nixos-rebuild --rollback failed (only with --rollback-now)
#
# Installation on NixOS:
# Place in /usr/local/bin/nixos-rollback.sh and make executable.
# Add a systemd oneshot service to run it after sentinel-check detects
# failures, or invoke directly from a sentinel timer.
# =============================================================================
set -euo pipefail
# ── Configuration ────────────────────────────────────────────────────────────
# These can be overridden via environment variables for testing.
LOADER_CONF="${NIXOS_ROLLBACK_LOADER_CONF:-/boot/loader/loader.conf}"
ENTRIES_DIR="${NIXOS_ROLLBACK_ENTRIES_DIR:-/boot/loader/entries}"
LOGFILE="${NIXOS_ROLLBACK_LOGFILE:-/var/log/nixos-rollback.log}"
SYSLOG_IDENT="nixos-rollback"
# ── CLI flags ────────────────────────────────────────────────────────────────
DRY_RUN=false
ROLLBACK_NOW=false
# ── Colors (disabled when not a terminal) ────────────────────────────────────
if [ -t 1 ]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
else
RED=''; GREEN=''; YELLOW=''; CYAN=''; NC=''
fi
# =============================================================================
# Help
# =============================================================================
usage() {
cat <<EOF
${CYAN}nixos-rollback.sh${NC} — Set the previous NixOS generation as systemd-boot default
${CYAN}USAGE${NC}
nixos-rollback.sh [OPTIONS]
${CYAN}OPTIONS${NC}
--dry-run Show what would be done without making changes
--rollback-now Also run 'nixos-rebuild switch --rollback' for
immediate fix of the running system (requires
nixos-rebuild on PATH)
-h, --help Show this help text
${CYAN}DESCRIPTION${NC}
Reads the current default boot entry from ${LOADER_CONF},
determines the previous generation number, and writes it as the
new default. The script only modifies systemd-boot config —
it does NOT touch the Nix store or system profile unless
--rollback-now is passed.
Designed as the rollback half of a boot sentinel:
1. System boots into generation N
2. sentinel-check.sh detects Tier-1 service failures
3. nixos-rollback.sh sets default to generation N-1
4. Next reboot uses the working generation
${CYAN}EXIT CODES${NC}
0 Rollback applied (or dry-run would apply)
1 Preflight failure (missing files, permissions)
2 No previous generation available (only one generation)
3 nixos-rebuild --rollback failed (with --rollback-now)
${CYAN}FILES${NC}
${LOADER_CONF} systemd-boot loader configuration
${ENTRIES_DIR}/ generation entry .conf files
${LOGFILE} action log (append-only)
EOF
}
# =============================================================================
# Logging
# =============================================================================
log() {
local level="$1"; shift
local msg="$*"
local timestamp
timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
echo "${timestamp} [${level}] ${msg}" >> "${LOGFILE}"
logger -t "${SYSLOG_IDENT}" -p "user.${level}" "${msg}"
# Also print to stderr for ERROR/WARN, stdout for INFO
case "${level}" in
ERROR) echo >&2 "${RED}[ERROR]${NC} ${msg}" ;;
WARN) echo >&2 "${YELLOW}[WARN]${NC} ${msg}" ;;
INFO) echo " ${GREEN}[INFO]${NC} ${msg}" ;;
esac
}
info() { log "INFO" "$@"; }
warn() { log "WARN" "$@"; }
error() { log "ERROR" "$@"; }
# =============================================================================
# Preflight checks
# =============================================================================
preflight() {
# Must run as root (need to write to /boot), unless overridden for testing
if [ -z "${NIXOS_ROLLBACK_SKIP_ROOT_CHECK:-}" ] && [ "$(id -u)" -ne 0 ]; then
error "This script must be run as root (needs write access to /boot/loader)"
error "Set NIXOS_ROLLBACK_SKIP_ROOT_CHECK=1 for testing against mock paths."
exit 1
fi
# Directories and files
if [ ! -d "${ENTRIES_DIR}" ]; then
error "Boot entries directory not found: ${ENTRIES_DIR}"
exit 1
fi
if [ ! -f "${LOADER_CONF}" ]; then
error "Loader config not found: ${LOADER_CONF}"
exit 1
fi
if [ ! -r "${LOADER_CONF}" ]; then
error "Cannot read loader config: ${LOADER_CONF}"
exit 1
fi
# Check write access to /boot/loader (parent of loader.conf)
local loader_dir
loader_dir="$(dirname "${LOADER_CONF}")"
if [ ! -w "${loader_dir}" ]; then
error "Cannot write to ${loader_dir} (insufficient permissions)"
exit 1
fi
# Logfile directory must exist
local log_dir
log_dir="$(dirname "${LOGFILE}")"
if [ ! -d "${log_dir}" ]; then
warn "Log directory ${log_dir} does not exist, creating it"
mkdir -p "${log_dir}" 2>/dev/null || {
error "Cannot create log directory ${log_dir}"
exit 1
}
fi
# Check --rollback-now dependencies
if [ "${ROLLBACK_NOW}" = true ]; then
if ! command -v nixos-rebuild &>/dev/null; then
error "nixos-rebuild not found on PATH (required for --rollback-now)"
exit 1
fi
fi
}
# =============================================================================
# Generation helpers
# =============================================================================
# get_current_default: reads the current default entry from loader.conf
# Returns: "nixos-generation-N.conf" or empty string
get_current_default() {
grep -E '^default\s+' "${LOADER_CONF}" 2>/dev/null \
| awk '{print $2}' \
|| true
}
# extract_gen_number: extracts the numeric generation from a conf filename
# Input: "nixos-generation-367.conf"
# Output: 367
extract_gen_number() {
echo "$1" | sed 's/nixos-generation-//;s/\.conf//'
}
# get_all_gen_numbers: returns sorted list of generation numbers from entries dir
get_all_gen_numbers() {
local -a gens=()
local f n
for f in "${ENTRIES_DIR}"/nixos-generation-*.conf; do
[ -f "${f}" ] || continue
n="$(basename "${f}" | sed 's/nixos-generation-//;s/\.conf//')"
gens+=("${n}")
done
if [ "${#gens[@]}" -eq 0 ]; then
return 1
fi
# Sort numerically and output
printf '%s\n' "${gens[@]}" | sort -n
}
# get_previous_gen: given current generation number, find the previous one
# from the list of all available generations
get_previous_gen() {
local current="$1"
shift
local -a gens=("$@")
local prev=""
local g
for g in "${gens[@]}"; do
if [ "${g}" -lt "${current}" ]; then
prev="${g}"
fi
done
if [ -z "${prev}" ]; then
return 1
fi
echo "${prev}"
}
# =============================================================================
# Main rollback logic
# =============================================================================
do_rollback() {
# Step 1: Read current default
local current_entry
current_entry="$(get_current_default)"
if [ -z "${current_entry}" ]; then
error "No 'default' entry found in ${LOADER_CONF}"
error "Cannot determine current generation — aborting"
exit 1
fi
info "Current default boot entry: ${current_entry}"
# Step 2: Build sorted list of all available generations
local -a all_gens=()
local line
while IFS= read -r line; do
all_gens+=("${line}")
done < <(get_all_gen_numbers || true)
if [ "${#all_gens[@]}" -eq 0 ]; then
error "No NixOS generation .conf files found in ${ENTRIES_DIR}"
exit 1
fi
info "Available generations: ${all_gens[*]}"
# Step 3: Find current generation number
local current_gen
current_gen="$(extract_gen_number "${current_entry}")"
# Verify current_gen is a valid number
if ! [[ "${current_gen}" =~ ^[0-9]+$ ]]; then
error "Could not parse generation number from '${current_entry}'"
exit 1
fi
# Step 4: Find the previous generation
local prev_gen
prev_gen="$(get_previous_gen "${current_gen}" "${all_gens[@]}")" || {
error "No previous generation found before generation ${current_gen}"
error "This is the oldest available generation — cannot roll back further"
exit 2
}
local prev_entry="nixos-generation-${prev_gen}.conf"
local prev_conf_path="${ENTRIES_DIR}/${prev_entry}"
if [ ! -f "${prev_conf_path}" ]; then
error "Previous generation entry not found: ${prev_conf_path}"
error "The .conf file for generation ${prev_gen} is missing — cannot roll back"
exit 1
fi
info "Target rollback generation: ${prev_gen}${prev_entry}"
# Step 5: Apply the rollback
if [ "${DRY_RUN}" = true ]; then
echo ""
echo " ${CYAN}[DRY RUN]${NC} Would change ${LOADER_CONF}:"
echo " ${YELLOW}-${NC} default ${current_entry}"
echo " ${GREEN}+${NC} default ${prev_entry}"
echo ""
info "DRY RUN — no changes made"
exit 0
fi
# Write new default
# Use sed with a backup (.bak)
sed -i.bak "s/^default\s\+${current_entry}/default ${prev_entry}/" "${LOADER_CONF}"
# Verify the change was applied
local new_default
new_default="$(get_current_default)"
if [ "${new_default}" != "${prev_entry}" ]; then
error "Failed to set default boot entry to ${prev_entry}"
error "Current default is still: ${new_default}"
# Attempt to restore backup
if [ -f "${LOADER_CONF}.bak" ]; then
cp "${LOADER_CONF}.bak" "${LOADER_CONF}"
info "Restored backup from ${LOADER_CONF}.bak"
fi
exit 1
fi
info "Successfully set default boot entry to ${prev_entry} (generation ${prev_gen})"
info "Backup of previous config saved to ${LOADER_CONF}.bak"
# Step 6: Optionally run nixos-rebuild switch --rollback
if [ "${ROLLBACK_NOW}" = true ]; then
echo ""
info "Running nixos-rebuild switch --rollback for immediate effect..."
if nixos-rebuild switch --rollback 2>&1 | while IFS= read -r line; do
logger -t "${SYSLOG_IDENT}" "nixos-rebuild: ${line}"
echo " ${line}"
done; then
info "nixos-rebuild switch --rollback completed successfully"
else
local rc=$?
error "nixos-rebuild switch --rollback failed with exit code ${rc}"
error "The boot default has been changed but the current system was NOT rolled back"
error "Reboot to apply the rollback"
exit 3
fi
fi
info "Rollback complete. Next boot will use generation ${prev_gen}."
if [ "${ROLLBACK_NOW}" = false ]; then
echo ""
echo " ${YELLOW}NOTE:${NC} The current running system is unchanged."
echo " Reboot to boot into generation ${prev_gen}."
echo " Or re-run with --rollback-now for immediate effect."
fi
}
# =============================================================================
# Main
# =============================================================================
main() {
# Parse arguments
while [ $# -gt 0 ]; do
case "$1" in
--dry-run)
DRY_RUN=true
shift
;;
--rollback-now)
ROLLBACK_NOW=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo >&2 "Unknown option: $1"
echo >&2 "Use --help for usage information."
exit 1
;;
esac
done
echo ""
echo " ${CYAN}═══ NixOS systemd-boot Rollback ═══${NC}"
echo ""
preflight
if [ "${DRY_RUN}" = true ]; then
info "DRY RUN mode — no changes will be made"
fi
if [ "${ROLLBACK_NOW}" = true ]; then
info "ROLLBACK NOW mode — will also run nixos-rebuild switch --rollback"
fi
echo ""
do_rollback
}
main "$@"

View File

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

View File

@@ -0,0 +1,184 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.rollbackSentinel;
# ── Scripts ────────────────────────────────────────────────────────────────
# Sentinel check — verifies Tier-1 services are active after boot.
# Exits nonzero when any Tier-1 service is down, which triggers the rollback.
sentinelCheck = pkgs.writeShellScriptBin "sentinel-check.sh" ''
#!/usr/bin/env bash
set -euo pipefail
SYSLOG_IDENT="nixos-sentinel"
LOGFILE="/var/log/nixos-sentinel.log"
echo "=== NixOS Sentinel Check ==="
echo "Tier-1 services: ${builtins.toString cfg.tier1Services}"
echo "Tier-2 services: ${builtins.toString cfg.tier2Services}"
FAILED=0
# Check Tier-1 services any failure means rollback
for svc in ${builtins.toString cfg.tier1Services}; do
if systemctl is-active --quiet "$svc" 2>/dev/null; then
echo " [OK] Tier-1: $svc"
else
echo " [FAIL] Tier-1: $svc is NOT active"
logger -t "$SYSLOG_IDENT" -p user.err "Tier-1 FAILURE: $svc is not active"
FAILED=1
fi
done
# Check Tier-2 services warn only
for svc in ${builtins.toString cfg.tier2Services}; do
if systemctl is-active --quiet "$svc" 2>/dev/null; then
echo " [OK] Tier-2: $svc"
else
echo " [WARN] Tier-2: $svc is NOT active"
logger -t "$SYSLOG_IDENT" -p user.warn "Tier-2 WARNING: $svc is not active"
fi
done
echo "=== Sentinel result: $([ "$FAILED" -eq 0 ] && echo 'PASS' || echo 'FAIL') ==="
echo "$(date '+%Y-%m-%d %H:%M:%S') [INFO] sentinel $([ "$FAILED" -eq 0 ] && echo 'PASS' || echo 'FAIL')" >> "$LOGFILE"
exit $FAILED
'';
# Rollback script — package the companion shell script from this directory.
# Uses builtins.readFile to embed the content at evaluation time.
rollbackScript = pkgs.writeShellScriptBin "nixos-rollback.sh" (builtins.readFile ./nixos-rollback.sh);
# Resolve rollback flags from config
rollbackFlags =
if cfg.rollbackMode == "dry-run" then "--dry-run"
else if cfg.rollbackMode == "rollback-now" then "--rollback-now"
else "";
in {
options.services.rollbackSentinel = {
enable = mkEnableOption "NixOS Rollback Sentinel auto-rollback on critical service failure";
tier1Services = mkOption {
type = types.listOf types.str;
default = [ "sshd" "docker" "traefik" "authelia" ];
description = ''
Tier-1 services whose failure triggers an automatic systemd-boot rollback.
On boot, the sentinel waits ${cfg.bootDelay} seconds, then checks each
service. If ANY service in this list is inactive, it runs the rollback
script which sets the previous NixOS generation as the default boot entry.
'';
};
tier2Services = mkOption {
type = types.listOf types.str;
default = [
"gitea" "hermes" "ollama" "synapse" "nextcloud"
"vaultwarden" "wireguard" "homeassistant" "fail2ban"
];
description = ''
Tier-2 services whose failure is logged as a warning but does NOT trigger
an automatic rollback. Useful for detecting non-critical service issues.
'';
};
tier3InfoServices = mkOption {
type = types.listOf types.str;
default = [
"act_runner" "syncthing" "restic" "fava"
"homer" "cups" "fstrim"
];
description = ''
Tier-3 informational checks (log-only, no warning). These are services
that the sentinel will note the status of for diagnostics.
'';
};
bootDelay = mkOption {
type = types.str;
default = "120";
description = ''
Seconds to wait after multi-user.target before running the boot-time
sentinel check. This gives Tier-1 services time to start before
the sentinel decides they've failed.
'';
};
rollbackMode = mkOption {
type = types.enum [ "set-default" "rollback-now" "dry-run" ];
default = "set-default";
description = ''
Rollback strategy when Tier-1 failures are detected:
- set-default: Write the previous generation to loader.conf (next reboot).
- rollback-now: Also run nixos-rebuild switch --rollback for immediate fix.
- dry-run: Log what would happen but take no action (testing).
'';
};
enablePostRebuild = mkOption {
type = types.bool;
default = true;
description = ''
When enabled, the sentinel check runs after every nixos-rebuild switch
activation. If a newly deployed generation has Tier-1 failures, it
triggers rollback immediately.
'';
};
};
config = mkIf cfg.enable {
# ── Deploy scripts to PATH ───────────────────────────────────────────────
environment.systemPackages = [ sentinelCheck rollbackScript ];
# Ensure log directory exists
systemd.tmpfiles.rules = [
"d /var/log/nixos-sentinel 0755 root root -"
];
# ── Boot-time sentinel service ───────────────────────────────────────────
# Runs after multi-user.target with a configurable delay, checks Tier-1
# services, and triggers rollback if any are down.
systemd.services.nixos-sentinel = {
description = "NixOS Boot Sentinel check critical services, roll back on failure";
after = [ "network.target" "multi-user.target" ];
wants = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
path = with pkgs; [ coreutils gawk gnused systemd ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStartPre = "${pkgs.coreutils}/bin/sleep ${cfg.bootDelay}";
ExecStart = "${sentinelCheck}/bin/sentinel-check.sh";
ExecStartPost = "${rollbackScript}/bin/nixos-rollback.sh ${rollbackFlags}";
};
};
# ── Post-rebuild sentinel service (triggered by activation script) ──────
systemd.services.nixos-sentinel-rebuild = mkIf cfg.enablePostRebuild {
description = "NixOS Post-Rebuild Sentinel check services after nixos-rebuild";
after = [ "network.target" ];
path = with pkgs; [ coreutils gawk gnused systemd ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${sentinelCheck}/bin/sentinel-check.sh";
ExecStartPost = "${rollbackScript}/bin/nixos-rollback.sh ${rollbackFlags}";
};
};
# Activation script — fires after every nixos-rebuild switch
system.activationScripts.rollback-sentinel = mkIf cfg.enablePostRebuild ''
# Start the post-rebuild sentinel in the background.
# This runs on every activation (boot + nixos-rebuild). On boot the
# boot-time service handles it, so this is primarily for nixos-rebuild,
# but running twice is safe (idempotent rollback).
systemctl start nixos-sentinel-rebuild.service --no-block 2>/dev/null || true
'';
};
}

77
overlays/reticulum.nix Normal file
View File

@@ -0,0 +1,77 @@
final: prev: let
python3 = final.python3;
pyPkgs = python3.pkgs;
in {
reticulumStack = python3.pkgs.buildPythonApplication rec {
pname = "reticulum";
version = "1.2.9";
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";
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";
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";
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,34 +1,36 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEdoTUQ4QSBOL29w
eGk1N2xxTHJtaUEvWWZmbkh1bk11Tjk3anNnMDB1cCtPYUMzdTNJCkdhQ08vblNG
UlV1K2xVTGZVTzFWYXAzcjZaMWs0RTFWdStKSmlSTURvK1EKLT4gLC1zKU8zVkgt
Z3JlYXNlIFUiXFcpS302IHByVn5jOy0gRDMKQjV3SHpDWUIybGFyQUg3ZlR0R2hV
eWM3SFlCVW5mdlpBVUF3a0xpNlZCeGNUd1oxTTlkc1RkTXdZS0lFTmN3Ci0tLSA3
VlBqM1VLWllZc0JnOTMvUFRjMU13OTdzMmhsdGJubkk5eGpERVVLYUk4Cnzh5UbU
FlgqpM8jkJ6XlsaIDCw/G3D6uJ/GRJW4gIekuhAUxpZJrc8eOA8ZuHfGrBbH3acV
tVafX5F0Kr2oOblqZ6gduZOUS52KmWH8stiBJM+e5ZZ7zRQVE4PJUKUPCzi+WdcH
zr295T//FOdicrYHdsjfziKEHzBtUCFiATW05+O2zMjYjO6cPzePcCzPWinwiID6
V+f6ngfkkQaj3wBGkzaieQJzRcdSwky21aVhGCCX/bvqx61iW2d5QAKxGbtQ2RcG
X1okr+xunAM94nzDMv46vyN97KxY7cZd4pAaOxoICc2Tfhtw6F+iS6QkQh1odJzO
7ZH+sSQCvndG+8z9shXGiHalASF5tdguM+JlEvAGljcaiAUtsQWxr9CoWiEkC6c6
NCaECSYO8Il+SXBQnSZSGJSNDhuPYCYrsjXGSAONFixuyeslAkq9x2WUaUS4H063
1QvRF7XO2tBPtgCLsSjdiGp0h+ImUaGdu6fDR7zrDsGsaAFCSFeH/rGNNXRQ2vP2
CSfPfDDCqpUSCn0WuA30BtaPLxGmZT6OjFevKzYMNDmdeq9ia/q8K0hmjLUBdN3k
tdYWbwoaf4gYbUWxSleD768b0Jgxss9Vod+sFQ+NYRksdGIeyND+aQIc312XehfA
qHFBS8nlj7eUF5bdvCYQ64z741mH4cNlGxyjPBH1x8FHnEOocJXYt1l2AZSRJmJA
c3z0QGXyuCbsrLBXWK1EKa/Juo4PGGsEVoLRhwJAQy9+i1JN0yrfRvSPyzvD4px6
wRPzlZ80MQdb2lv84WS/zcOEZmZzlLntszTRRdIfAsuaavP2Rquh4rEXABYeTZwp
5dem79s8bdW2nFsGMNz1OQKQwocyjYu1jJMHu6Gp7Ngdl1xyW7xfg0dezE1c0cIh
xt1aLER9YJp4n5to5cOH16l3mjDHnAvABx38xE9loNL3399J/evw7LxpTYQ4v2Xv
x8xnDHcqJ+deFSwyuUnMS5DkUeYuHmUl0Q2WYcfY+ibCmcgCb2ObTtuN1/ZxNYrL
OKrnmfuSvBgyuIOj5e6uWW0+Zs8dHKXu2TgV8WignxOhl5zQgCpCBlqVfO0t+NCu
Gi26hU/fhGWQ/1oQa3VkpGsypZbJpgQvfWxfcGHP/MMhnl01zzlP8/aexSY3pAxf
fz9v0IVh6xxtu3zbiiVzUsXbfG7t+xY98jMphf4AS2mWva3GWVmhhu0lS3J3P+go
YEEP4rOFHeU0Y1/6kLydTXvz4jMH0H92XQIzshd7vzQnEJPUPAzqRmw3LKYGgCI+
wZEnxJ6ckqTkGBFnxTpy9LLllwmnz2Ky87nY3XAmqxlhb2Ap1XFAlfgszmGjc+Il
KkIgoWQHTUm6QM9ta++oUTIDneOvxGd0zZsqoEhiC/7E01BNNZ6E58TeJU3fDlA3
mX6n05XjwPRpgXZfayPoAgBlZc2H4KeiynxwNZ/dWu7qz7L6Ppk6Nvtly8giTbFx
CA+tto7vq+D+CAEJ4bgyq4BCH4GL4APrhPcWp98Mko1WCiRTIKgkZxQCYvlg/LZq
LNhMacP9T1qTvNC+yR1NEMiegE3APzk6CkDpVaO9+5f/sqifNPINCMothenI9ePw
zjQLI3Mo1m73bkomytUZ7i1VstP5sEZ5LF72Sq7BpR3oQ3Gp0CAN9w==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEdoTUQ4QSBWNEpt
cGFNeVBBaDRqb3pLSEZGQW0wb3VmVnBoZCswUFkzbnBLUnJ0QTNrClRqVkk4RUVO
d29KYjd5YUcwankvaTFmVHUxQVpDT2ZYWHRaY3JXTUtQMU0KLT4gKXBtQ3UsXi1n
cmVhc2UgNnwxYCBXVyA/KCQmIHt9NAo3OTZVUHR2UXkvaEFwY0ZBdEJsaFpsbHJ0
cklKcDVHcEdWMEdPSkpnN0FiRU43RW5hUWFMdjR3WFRRSFBLSGlmClM3cTNJWlNM
TExkdHdXUHJISkNIaE1TTUxUc1NUWkV1a09HeFU3bVZwQXMKLS0tIGhOcXFTUElS
azJJNnEreUhMWTJBaVZGSTJPRUFqQkVYS01KRENUVVpZSDgK8+8onFejroBo7MeO
dW+so4lOsq4zJKn3f0cxmCFg1f0X8zt6h4Uc3A5Cvr1uU+6yw1FWmJ7xa3jJz3lO
EEaKQJXYC+xIIKGcA7qILa0SFp4a/4OuYjcg27HrlPhg7u5wDhQrd0LdVEe1Xngp
ZivX7P7HwIna3X8C+TL+K2v/AG2N/z86cdKfRvxyMKNbHhYw+CfHEnWgh8tJ++4h
G9evNniuNqte6cQaRe7jODfPNW4FuY/Sb7barlJ/M9iAQdYAdyLAzU1LABeHeUfD
wtHjxy9DUZ55Vg8bB8M2JJU9MkoRT4ewiVd9LeC1GWeVmKsm93wsmrov714i7U2j
wHtDkjqEF2MmzuQc18sjNaAHiwz8j6o5xU2L/Q4+Q707yISWG7RGZYh389Cr1rnw
siUq/Vunqw2wk13+J/4vu9nqt5mMktBaCtp+QiWIurjwB5LUAyChrSm+dg5lb0Mt
UhSc0lq1+E3vxAXM2Hmk+vP86VD+6WJvAU82VFApF1s6zG2FU1/AcOVVf54nan/q
f+rgSFfASHQCYSblUJHyEtwLNsWEmTGmOEn1buUKD/H0zatPQnc0rYpjlx2V0Sjd
6yB5+wPrZ0AkN1pjcsPKOv8Kaog2DzqIjib+SaSTaRxWHQEb9uzvaReAcYI5HOpE
gkC040HN33BItATbo4+hz70Im8Ni/VXD+g6yzM6Hj1hJL+PinTKeg5keQRFIZjMx
grzievB2wVBBgLgN3qMdTFmpplaL7iL702JjXZUTTK9Izp+9wiCsV1fTa53FWDht
ylFL5SWElqXjK+QBXxAe+Jk6VQov5HI21YDXL67S554ABeRok23wxrQ31TCI4xq9
PQV7VtNRjyVud7S29m3OwpWOsgTZhn+JclHj2v4bNJzJkJnZRTmcvGPktzRI5+R4
e5vxVhGnJDzI71txaHl8+xS1lu9VzCQUrxX6TXyTRV4KjIOz0g06JOBgmBRBvJca
7MZbC65xpisl/gyLRbgkVga3t94dPV+dpZsn8eq6427IyRbKslJefatggR9//c6I
5N5fl0fR3gJQMB+HRbipBH2YsdbdWJyb4Nn6STZxIfrqoG/xC6C1raF0xK7hUx6i
4DUDSPohM8fOIswQPfE+FH3eygfzu/Ln5+ghsgHTEhgFvmgMvyxaAt6kHIzIUhMX
M3dASr4VPDpIXuXsRWwYLEifhzxsuvwVxfwtsnCaR6XKijsYECWGDdYOWHdleeqx
wDPhxEesfFVhKxhrKY9Ir8k9/FFBKQU/3GjW4+SMAg5Al1YEzxshP9vKuVcsei7W
JDwAwotNXaCm6NBckiyZJE53ou6+gckPY7V9cOfnuH74Z9ywkFzB3HW3ZlonaGyM
oGmLGcccavFtyhg5s/As4i6X8ARIpDiwe59Pn3GNXMctySqIrrr2ogUoXgrfFCie
6GOTdeMW7GeOSdJUxCofghlspS/nq01Og77VI/beWYrIwLubSka6Zaltww9zgObk
/FGEMgFkEpq7iyCvYSPA8F46pJKvnMP3S84AWCPmcTcHeg4lwGPvs6btexXBGdoz
nkCyq7wdH5Nngm7jUbl88LtaLZPAQkuqXphBVTnrF9Ofbnb4iRZ2Op4xpx9rGyvx
mO6UEhL6V1i2YZFNkNMg/W8aoMiUgBdqbkxaxblT9L0aNdlFU9+LbWYolURVEadd
Qjv0Z1gMA+tsuBbVszwsMfneZ5+B9Q==
-----END AGE ENCRYPTED FILE-----

View File

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

View File

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