nixos/n8n: handle _FILE env variables with systemd's LoadCredential (#460626)

This commit is contained in:
Gergő Gutyina
2026-01-02 17:41:57 +00:00
committed by GitHub
2 changed files with 44 additions and 4 deletions
+34 -3
View File
@@ -6,6 +6,18 @@
}:
let
cfg = config.services.n8n;
envVarToCredName = varName: lib.toLower varName;
# Partition environment variables into regular and file-based (_FILE suffix)
regularEnv = lib.filterAttrs (name: _value: !(lib.hasSuffix "_FILE" name)) cfg.environment;
fileBasedEnv = lib.filterAttrs (name: _value: lib.hasSuffix "_FILE" name) cfg.environment;
# Transform file-based env vars to point to credentials directory
fileBasedEnvTransformed = lib.mapAttrs' (
varName: _secretPath: lib.nameValuePair varName "%d/${envVarToCredName varName}"
) fileBasedEnv;
in
{
imports = [
@@ -32,6 +44,18 @@ in
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 =
@@ -92,15 +116,22 @@ in
description = "n8n service";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
environment = cfg.environment // {
HOME = config.services.n8n.environment.N8N_USER_FOLDER;
};
environment =
regularEnv
// {
HOME = config.services.n8n.environment.N8N_USER_FOLDER;
}
// fileBasedEnvTransformed;
serviceConfig = {
Type = "simple";
ExecStart = lib.getExe cfg.package;
Restart = "on-failure";
StateDirectory = "n8n";
LoadCredential = lib.mapAttrsToList (
varName: secretPath: "${envVarToCredName varName}:${secretPath}"
) fileBasedEnv;
# Basic Hardening
NoNewPrivileges = "yes";
PrivateTmp = "yes";
+10 -1
View File
@@ -1,7 +1,8 @@
{ lib, ... }:
{ lib, pkgs, ... }:
let
port = 5678;
webhookUrl = "http://example.com";
secretFile = toString (pkgs.writeText "n8n-encryption-key" "test-encryption-key-12345");
in
{
name = "n8n";
@@ -18,6 +19,8 @@ in
WEBHOOK_URL = webhookUrl;
N8N_TEMPLATES_ENABLED = false;
DB_PING_INTERVAL_SECONDS = 2;
# !!! Don't do this with real keys. The /nix store is world-readable!
N8N_ENCRYPTION_KEY_FILE = secretFile;
};
};
};
@@ -25,6 +28,8 @@ in
testScript = ''
machine.wait_for_unit("n8n.service")
machine.wait_for_console_text("Editor is now accessible via")
# Test regular environment variables
machine.succeed("curl --fail -vvv http://localhost:${toString port}/")
machine.succeed("grep -qF ${webhookUrl} /etc/systemd/system/n8n.service")
machine.succeed("grep -qF 'HOME=/var/lib/n8n' /etc/systemd/system/n8n.service")
@@ -32,5 +37,9 @@ in
machine.succeed("grep -qF 'N8N_DIAGNOSTICS_ENABLED=false' /etc/systemd/system/n8n.service")
machine.succeed("grep -qF 'N8N_TEMPLATES_ENABLED=false' /etc/systemd/system/n8n.service")
machine.succeed("grep -qF 'DB_PING_INTERVAL_SECONDS=2' /etc/systemd/system/n8n.service")
# Test _FILE environment variables
machine.succeed("grep -qF 'LoadCredential=n8n_encryption_key_file:${secretFile}' /etc/systemd/system/n8n.service")
machine.succeed("grep -qF 'N8N_ENCRYPTION_KEY_FILE=%d/n8n_encryption_key_file' /etc/systemd/system/n8n.service")
'';
}