1{ config, pkgs, lib, ... }:
2
3with lib;
4
5let
6 cfg = config.services.n8n;
7 format = pkgs.formats.json {};
8 configFile = format.generate "n8n.json" cfg.settings;
9in
10{
11 options.services.n8n = {
12 enable = mkEnableOption "n8n server";
13
14 openFirewall = mkOption {
15 type = types.bool;
16 default = false;
17 description = "Open ports in the firewall for the n8n web interface.";
18 };
19
20 settings = mkOption {
21 type = format.type;
22 default = {};
23 description = ''
24 Configuration for n8n, see <https://docs.n8n.io/hosting/environment-variables/configuration-methods/>
25 for supported values.
26 '';
27 };
28
29 webhookUrl = mkOption {
30 type = types.str;
31 default = "";
32 description = ''
33 WEBHOOK_URL for n8n, in case we're running behind a reverse proxy.
34 This cannot be set through configuration and must reside in an environment variable.
35 '';
36 };
37
38 };
39
40 config = mkIf cfg.enable {
41 services.n8n.settings = {
42 # We use this to open the firewall, so we need to know about the default at eval time
43 port = lib.mkDefault 5678;
44 };
45
46 systemd.services.n8n = {
47 description = "N8N service";
48 after = [ "network.target" ];
49 wantedBy = [ "multi-user.target" ];
50 environment = {
51 # This folder must be writeable as the application is storing
52 # its data in it, so the StateDirectory is a good choice
53 N8N_USER_FOLDER = "/var/lib/n8n";
54 HOME = "/var/lib/n8n";
55 N8N_CONFIG_FILES = "${configFile}";
56 WEBHOOK_URL = "${cfg.webhookUrl}";
57
58 # Don't phone home
59 N8N_DIAGNOSTICS_ENABLED = "false";
60 N8N_VERSION_NOTIFICATIONS_ENABLED = "false";
61 };
62 serviceConfig = {
63 Type = "simple";
64 ExecStart = "${pkgs.n8n}/bin/n8n";
65 Restart = "on-failure";
66 StateDirectory = "n8n";
67
68 # Basic Hardening
69 NoNewPrivileges = "yes";
70 PrivateTmp = "yes";
71 PrivateDevices = "yes";
72 DevicePolicy = "closed";
73 DynamicUser = "true";
74 ProtectSystem = "strict";
75 ProtectHome = "read-only";
76 ProtectControlGroups = "yes";
77 ProtectKernelModules = "yes";
78 ProtectKernelTunables = "yes";
79 RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
80 RestrictNamespaces = "yes";
81 RestrictRealtime = "yes";
82 RestrictSUIDSGID = "yes";
83 MemoryDenyWriteExecute = "no"; # v8 JIT requires memory segments to be Writable-Executable.
84 LockPersonality = "yes";
85 };
86 };
87
88 networking.firewall = mkIf cfg.openFirewall {
89 allowedTCPPorts = [ cfg.settings.port ];
90 };
91 };
92}