feat: enable n8n AI assistant AZ-PRM-1

This commit is contained in:
2026-09-08 10:45:26 +02:00
parent 1772ab207e
commit ef563f1ce1
13 changed files with 887 additions and 744 deletions
+3
View File
@@ -59,6 +59,9 @@ in {
n8n-runner-auth-token = {
file = ../../secrets/n8n-runner-auth-token-prm.age;
};
n8n-sandbox-env = {
file = ../../secrets/n8n-sandbox-env-prm.age;
};
pgadmin-pw = {
file = ../../secrets/pgadmin-pw.age;
owner = "pgadmin";
@@ -4,6 +4,7 @@
./atrocore.nix
./excalidraw.nix
./kestra.nix
./n8n-sandbox.nix
./online3dviewer.nix
./semaphore.nix
./stirling-pdf.nix
@@ -15,5 +16,17 @@
if ! /run/current-system/sw/bin/podman network exists web-dev; then
/run/current-system/sw/bin/podman network create web-dev --subnet=10.89.1.0/24 --internal
fi
# Routed egress network: unlike web/web-dev (isolated), netavark sets up
# a default gateway plus NAT/masquerade for attached containers —
# outbound via host, incl. the NetBird overlay (wt0, 100.91.0.0/16).
if ! /run/current-system/sw/bin/podman network exists vpn-egress; then
/run/current-system/sw/bin/podman network create vpn-egress --subnet=10.89.9.0/24
fi
# n8n AI-Assistant sandbox stack (n8n-sandbox.nix): routed, NOT --internal
# — the privileged DinD runner must pull its sandbox images from ghcr.io.
# DNS between sandbox-api/sandbox-runner-1 via netavark/aardvark.
if ! /run/current-system/sw/bin/podman network exists n8n-sandbox; then
/run/current-system/sw/bin/podman network create n8n-sandbox --subnet=10.89.10.0/24
fi
'';
}
@@ -0,0 +1,90 @@
{config, ...}: let
sandboxApiPort = config.m3ta.ports.get "n8n-sandbox-api";
tlsDir = "/var/lib/n8n-sandbox/tls";
apiImage = "ghcr.io/n8n-io/n8n-sandbox-service-api:1.2.0";
runnerImage = "ghcr.io/n8n-io/n8n-sandbox-service-runner-dind:1.2.0";
sandboxImage = "ghcr.io/n8n-io/n8n-sandbox-service-sandbox:latest";
in {
virtualisation.oci-containers.containers = {
sandbox-api = {
image = apiImage;
environmentFiles = [config.age.secrets.n8n-sandbox-env.path];
environment = {
SANDBOX_API_GRPC_TLS_CERT_FILE = "/tls/api/grpc-server.crt";
SANDBOX_API_GRPC_TLS_KEY_FILE = "/tls/api/grpc-server.key";
SANDBOX_API_GRPC_TLS_CLIENT_CA_FILE = "/tls/api/ca.crt";
SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_CA_FILE = "/tls/api/ca.crt";
SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_CERT_FILE = "/tls/api/control-grpc-api-client.crt";
SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_KEY_FILE = "/tls/api/control-grpc-api-client.key";
SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_SERVER_NAME = "sandbox-runner-1";
};
volumes = ["${tlsDir}:/tls:ro"];
ports = ["127.0.0.1:${toString sandboxApiPort}:8080"];
extraOptions = ["--network=n8n-sandbox"];
};
sandbox-runner-1 = {
image = runnerImage;
environmentFiles = [config.age.secrets.n8n-sandbox-env.path];
environment = {
SANDBOX_RUNNER_API_GRPC_ADDR = "sandbox-api:9090";
SANDBOX_RUNNER_HTTP_BASE_URL = "http://sandbox-runner-1:8080";
SANDBOX_RUNNER_CONTROL_GRPC_LISTEN_ADDR = ":9091";
SANDBOX_RUNNER_CONTROL_GRPC_ADVERTISE_ADDR = "sandbox-runner-1:9091";
SANDBOX_RUNNER_ID = "runner-1";
SANDBOX_RUNNER_DOCKER_SANDBOX_IMAGE = sandboxImage;
SANDBOX_RUNNER_REGISTRATION_GRPC_CA_FILE = "/tls/runner/ca.crt";
SANDBOX_RUNNER_REGISTRATION_GRPC_CERT_FILE = "/tls/runner/grpc-client.crt";
SANDBOX_RUNNER_REGISTRATION_GRPC_KEY_FILE = "/tls/runner/grpc-client.key";
SANDBOX_RUNNER_REGISTRATION_GRPC_SERVER_NAME = "sandbox-api";
SANDBOX_RUNNER_CONTROL_GRPC_TLS_CERT_FILE = "/tls/runner/control-grpc-server.crt";
SANDBOX_RUNNER_CONTROL_GRPC_TLS_KEY_FILE = "/tls/runner/control-grpc-server.key";
SANDBOX_RUNNER_CONTROL_GRPC_TLS_CLIENT_CA_FILE = "/tls/runner/ca.crt";
};
volumes = ["${tlsDir}:/tls:ro"];
dependsOn = ["sandbox-api"];
extraOptions = ["--network=n8n-sandbox" "--privileged"];
};
};
systemd.services.n8n-sandbox-certs = {
description = "n8n sandbox: mTLS certificate bootstrap";
wantedBy = ["multi-user.target"];
after = ["network-online.target"];
wants = ["network-online.target"];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ConditionPathExists = "!${tlsDir}/ca.crt";
};
path = [config.virtualisation.podman.package];
preStart = ''
mkdir -p ${tlsDir}
'';
script = ''
podman run --rm \
--name n8n-sandbox-certs \
--user 0:0 \
-e NUM_RUNNERS=1 \
-v ${tlsDir}:/tls \
--entrypoint sh \
${apiImage} \
-c 'bootstrap-mtls.sh --out-dir /tls --api-san sandbox-api --control-san-prefix sandbox-runner --world-readable && chown -R sandbox-api:sandbox-api /tls/api && chmod -R a+rX /tls'
'';
};
systemd.services."podman-sandbox-api" = {
after = ["n8n-sandbox-certs.service"];
requires = ["n8n-sandbox-certs.service"];
preStart = ''
${config.virtualisation.podman.package}/bin/podman rm -f sandbox-api 2>/dev/null || true
'';
};
systemd.services."podman-sandbox-runner-1" = {
after = ["n8n-sandbox-certs.service"];
requires = ["n8n-sandbox-certs.service"];
preStart = ''
${config.virtualisation.podman.package}/bin/podman rm -f sandbox-runner-1 2>/dev/null || true
'';
};
}
@@ -7,8 +7,6 @@
servicePort = config.m3ta.ports.get serviceName;
in {
virtualisation.oci-containers.containers."${serviceName}" = {
# Image contains the Semaphore server plus ansible, terraform,
# opentofu and terragrunt runners.
image = "docker.io/semaphoreui/semaphore:latest";
environment = {
SEMAPHORE_DB_DIALECT = "postgres";
@@ -16,26 +14,24 @@ in {
SEMAPHORE_DB_PORT = "5432";
SEMAPHORE_PLAYBOOK_PATH = "/tmp/semaphore/";
};
# SEMAPHORE_DB (name), SEMAPHORE_DB_USER, SEMAPHORE_DB_PASS and the
# admin bootstrap vars (SEMAPHORE_ADMIN*) come from the agenix secret
# (podman env-file).
environmentFiles = [config.age.secrets.semaphore-env.path];
ports = ["127.0.0.1:${toString servicePort}:3000"];
volumes = [
"semaphore_data:/var/lib/semaphore"
];
# Public DNS: the corporate DNS behind aardvark answers AAAA queries for
# galaxy.ansible.com but not A queries — with no IPv6 route in the web
# network ansible-galaxy downloads failed (EAI_AGAIN / empty responses).
# Bypass with public resolvers; git.az-gruppe.com is publicly resolvable,
# targets connect by IP (same pattern as librechat/homarr on AZ-CLD-1).
extraOptions = ["--network=web" "--ip=10.89.0.17" "--add-host=postgres:10.89.0.1" "--dns=8.8.8.8" "--dns=8.8.4.4"];
extraOptions = [
"--network=web:ip=10.89.0.17"
"--network=vpn-egress"
"--add-host=postgres:10.89.0.1"
"--dns=8.8.8.8"
"--dns=8.8.4.4"
];
};
# Idempotent provisioning of the semaphore role/database on the host
# PostgreSQL 17 (same pattern as atrocore-db-init: initialScript cannot
# be used, the cluster is already initialized and the password must
# never land in the Nix store).
networking.firewall.extraForwardRules = ''
iifname "vpn-egress" oifname "wt0" accept
'';
systemd.services.semaphore-db-init = {
description = "Provision semaphore role/database from agenix secret";
after = ["postgresql.service"];
@@ -49,8 +45,6 @@ in {
RemainAfterExit = true;
};
path = [config.services.postgresql.package];
# Env-file format: plain KEY=value lines (no quotes), same file the
# container consumes.
script = ''
set -euo pipefail
ENV_FILE="${config.age.secrets.semaphore-env.path}"
+24 -3
View File
@@ -1,25 +1,46 @@
{config, ...}: let
serviceName = "n8n";
servicePort = config.m3ta.ports.get serviceName;
sandboxApiPort = config.m3ta.ports.get "n8n-sandbox-api";
sambaMounts = [
"/mnt/AutoAblage"
"/mnt/SkriptHelper"
"/mnt/DMS-ALT-INBOX"
"/mnt/DMS-INBOX"
];
in {
services.n8n = {
enable = true;
environment = {
WEBHOOK_URL = "https://wf.l.az-gruppe.com";
NODES_EXCLUDE = "[]";
N8N_RESTRICT_FILE_ACCESS_TO = "/mnt/DMS-INBOX;/mnt/DMS-ALT-INBOX";
N8N_RESTRICT_FILE_ACCESS_TO = builtins.concatStringsSep ";" sambaMounts;
N8N_RUNNERS_ENABLED = true;
N8N_NATIVE_PYTHON_RUNNER = true;
N8N_RUNNERS_AUTH_TOKEN_FILE = config.age.secrets.n8n-runner-auth-token.path;
N8N_LOG_OUTPUT = "json";
N8N_LOG_LEVEL = "info";
N8N_ENABLED_MODULES = "instance-ai";
N8N_INSTANCE_AI_SANDBOX_ENABLED = true;
N8N_INSTANCE_AI_SANDBOX_PROVIDER = "n8n-sandbox";
N8N_INSTANCE_AI_SANDBOX_IMAGE = "ghcr.io/n8n-io/n8n-sandbox-service-sandbox:latest";
N8N_INSTANCE_AI_SANDBOX_API_URL = "http://127.0.0.1:${toString sandboxApiPort}";
N8N_SANDBOX_SERVICE_URL = "http://127.0.0.1:${toString sandboxApiPort}";
};
taskRunners.enable = true;
};
systemd.services.n8n.serviceConfig = {
EnvironmentFile = ["${config.age.secrets.n8n-env.path}"];
systemd.services.n8n = {
serviceConfig = {
EnvironmentFile = ["${config.age.secrets.n8n-env.path}" "${config.age.secrets.n8n-sandbox-env.path}"];
ReadWritePaths = sambaMounts;
};
wants = ["podman-sandbox-api.service"];
after = ["podman-sandbox-api.service"];
};
systemd.services.n8n-task-runner.serviceConfig.ReadWritePaths = sambaMounts;
# Traefik configuration specific to n8n
services.traefik.dynamicConfigOptions.http = {
services.${serviceName}.loadBalancer.servers = [
+42 -1
View File
@@ -8,6 +8,15 @@
staticPort = config.m3ta.ports.get "netbox-static";
hostName = "nb.l.az-gruppe.com";
staticRoot = "${config.services.netbox.dataDir}/static";
# nixpkgs >= 25.11: Plugin-Pakete liegen im passthru-Set netbox.plugins
# (python3Packages.netbox-* sind Throw-Stubs); pluginName = NetBox-Modulname
netboxPlugins = with pkgs.unstable.netbox.plugins; [
netbox-dns
netbox-qrcode
netbox-routing
netbox-topology-views
netbox-floorplan-plugin
];
in {
services.netbox = {
enable = true;
@@ -22,13 +31,45 @@ in {
SECURE_SSL_REDIRECT = true;
SESSION_COOKIE_SECURE = true;
CSRF_COOKIE_SECURE = true;
# NetBox aktiviert Plugins erst über PLUGINS in configuration.py —
# der NixOS-NetBox-Modul trägt sie NICHT automatisch ein.
PLUGINS = map (p: p.pluginName) netboxPlugins;
};
# installiert die Pakete in NetBox' Python-Environment (extraBuildInputs)
plugins = _: netboxPlugins;
extraConfig = ''
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
'';
};
# Der Upgrade-Guard im NetBox-Modul (preStart) vergleicht /var/lib/netbox/version
# nur mit dem BASISPAKET (services.netbox.package). Reine Config-/Plugin-
# Änderungen bei gleicher NetBox-Version überspringen damit migrate +
# collectstatic → Plugin-Tabellen fehlen ("database migration missing").
# Deshalb Migrationen/Statics hier VOR jedem netbox.service-Start ausführen
# (idempotent, entspricht dem offiziellen NetBox-Upgrade-Pfad):
systemd.services.netbox = {
wants = ["netbox-migrate.service"];
after = ["netbox-migrate.service"];
};
systemd.services.netbox-migrate = {
description = "NetBox: DB-Migrationen & Statics (v. a. Plugins)";
wants = ["network-online.target"];
after = ["network-online.target" "postgresql.service" "redis-netbox.service"];
serviceConfig = {
Type = "oneshot";
User = "netbox";
Group = "netbox";
StateDirectory = "netbox";
TimeoutStartSec = "10min";
};
script = ''
/run/current-system/sw/bin/netbox-manage migrate --no-input
/run/current-system/sw/bin/netbox-manage collectstatic --no-input
'';
};
services.nginx = {
enable = true;
virtualHosts.netbox-static = {
+1
View File
@@ -37,6 +37,7 @@
litellm = 4000;
n8n = 5678;
n8n-sandbox-api = 5082;
netbox = 8001;
netbox-static = 8002;
kestra = 5080;