feat: herdr + agent network over netbird

This commit is contained in:
2026-08-23 14:39:31 +02:00
parent 1ba70151ef
commit 7313e167b1
63 changed files with 384 additions and 735 deletions
+14 -1
View File
@@ -21,9 +21,22 @@ in {
};
};
programs.virt-manager.enable = true;
systemd.services.virt-secret-init-encryption.enable = false;
environment = {
systemPackages = [pkgs.qemu];
etc = {
"ovmf/OVMF_CODE.fd" = {
source = "${(pkgs.OVMF.override {
secureBoot = true;
tpmSupport = true;
}).fd}/FV/OVMF_CODE.fd";
};
"ovmf/OVMF_VARS.fd" = {
source = "${(pkgs.OVMF.override {
secureBoot = true;
tpmSupport = true;
}).fd}/FV/OVMF_VARS.fd";
};
};
};
};
}
+3 -2
View File
@@ -210,7 +210,7 @@
in {
# ── NixOS user definition ──
users.users.m3tam3re = {
password = "12345";
hashedPassword = "$y$j9T$ORX4btVZgs9Xjq2oIvzJm0$lXiPwaa0D6t.eMDIx1UBesEAMOkWXBoGwpeI7X0aS8D";
isNormalUser = true;
description = "m3tam3re";
extraGroups = [
@@ -228,8 +228,9 @@ in {
];
openssh.authorizedKeys.keys = [
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC3YEmpYbM+cpmyD10tzNRHEn526Z3LJOzYpWEKdJg8DaYyPbDn9iyVX30Nja2SrW4Wadws0Y8DW+Urs25/wVB6mKl7jgPJVkMi5hfobu3XAz8gwSdjDzRSWJrhjynuaXiTtRYED2INbvjLuxx3X8coNwMw58OuUuw5kNJp5aS2qFmHEYQErQsGT4MNqESe3jvTP27Z5pSneBj45LmGK+RcaSnJe7hG+KRtjuhjI7RdzMeDCX73SfUsal+rHeuEw/mmjYmiIItXhFTDn8ZvVwpBKv7xsJG90DkaX2vaTk0wgJdMnpVIuIRBa4EkmMWOQ3bMLGkLQeK/4FUkNcvQ/4+zcZsg4cY9Q7Fj55DD41hAUdF6SYODtn5qMPsTCnJz44glHt/oseKXMSd556NIw2HOvihbJW7Rwl4OEjGaO/dF4nUw4c9tHWmMn9dLslAVpUuZOb7ykgP0jk79ldT3Dv+2Hj0CdAWT2cJAdFX58KQ9jUPT3tBnObSF1lGMI7t77VU= m3tam3re@m3-nix"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBZcjCKl0DRuOUOMXbM0GKY5JjvmyFpVZ/tRlTKWu/zp razr"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEZbg/Z9mnflXuLahGY8WOSBMqbgeqVIkIwRkquys1Ml sascha.koenig@azintec.com"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKCTBFMPCOpKerJBlcvH44602sGyJvgXrwYUegYf96D0 m3ta-adm"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBZcjCKl0DRuOUOMXbM0GKY5JjvmyFpVZ/tRlTKWu/zp"
];
packages = [inputs.home-manager.packages.${pkgs.stdenv.hostPlatform.system}.default];
};
+1
View File
@@ -5,6 +5,7 @@
# here, NOT in environment.systemPackages
];
programs.fish.enable = true;
programs.mosh.enable = true;
programs.nh = {
enable = true;
clean.enable = true;
@@ -1,14 +1,12 @@
{lib, ...}: {
imports = [
./baserow.nix
# ./baserow.nix
./ghost.nix
./honcho.nix
./kestra.nix
# ./honcho.nix
./littlelink.nix
./matomo.nix
# ./matomo.nix
./netbird.nix
# ./n8n.nix
# ./pangolin.nix
./restreamer.nix
./slash.nix
./slash-nemoti.nix
@@ -16,7 +14,7 @@
];
system.activationScripts.createPodmanNetworkWeb = lib.mkAfter ''
if ! /run/current-system/sw/bin/podman network exists web; then
/run/current-system/sw/bin/podman network create web --subnet=10.89.0.0/24 --internal
/run/current-system/sw/bin/podman network create web --subnet=10.89.0.0/24 --gateway=10.89.0.1
fi
'';
}
+1 -1
View File
@@ -4,7 +4,7 @@
environmentFiles = [config.age.secrets.ghost-env.path];
ports = ["127.0.0.1:3002:2368"];
volumes = ["ghost_data:/var/lib/ghost/content"];
extraOptions = ["--add-host=mysql:10.89.0.1" "--ip=10.89.0.11" "--network=web"];
extraOptions = ["--add-host=mysql:10.89.0.1" "--ip=10.89.0.11" "--network=web" "--dns=8.8.8.8"];
};
# Traefik configuration specific to ghost
services.traefik.dynamicConfigOptions.http = {
@@ -1,209 +0,0 @@
{
config,
lib,
pkgs,
...
}: let
serviceName = "honcho";
image = "ghcr.io/plastic-labs/honcho:v3.0.6";
apiIp = "10.89.0.24";
deriverIp = "10.89.0.25";
redisIp = "10.89.0.26";
postgresHost = "10.89.0.1";
postgresPort = config.m3ta.ports.get "postgres";
honchoPort = config.m3ta.ports.get "honcho";
# m3-atlas Netbird mesh address, discovered from `netbird status -d`.
# Binding the host port here keeps self-hosted Honcho off public interfaces.
netbirdBindAddress = "100.81.142.56";
netbirdRange = "100.64.0.0/16";
dbName = "honcho";
dbUser = "honcho";
redisName = "${serviceName}-redis";
runtimeDirectory = "/run/${serviceName}";
runtimeEnvFile = "${runtimeDirectory}/env";
# Keep auth disabled for the first deployment because Honcho clients need
# generated JWTs. The JWT secret is still provisioned so enabling auth later is
# a one-line change here plus client token generation.
authUseAuth = false;
sharedEnvironment = {
CACHE_ENABLED = "true";
CACHE_URL = "redis://${redisName}:6379/0?suppress=true";
LOG_LEVEL = "INFO";
TELEMETRY_ENABLED = "false";
VECTOR_STORE_MIGRATED = "false";
VECTOR_STORE_TYPE = "pgvector";
AUTH_USE_AUTH = lib.boolToString authUseAuth;
};
sharedEnvironmentFiles = [
runtimeEnvFile
config.age.secrets."${serviceName}-selfhost-env".path
];
webNetwork = ip: [
"--add-host=postgres:${postgresHost}"
"--network=web:ip=${ip}"
];
# The shared web network is intentionally internal. API and deriver also join
# this egress-only network so LLM provider calls can leave the host without
# exposing any extra inbound ports.
networksWithEgress = ip:
(webNetwork ip)
++ [
"--network=${serviceName}-egress"
];
apiHealthCmd = ''/app/.venv/bin/python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health', timeout=2).read()"'';
in {
system.activationScripts.createPodmanNetworkHonchoEgress = lib.mkAfter ''
if ! /run/current-system/sw/bin/podman network exists ${serviceName}-egress; then
/run/current-system/sw/bin/podman network create ${serviceName}-egress
fi
'';
virtualisation.oci-containers.containers = {
"${serviceName}-redis" = {
image = "docker.io/redis:8.2";
autoStart = true;
volumes = ["${serviceName}_redis_data:/data"];
extraOptions =
(webNetwork redisIp)
++ [
"--health-cmd=redis-cli ping"
"--health-interval=5s"
"--health-timeout=5s"
"--health-retries=5"
];
};
"${serviceName}-api" = {
inherit image;
autoStart = true;
entrypoint = "sh";
cmd = ["docker/entrypoint.sh"];
environment = sharedEnvironment;
environmentFiles = sharedEnvironmentFiles;
ports = ["${netbirdBindAddress}:${toString honchoPort}:8000"];
dependsOn = [redisName];
extraOptions =
(networksWithEgress apiIp)
++ [
"--health-cmd=${apiHealthCmd}"
"--health-interval=5s"
"--health-timeout=5s"
"--health-retries=5"
"--health-start-period=10s"
];
};
"${serviceName}-deriver" = {
inherit image;
autoStart = true;
entrypoint = "/app/.venv/bin/python";
cmd = ["-m" "src.deriver"];
environment = sharedEnvironment;
environmentFiles = sharedEnvironmentFiles;
dependsOn = ["${serviceName}-api" redisName];
extraOptions = networksWithEgress deriverIp;
};
};
systemd.services = {
"${serviceName}-postgres-bootstrap" = {
description = "Bootstrap Honcho PostgreSQL role, database, password, and pgvector";
after = ["postgresql.service" "agenix.service"];
requires = ["postgresql.service" "agenix.service"];
before = ["${serviceName}-env.service" "podman-${serviceName}-api.service" "podman-${serviceName}-deriver.service"];
requiredBy = ["podman-${serviceName}-api.service" "podman-${serviceName}-deriver.service"];
path = [
config.services.postgresql.package
pkgs.coreutils
];
serviceConfig = {
Type = "oneshot";
User = "postgres";
Group = "postgres";
};
script = ''
set -euo pipefail
test -s ${config.age.secrets."${serviceName}-selfhost-db-password".path}
psql -v ON_ERROR_STOP=1 --dbname=postgres <<'SQL'
DO $$
BEGIN
CREATE ROLE ${dbUser} LOGIN;
EXCEPTION WHEN duplicate_object THEN
NULL;
END
$$;
SELECT 'CREATE DATABASE ${dbName} OWNER ${dbUser}'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '${dbName}')\gexec
ALTER DATABASE ${dbName} OWNER TO ${dbUser};
\set honcho_password `cat ${config.age.secrets."${serviceName}-selfhost-db-password".path}`
ALTER ROLE ${dbUser} WITH LOGIN PASSWORD :'honcho_password';
SQL
psql -v ON_ERROR_STOP=1 --dbname=${dbName} <<'SQL'
CREATE EXTENSION IF NOT EXISTS vector;
GRANT ALL PRIVILEGES ON DATABASE ${dbName} TO ${dbUser};
SQL
'';
};
"${serviceName}-env" = {
description = "Generate Honcho runtime environment file with agenix secrets";
after = ["agenix.service" "${serviceName}-postgres-bootstrap.service"];
requires = ["agenix.service" "${serviceName}-postgres-bootstrap.service"];
before = ["podman-${serviceName}-api.service" "podman-${serviceName}-deriver.service"];
requiredBy = ["podman-${serviceName}-api.service" "podman-${serviceName}-deriver.service"];
path = [
pkgs.coreutils
pkgs.python3
];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
set -euo pipefail
install -d -m 0750 ${runtimeDirectory}
db_password_encoded=$(
python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip(), safe=""))' \
< ${config.age.secrets."${serviceName}-selfhost-db-password".path}
)
jwt_secret=$(tr -d '\r\n' < ${config.age.secrets."${serviceName}-selfhost-jwt-secret".path})
umask 077
cat > ${runtimeEnvFile} <<ENV
DB_CONNECTION_URI=postgresql+psycopg://${dbUser}:$db_password_encoded@postgres:${toString postgresPort}/${dbName}
AUTH_JWT_SECRET=$jwt_secret
ENV
'';
};
"podman-${serviceName}-api" = {
after = ["${serviceName}-env.service" "${serviceName}-postgres-bootstrap.service"];
requires = ["${serviceName}-env.service" "${serviceName}-postgres-bootstrap.service"];
};
"podman-${serviceName}-deriver" = {
after = ["${serviceName}-env.service" "${serviceName}-postgres-bootstrap.service"];
requires = ["${serviceName}-env.service" "${serviceName}-postgres-bootstrap.service"];
};
};
networking.firewall.extraCommands = ''
# Self-hosted Honcho API: only Netbird mesh peers may reach ${netbirdBindAddress}:${toString honchoPort}.
ip46tables -A nixos-fw -p tcp --dport ${toString honchoPort} -s ${netbirdRange} -j nixos-fw-accept
'';
}
@@ -1,32 +0,0 @@
{config, ...}: {
virtualisation.oci-containers.containers."kestra" = {
image = "docker.io/kestra/kestra:latest";
environmentFiles = [config.age.secrets.kestra-env.path];
cmd = ["server" "standalone" "--config" "/etc/config/application.yaml"];
ports = ["127.0.0.1:3018:8080"];
user = "root";
volumes = [
"/var/run/docker.sock:/var/run/docker.sock"
"${config.age.secrets.kestra-config.path}:/etc/config/application.yaml"
"kestra_data:/app/storage"
"/tmp/kestra-wd:/tmp/kestra-wd"
];
extraOptions = ["--add-host=postgres:10.89.0.1" "--ip=10.89.0.18" "--network=web"];
};
systemd.tmpfiles.rules = [
"d /tmp/kestra-wd 0750 1000 1000 - -"
];
# Traefik configuration specific to littlelink
services.traefik.dynamicConfigOptions.http = {
services.kestra.loadBalancer.servers = [{url = "http://localhost:3018/";}];
routers.kestra = {
rule = "Host(`k.m3ta.dev`)";
tls = {certResolver = "godaddy";};
service = "kestra";
entrypoints = "websecure";
};
};
}
@@ -1,5 +1,6 @@
{
config,
lib,
pkgs,
...
}: let
@@ -121,7 +122,7 @@ in {
virtualisation.oci-containers.containers = {
"${serviceName}-dashboard" = {
image = "netbirdio/dashboard:latest";
image = "docker.io/netbirdio/dashboard:latest";
autoStart = true;
environmentFiles = [config.age.secrets."${serviceName}-dashboard-env".path];
extraOptions = [
@@ -131,7 +132,7 @@ in {
};
"${serviceName}-server" = {
image = "netbirdio/netbird-server:latest";
image = "docker.io/netbirdio/netbird-server:latest";
autoStart = true;
ports = ["${toString stunPort}:${toString stunPort}/udp"];
environmentFiles = [config.age.secrets."${serviceName}-server-env".path];
@@ -147,12 +148,15 @@ in {
};
"${serviceName}-proxy" = {
image = "netbirdio/reverse-proxy:latest";
image = "docker.io/netbirdio/reverse-proxy:latest";
autoStart = true;
ports = ["${toString wireguardPort}:${toString wireguardPort}/udp"];
volumes = [
"${serviceName}_proxy_certs:/certs"
];
environment = {
NB_PROXY_PRIVATE = "true";
};
environmentFiles = [config.age.secrets."${serviceName}-proxy-env".path];
cmd = [
"--domain=${proxyDomain}"
+1 -1
View File
@@ -7,7 +7,7 @@
./rustfs.nix
./mysql.nix
./netbird.nix
./n8n.nix
# ./n8n.nix
./paperless.nix
./postgres.nix
./searx.nix
@@ -5,7 +5,7 @@
}: {
services.gitea-actions-runner = {
instances.default = {
enable = true;
enable = false;
name = "${config.networking.hostName}-runner";
url = "https://code.m3ta.dev";
tokenFile = config.age.secrets.gitea-runner-token.path;
+6 -1
View File
@@ -1,4 +1,9 @@
{config, ...}: {
{
config,
inputs,
pkgs,
...
}: {
services.paperless = {
enable = true;
port = config.m3ta.ports.get "paperless";
+1 -5
View File
@@ -18,17 +18,13 @@
# Localhost connections (IPv4 and IPv6)
host all postgres 127.0.0.1/32 scram-sha-256
host all postgres ::1/128 scram-sha-256
host outline outline 127.0.0.1/32 scram-sha-256
host outline outline ::1/128 scram-sha-256
host paperless paperless 127.0.0.1/32 scram-sha-256
host paperless paperless ::1/128 scram-sha-256
# Podman network connections for Baserow
host baserow baserow 10.89.0.0/24 scram-sha-256
host kestra kestra 10.89.0.0/24 scram-sha-256
host netbird netbird 10.89.0.0/24 scram-sha-256
host authentik authentik 10.89.0.0/24 scram-sha-256
host honcho honcho 10.89.0.0/24 scram-sha-256
# Deny all other connections
local all all reject
@@ -39,7 +35,7 @@
services.postgresqlBackup = {
enable = true;
startAt = "03:10:00";
databases = ["baserow" "paperless" "kestra" "authentik" "netbird" "honcho"];
databases = ["baserow" "paperless" "authentik" "netbird"];
};
networking.firewall = {
extraCommands = ''
+23 -7
View File
@@ -8,10 +8,8 @@
enable = true;
environmentFile = config.age.secrets.rustfs-env.path;
settings = {
# Reuse existing MinIO data directory
RUSTFS_VOLUMES = "/var/storage/s3";
# Keep same ports as MinIO to avoid changing Traefik and client configs
RUSTFS_ADDRESS = ":3008";
RUSTFS_CONSOLE_ENABLE = "true";
RUSTFS_CONSOLE_ADDRESS = ":3007";
@@ -22,18 +20,18 @@
# Traefik configuration — same routes as before
services.traefik.dynamicConfigOptions.http = {
services.minio-console.loadBalancer.servers = [
services.rustfs-console.loadBalancer.servers = [
{
url = "http://localhost:3007/";
}
];
services.minio.loadBalancer.servers = [
services.rustfs.loadBalancer.servers = [
{
url = "http://localhost:3008/";
}
];
routers.minio = {
routers.rustfs = {
rule = "Host(`s3.m3tam3re.com`)";
tls = {
certResolver = "godaddy";
@@ -41,13 +39,31 @@
service = "minio";
entrypoints = "websecure";
};
routers.minio-console = {
routers.rustfs-console = {
rule = "Host(`s3c.m3ta.dev`)";
tls = {
certResolver = "godaddy";
};
service = "rustfs-console";
entrypoints = "websecure";
};
routers.rustfs-old = {
rule = "Host(`s3.m3ta.dev`)";
tls = {
certResolver = "godaddy";
};
service = "rustfs";
entrypoints = "websecure";
middlewares = ["subdomain-redirect"];
};
routers.rustfs-console-old = {
rule = "Host(`minio.m3tam3re.com`)";
tls = {
certResolver = "godaddy";
};
service = "minio-console";
service = "rustfs";
entrypoints = "websecure";
middlewares = ["subdomain-redirect"];
};
};
}
+4 -9
View File
@@ -37,6 +37,10 @@
};
websecure = {
address = ":443";
# NetBird (signal/management gRPC, proxy mapping stream) requires
# long-lived HTTP/2 streams — see https://docs.netbird.io/selfhosted/maintenance/configuration-files
# Default readTimeout (60s) kills the streams every minute.
transport.respondingTimeouts.readTimeout = "0s";
};
};
};
@@ -85,15 +89,6 @@
};
routers = {
# ── Hermes Dashboard — Netbird mesh only ─────────────────────
hermes-dashboard = {
rule = "Host(`dash.m3ta.dev`)";
service = "hermes-dashboard";
entrypoints = ["websecure"];
tls = {
certResolver = "godaddy";
};
};
api = {
rule = "Host(`r.m3tam3re.com`)";
service = "api@internal";
+1
View File
@@ -1,6 +1,7 @@
{pkgs, ...}: {
programs.nix-ld.enable = true;
programs.nix-ld.libraries = with pkgs; [];
programs.mosh.enable = true;
programs.fish.enable = true;
programs.nh = {
enable = true;
+6 -1
View File
@@ -73,6 +73,10 @@ in {
extraPackages = with pkgs; [
basecamp
docker
herdr
opencode
pi-coding-agent
bun
git
curl
jq
@@ -123,7 +127,7 @@ in {
settings = {
# ── Model ──────────────────────────────────────────────────────────
model = {
default = "glm-5.2";
default = "glm-5.3";
provider = "zai";
};
@@ -290,6 +294,7 @@ in {
isNormalUser = false;
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICAVF7jGP1S6vc5CxeBFD/UxiImHOgbPlKg8WYyNtOA3"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBZcjCKl0DRuOUOMXbM0GKY5JjvmyFpVZ/tRlTKWu/zp"
];
};
}
+1 -4
View File
@@ -1,4 +1,4 @@
{lib, ...}: {
{
services.n8n = {
enable = true;
openFirewall = true;
@@ -7,7 +7,4 @@
N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS = "false";
};
};
# Temporary fix for upstream module
systemd.services.n8n.serviceConfig.LoadCredential = lib.mkForce [];
systemd.services.n8n.environment.N8N_RUNNERS_AUTH_TOKEN_FILE = lib.mkForce null;
}