Unverified Commit 5e931b16 authored by Gergő Gutyina's avatar Gergő Gutyina Committed by GitHub
Browse files

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

parents 95ece6cd 6ef54c58
Loading
Loading
Loading
Loading
+34 −3
Original line number Diff line number Diff line
@@ -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 // {
      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
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";
@@ -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")
  '';
}