Loading nixos/modules/services/misc/n8n.nix +34 −3 Original line number Diff line number Diff line Loading @@ -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 = [ Loading @@ -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 = Loading Loading @@ -92,15 +116,22 @@ in description = "n8n service"; after = [ "network.target" ]; wantedBy = [ "multi-user.target" ]; environment = cfg.environment // { 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"; Loading nixos/tests/n8n.nix +10 −1 Original line number Diff line number Diff line { lib, ... }: { lib, pkgs, ... }: let port = 5678; webhookUrl = "http://example.com"; secretFile = toString (pkgs.writeText "n8n-encryption-key" "test-encryption-key-12345"); in { name = "n8n"; Loading @@ -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; }; }; }; Loading @@ -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") Loading @@ -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") ''; } Loading
nixos/modules/services/misc/n8n.nix +34 −3 Original line number Diff line number Diff line Loading @@ -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 = [ Loading @@ -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 = Loading Loading @@ -92,15 +116,22 @@ in description = "n8n service"; after = [ "network.target" ]; wantedBy = [ "multi-user.target" ]; environment = cfg.environment // { 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"; Loading
nixos/tests/n8n.nix +10 −1 Original line number Diff line number Diff line { lib, ... }: { lib, pkgs, ... }: let port = 5678; webhookUrl = "http://example.com"; secretFile = toString (pkgs.writeText "n8n-encryption-key" "test-encryption-key-12345"); in { name = "n8n"; Loading @@ -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; }; }; }; Loading @@ -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") Loading @@ -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") ''; }