From 6ef54c585b8a2d61b4b54784e50031ddaff14ecf Mon Sep 17 00:00:00 2001 From: sweenu Date: Tue, 11 Nov 2025 11:02:38 +0100 Subject: [PATCH] nixos/n8n: handle _FILE env variables with systemd's LoadCredential --- nixos/modules/services/misc/n8n.nix | 37 ++++++++++++++++++++++++++--- nixos/tests/n8n.nix | 11 ++++++++- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/nixos/modules/services/misc/n8n.nix b/nixos/modules/services/misc/n8n.nix index 1b66a4c7705b..1c2e38717c2b 100644 --- a/nixos/modules/services/misc/n8n.nix +++ b/nixos/modules/services/misc/n8n.nix @@ -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 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"; diff --git a/nixos/tests/n8n.nix b/nixos/tests/n8n.nix index 7936500a4d53..490f9cdd17c0 100644 --- a/nixos/tests/n8n.nix +++ b/nixos/tests/n8n.nix @@ -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") ''; }