438 lines
16 KiB
Nix
438 lines
16 KiB
Nix
{
|
|
config,
|
|
pkgs,
|
|
lib,
|
|
...
|
|
}:
|
|
let
|
|
cfg = config.services.n8n;
|
|
|
|
# Partition environment variables into regular and file-based (_FILE suffix)
|
|
envVarToCredName = varName: lib.toLower varName;
|
|
partitionEnv =
|
|
allEnv:
|
|
let
|
|
env = lib.filterAttrs (_name: value: value != null) allEnv;
|
|
regular = lib.filterAttrs (name: _value: !(lib.hasSuffix "_FILE" name)) env;
|
|
fileBased = lib.filterAttrs (name: _value: lib.hasSuffix "_FILE" name) env;
|
|
fileBasedTransformed = lib.mapAttrs' (
|
|
varName: _secretPath: lib.nameValuePair varName "%d/${envVarToCredName varName}"
|
|
) fileBased;
|
|
in
|
|
{
|
|
inherit regular fileBased fileBasedTransformed;
|
|
};
|
|
|
|
n8nEnv = partitionEnv cfg.environment;
|
|
|
|
customNodesDir = pkgs.linkFarm "n8n-custom-nodes" (
|
|
map (pkg: {
|
|
name = pkg.pname;
|
|
path = "${pkg}/lib/node_modules/${pkg.pname}";
|
|
}) cfg.customNodes
|
|
);
|
|
|
|
# Runners
|
|
runnersCfg = cfg.taskRunners;
|
|
runnersEnv = partitionEnv runnersCfg.environment;
|
|
commonAllowedEnv = lib.attrNames runnersEnv.regular;
|
|
enabledRunners = lib.filterAttrs (_name: runnerCfg: runnerCfg.enable) runnersCfg.runners;
|
|
anyRunnerEnabled = runnersCfg.enable && (enabledRunners != { });
|
|
runnerTypes = lib.attrNames enabledRunners;
|
|
runnersStateDir = "n8n-task-runners";
|
|
taskRunnerConfigs = lib.mapAttrsToList (runnerType: runnerCfg: {
|
|
runner-type = runnerType;
|
|
workdir = "/var/lib/${runnersStateDir}";
|
|
command = runnerCfg.command;
|
|
args = runnerCfg.args;
|
|
allowed-env = commonAllowedEnv;
|
|
env-overrides = runnerCfg.environment;
|
|
health-check-server-port = toString runnerCfg.healthCheckPort;
|
|
}) enabledRunners;
|
|
|
|
launcherConfigFile = pkgs.writeText "n8n-task-runners.json" (
|
|
builtins.toJSON { task-runners = taskRunnerConfigs; }
|
|
);
|
|
in
|
|
{
|
|
meta.maintainers = with lib.maintainers; [
|
|
sweenu
|
|
gepbird
|
|
];
|
|
|
|
imports = [
|
|
(lib.mkRemovedOptionModule [ "services" "n8n" "settings" ] "Use services.n8n.environment instead.")
|
|
(lib.mkRemovedOptionModule [
|
|
"services"
|
|
"n8n"
|
|
"webhookUrl"
|
|
] "Use services.n8n.environment.WEBHOOK_URL instead.")
|
|
];
|
|
|
|
options.services.n8n = {
|
|
enable = lib.mkEnableOption "n8n server";
|
|
|
|
package = lib.mkPackageOption pkgs "n8n" { };
|
|
|
|
customNodes = lib.mkOption {
|
|
type = lib.types.listOf lib.types.package;
|
|
default = [ ];
|
|
example = lib.literalExpression "[ pkgs.n8n-nodes-carbonejs ]";
|
|
description = ''
|
|
List of custom n8n community node packages to load.
|
|
Each package is expected to be an npm package with an `n8n.nodes` entry in its `package.json`.
|
|
The packages are made available to n8n via the `N8N_CUSTOM_EXTENSIONS` environment variable.
|
|
'';
|
|
};
|
|
|
|
openFirewall = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Open ports in the firewall for the n8n web interface.";
|
|
};
|
|
|
|
environment = lib.mkOption {
|
|
description = ''
|
|
Environment variables to pass to the n8n service.
|
|
See <https://docs.n8n.io/hosting/configuration/environment-variables/> for available options.
|
|
|
|
Environment variables ending with `_FILE` are automatically handled as secrets:
|
|
they are loaded via systemd credentials for secure access with `DynamicUser=true`.
|
|
|
|
This can be useful to pass secrets via tools like `agenix` or `sops-nix`.
|
|
'';
|
|
example = lib.literalExpression ''
|
|
{
|
|
N8N_ENCRYPTION_KEY_FILE = "/run/n8n/encryption_key";
|
|
DB_POSTGRESDB_PASSWORD_FILE = "/run/n8n/db_postgresdb_password";
|
|
WEBHOOK_URL = "https://n8n.example.com";
|
|
}
|
|
'';
|
|
type = lib.types.submodule {
|
|
freeformType =
|
|
with lib.types;
|
|
attrsOf (oneOf [
|
|
str
|
|
(coercedTo int toString str)
|
|
(coercedTo bool builtins.toJSON str)
|
|
]);
|
|
options = {
|
|
GENERIC_TIMEZONE = lib.mkOption {
|
|
type = with lib.types; nullOr str;
|
|
default = config.time.timeZone;
|
|
defaultText = lib.literalExpression "config.time.timeZone";
|
|
description = ''
|
|
The n8n instance timezone. Important for schedule nodes (such as Cron).
|
|
'';
|
|
};
|
|
N8N_PORT = lib.mkOption {
|
|
type = with lib.types; coercedTo port toString str;
|
|
default = 5678;
|
|
description = "The HTTP port n8n runs on.";
|
|
};
|
|
N8N_USER_FOLDER = lib.mkOption {
|
|
type = lib.types.path;
|
|
# This folder must be writeable as the application is storing
|
|
# its data in it, so the StateDirectory is a good choice
|
|
default = "/var/lib/n8n";
|
|
description = ''
|
|
Provide the path where n8n will create the .n8n folder.
|
|
This directory stores user-specific data, such as database file and encryption key.
|
|
'';
|
|
readOnly = true;
|
|
};
|
|
N8N_DIAGNOSTICS_ENABLED = lib.mkOption {
|
|
type = with lib.types; coercedTo bool builtins.toJSON str;
|
|
default = false;
|
|
description = ''
|
|
Whether to share selected, anonymous telemetry with n8n.
|
|
Note that if you set this to false, you can't enable Ask AI in the Code node.
|
|
'';
|
|
};
|
|
N8N_VERSION_NOTIFICATIONS_ENABLED = lib.mkOption {
|
|
type = with lib.types; coercedTo bool builtins.toJSON str;
|
|
default = false;
|
|
description = ''
|
|
When enabled, n8n sends notifications of new versions and security updates.
|
|
'';
|
|
};
|
|
N8N_CUSTOM_EXTENSIONS = lib.mkOption {
|
|
internal = true;
|
|
type = with lib.types; nullOr path;
|
|
default = if cfg.customNodes != [ ] then toString customNodesDir else null;
|
|
description = ''
|
|
Specify the path to directories containing your custom nodes.
|
|
'';
|
|
};
|
|
N8N_RUNNERS_MODE = lib.mkOption {
|
|
internal = true;
|
|
type =
|
|
with lib.types;
|
|
enum [
|
|
"internal"
|
|
"external"
|
|
];
|
|
default = if runnersCfg.enable then "external" else "internal";
|
|
description = ''
|
|
How to launch and run the task runner.
|
|
`internal` means n8n will launch a task runner as child process.
|
|
`external` means an external orchestrator will launch the task runner.
|
|
'';
|
|
};
|
|
N8N_RUNNERS_BROKER_PORT = lib.mkOption {
|
|
type = with lib.types; coercedTo port toString str;
|
|
default = 5679;
|
|
description = ''
|
|
Port the task broker listens on for task runner connections.
|
|
'';
|
|
};
|
|
N8N_RUNNERS_BROKER_LISTEN_ADDRESS = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "127.0.0.1";
|
|
description = ''
|
|
Address the task broker listens on.
|
|
'';
|
|
};
|
|
N8N_RUNNERS_AUTH_TOKEN_FILE = lib.mkOption {
|
|
type = with lib.types; nullOr path;
|
|
default = null;
|
|
description = ''
|
|
Path to a file containing the shared authentication token
|
|
used between the n8n server (task broker) and the task runners.
|
|
|
|
This option is required when {option}`services.n8n.taskRunners.enable` is true.
|
|
The file should be readable by the service and not stored in the Nix store.
|
|
Use tools like `agenix` or `sops-nix` to manage this secret.
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
default = { };
|
|
};
|
|
|
|
taskRunners = {
|
|
enable = lib.mkEnableOption "n8n task runners for sandboxed Code node execution";
|
|
|
|
launcherPackage = lib.mkPackageOption pkgs "n8n-task-runner-launcher" { };
|
|
|
|
environment = lib.mkOption {
|
|
description = ''
|
|
Environment variables for the task runner launcher and runners.
|
|
These are common to all runners and passed via `allowed-env` in the launcher config.
|
|
See <https://docs.n8n.io/hosting/configuration/environment-variables/task-runners/> for available options.
|
|
|
|
Environment variables ending with `_FILE` are automatically handled as secrets:
|
|
they are loaded via systemd credentials for secure access with `DynamicUser=true`.
|
|
|
|
Note: The authentication token should be set via {option}`services.n8n.environment.N8N_RUNNERS_AUTH_TOKEN_FILE`.
|
|
'';
|
|
example = lib.literalExpression ''
|
|
{
|
|
N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT = 15;
|
|
N8N_RUNNERS_MAX_CONCURRENCY = 10;
|
|
}
|
|
'';
|
|
type = lib.types.submodule {
|
|
freeformType =
|
|
with lib.types;
|
|
attrsOf (oneOf [
|
|
str
|
|
(coercedTo int toString str)
|
|
(coercedTo bool builtins.toJSON str)
|
|
]);
|
|
options = {
|
|
N8N_RUNNERS_AUTH_TOKEN_FILE = lib.mkOption {
|
|
type = with lib.types; nullOr path;
|
|
default = cfg.environment.N8N_RUNNERS_AUTH_TOKEN_FILE;
|
|
defaultText = lib.literalExpression "config.services.n8n.environment.N8N_RUNNERS_AUTH_TOKEN_FILE";
|
|
description = ''
|
|
Path to the authentication token file for the task runner.
|
|
'';
|
|
};
|
|
N8N_RUNNERS_TASK_BROKER_URI = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "http://${cfg.environment.N8N_RUNNERS_BROKER_LISTEN_ADDRESS}:${cfg.environment.N8N_RUNNERS_BROKER_PORT}";
|
|
defaultText = lib.literalExpression ''"http://''${config.services.n8n.environment.N8N_RUNNERS_BROKER_LISTEN_ADDRESS}:''${config.services.n8n.environment.N8N_RUNNERS_BROKER_PORT}"'';
|
|
description = ''
|
|
URI of the n8n task broker that the runner connects to.
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
default = { };
|
|
};
|
|
|
|
runners = lib.mkOption {
|
|
type = lib.types.attrsOf (
|
|
lib.types.submodule (
|
|
{ name, ... }:
|
|
{
|
|
options = {
|
|
enable = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = ''
|
|
Whether to enable the ${name} task runner.
|
|
Only takes effect when {option}`services.n8n.taskRunners.enable` is true.
|
|
'';
|
|
};
|
|
|
|
command = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "Command to execute for this runner.";
|
|
};
|
|
|
|
healthCheckPort = lib.mkOption {
|
|
type = lib.types.port;
|
|
description = "Port for the runner's health check server.";
|
|
};
|
|
|
|
args = lib.mkOption {
|
|
type = with lib.types; listOf str;
|
|
default = [ ];
|
|
description = "Additional command-line arguments to pass to the task runner.";
|
|
};
|
|
|
|
environment = lib.mkOption {
|
|
type = with lib.types; attrsOf str;
|
|
default = { };
|
|
description = "Environment variables specific to this task runner.";
|
|
};
|
|
};
|
|
}
|
|
)
|
|
);
|
|
default = { };
|
|
defaultText = lib.literalExpression ''
|
|
{
|
|
javascript = {
|
|
enable = true;
|
|
command = lib.getExe' config.services.n8n.package "n8n-task-runner";
|
|
healthCheckPort = 5681;
|
|
};
|
|
python = {
|
|
enable = true;
|
|
command = lib.getExe' config.services.n8n.package "n8n-task-runner-python";
|
|
healthCheckPort = 5682;
|
|
};
|
|
}
|
|
'';
|
|
description = "Configuration for individual task runners.";
|
|
};
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
assertions = [
|
|
{
|
|
assertion = anyRunnerEnabled -> cfg.environment.N8N_RUNNERS_AUTH_TOKEN_FILE != null;
|
|
message = "services.n8n.environment.N8N_RUNNERS_AUTH_TOKEN_FILE must be set when task runners are enabled.";
|
|
}
|
|
];
|
|
|
|
systemd.services.n8n = {
|
|
description = "n8n service";
|
|
after = [ "network.target" ];
|
|
wantedBy = [ "multi-user.target" ];
|
|
environment =
|
|
n8nEnv.regular
|
|
// {
|
|
HOME = cfg.environment.N8N_USER_FOLDER;
|
|
}
|
|
// n8nEnv.fileBasedTransformed;
|
|
serviceConfig = {
|
|
Type = "simple";
|
|
ExecStart = lib.getExe cfg.package;
|
|
Restart = "on-failure";
|
|
StateDirectory = baseNameOf cfg.environment.N8N_USER_FOLDER;
|
|
|
|
LoadCredential = lib.mapAttrsToList (
|
|
varName: secretPath: "${envVarToCredName varName}:${secretPath}"
|
|
) n8nEnv.fileBased;
|
|
|
|
# Basic Hardening
|
|
NoNewPrivileges = "yes";
|
|
PrivateTmp = "yes";
|
|
PrivateDevices = "yes";
|
|
DevicePolicy = "closed";
|
|
DynamicUser = "true";
|
|
ProtectSystem = "strict";
|
|
ProtectHome = "read-only";
|
|
ProtectControlGroups = "yes";
|
|
ProtectKernelModules = "yes";
|
|
ProtectKernelTunables = "yes";
|
|
RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
|
|
RestrictNamespaces = "yes";
|
|
RestrictRealtime = "yes";
|
|
RestrictSUIDSGID = "yes";
|
|
MemoryDenyWriteExecute = "no"; # v8 JIT requires memory segments to be Writable-Executable.
|
|
LockPersonality = "yes";
|
|
};
|
|
};
|
|
|
|
warnings = lib.optional (runnersCfg.enable && !anyRunnerEnabled) ''
|
|
services.n8n.taskRunners.enable is true, but both JavaScript and Python runners are disabled.
|
|
Enable at least one runner or disable taskRunners.
|
|
'';
|
|
|
|
# We set the defaults here to ease adding attributes
|
|
services.n8n.taskRunners.runners = {
|
|
javascript = lib.mapAttrs (_: lib.mkDefault) {
|
|
enable = true;
|
|
command = lib.getExe' cfg.package "n8n-task-runner";
|
|
healthCheckPort = 5681;
|
|
};
|
|
python = lib.mapAttrs (_: lib.mkDefault) {
|
|
enable = true;
|
|
command = lib.getExe' cfg.package "n8n-task-runner-python";
|
|
healthCheckPort = 5682;
|
|
};
|
|
};
|
|
|
|
systemd.services.n8n-task-runner = lib.mkIf anyRunnerEnabled {
|
|
description = "n8n task runner";
|
|
after = [ "n8n.service" ];
|
|
requires = [ "n8n.service" ];
|
|
wantedBy = [ "multi-user.target" ];
|
|
environment = {
|
|
N8N_RUNNERS_CONFIG_PATH = launcherConfigFile;
|
|
}
|
|
// runnersEnv.regular
|
|
// runnersEnv.fileBasedTransformed;
|
|
serviceConfig = {
|
|
Type = "simple";
|
|
ExecStart = "${lib.getExe runnersCfg.launcherPackage} ${lib.concatStringsSep " " runnerTypes}";
|
|
Restart = "on-failure";
|
|
|
|
StateDirectory = runnersStateDir;
|
|
|
|
LoadCredential = lib.mapAttrsToList (
|
|
varName: secretPath: "${envVarToCredName varName}:${secretPath}"
|
|
) runnersEnv.fileBased;
|
|
|
|
# Hardening
|
|
DynamicUser = "true";
|
|
NoNewPrivileges = "yes";
|
|
PrivateTmp = "yes";
|
|
PrivateDevices = "yes";
|
|
DevicePolicy = "closed";
|
|
ProtectSystem = "strict";
|
|
ProtectHome = "read-only";
|
|
ProtectControlGroups = "yes";
|
|
ProtectKernelModules = "yes";
|
|
ProtectKernelTunables = "yes";
|
|
RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
|
|
RestrictNamespaces = "yes";
|
|
RestrictRealtime = "yes";
|
|
RestrictSUIDSGID = "yes";
|
|
MemoryDenyWriteExecute = "no"; # v8 JIT requires memory segments to be Writable-Executable.
|
|
LockPersonality = "yes";
|
|
};
|
|
};
|
|
|
|
networking.firewall = lib.mkIf cfg.openFirewall {
|
|
allowedTCPPorts = [ (lib.toInt cfg.environment.N8N_PORT) ];
|
|
};
|
|
};
|
|
}
|