1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8with lib;
9
10let
11
12 cfg = config.services.nats;
13
14 format = pkgs.formats.json { };
15
16 validateConfig =
17 file:
18 pkgs.callPackage (
19 { runCommand, nats-server }:
20 runCommand "validate-nats-conf"
21 {
22 nativeBuildInputs = [ nats-server ];
23 }
24 ''
25 nats-server --config "${file}" -t
26 ln -s "${file}" "$out"
27 ''
28 ) { };
29
30 unvalidatedConfigFile = format.generate "nats.conf" cfg.settings;
31
32 configFile =
33 if cfg.validateConfig then validateConfig unvalidatedConfigFile else unvalidatedConfigFile;
34in
35{
36
37 ### Interface
38
39 options = {
40 services.nats = {
41 enable = mkEnableOption "NATS messaging system";
42
43 user = mkOption {
44 type = types.str;
45 default = "nats";
46 description = "User account under which NATS runs.";
47 };
48
49 group = mkOption {
50 type = types.str;
51 default = "nats";
52 description = "Group under which NATS runs.";
53 };
54
55 serverName = mkOption {
56 default = "nats";
57 example = "n1-c3";
58 type = types.str;
59 description = ''
60 Name of the NATS server, must be unique if clustered.
61 '';
62 };
63
64 jetstream = mkEnableOption "JetStream";
65
66 port = mkOption {
67 default = 4222;
68 type = types.port;
69 description = ''
70 Port on which to listen.
71 '';
72 };
73
74 dataDir = mkOption {
75 default = "/var/lib/nats";
76 type = types.path;
77 description = ''
78 The NATS data directory. Only used if JetStream is enabled, for
79 storing stream metadata and messages.
80
81 If left as the default value this directory will automatically be
82 created before the NATS server starts, otherwise the sysadmin is
83 responsible for ensuring the directory exists with appropriate
84 ownership and permissions.
85 '';
86 };
87
88 settings = mkOption {
89 default = { };
90 type = format.type;
91 example = literalExpression ''
92 {
93 jetstream = {
94 max_mem = "1G";
95 max_file = "10G";
96 };
97 };
98 '';
99 description = ''
100 Declarative NATS configuration. See the
101 [
102 NATS documentation](https://docs.nats.io/nats-server/configuration) for a list of options.
103 '';
104 };
105
106 validateConfig = mkOption {
107 type = types.bool;
108 default = true;
109 description = ''
110 If true, validate nats config at build time. When the config can't
111 be checked during build time, for example when it includes other
112 files, disable this option.
113 '';
114 };
115 };
116 };
117
118 ### Implementation
119
120 config = mkIf cfg.enable {
121 services.nats.settings = {
122 server_name = cfg.serverName;
123 port = cfg.port;
124 jetstream = optionalAttrs cfg.jetstream { store_dir = cfg.dataDir; };
125 };
126
127 systemd.services.nats = {
128 description = "NATS messaging system";
129 wantedBy = [ "multi-user.target" ];
130 after = [ "network.target" ];
131
132 serviceConfig = mkMerge [
133 (mkIf (cfg.dataDir == "/var/lib/nats") {
134 StateDirectory = "nats";
135 StateDirectoryMode = "0750";
136 })
137 {
138 Type = "simple";
139 ExecStart = "${pkgs.nats-server}/bin/nats-server -c ${configFile}";
140 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
141 ExecStop = "${pkgs.coreutils}/bin/kill -SIGINT $MAINPID";
142 Restart = "on-failure";
143
144 User = cfg.user;
145 Group = cfg.group;
146
147 # Hardening
148 CapabilityBoundingSet = "";
149 LimitNOFILE = 800000; # JetStream requires 2 FDs open per stream.
150 LockPersonality = true;
151 MemoryDenyWriteExecute = true;
152 NoNewPrivileges = true;
153 PrivateDevices = true;
154 PrivateTmp = true;
155 PrivateUsers = true;
156 ProcSubset = "pid";
157 ProtectClock = true;
158 ProtectControlGroups = true;
159 ProtectHome = true;
160 ProtectHostname = true;
161 ProtectKernelLogs = true;
162 ProtectKernelModules = true;
163 ProtectKernelTunables = true;
164 ProtectProc = "invisible";
165 ProtectSystem = "strict";
166 ReadOnlyPaths = [ ];
167 ReadWritePaths = [ cfg.dataDir ];
168 RestrictAddressFamilies = [
169 "AF_INET"
170 "AF_INET6"
171 ];
172 RestrictNamespaces = true;
173 RestrictRealtime = true;
174 RestrictSUIDSGID = true;
175 SystemCallFilter = [
176 "@system-service"
177 "~@privileged"
178 ];
179 UMask = "0077";
180 }
181 ];
182 };
183
184 users.users = mkIf (cfg.user == "nats") {
185 nats = {
186 description = "NATS daemon user";
187 isSystemUser = true;
188 group = cfg.group;
189 home = cfg.dataDir;
190 };
191 };
192
193 users.groups = mkIf (cfg.group == "nats") { nats = { }; };
194 };
195
196}