1{
2 config,
3 pkgs,
4 lib,
5 ...
6}:
7let
8 cfg = config.services.suricata;
9 pkg = cfg.package;
10 yaml = pkgs.formats.yaml { };
11 inherit (lib)
12 mkEnableOption
13 mkPackageOption
14 mkOption
15 types
16 literalExpression
17 filterAttrsRecursive
18 concatStringsSep
19 strings
20 lists
21 mkIf
22 ;
23in
24{
25 meta.maintainers = with lib.maintainers; [ felbinger ];
26
27 options.services.suricata = {
28 enable = mkEnableOption "Suricata";
29
30 package = mkPackageOption pkgs "suricata" { };
31
32 configFile = mkOption {
33 type = types.path;
34 visible = false;
35 default = pkgs.writeTextFile {
36 name = "suricata.yaml";
37 text = ''
38 %YAML 1.1
39 ---
40 ${builtins.readFile (
41 yaml.generate "suricata-settings-raw.yaml" (
42 filterAttrsRecursive (name: value: value != null) cfg.settings
43 )
44 )}
45 '';
46 };
47 description = ''
48 Configuration file for suricata.
49
50 It is not usual to override the default values; it is recommended to use `settings`.
51 If you want to include extra configuration to the file, use the `settings.includes`.
52 '';
53 };
54
55 settings = mkOption {
56 type = types.submodule (import ./settings.nix { inherit config lib yaml; });
57 example = literalExpression ''
58 vars.address-groups.HOME_NET = "192.168.178.0/24";
59 outputs = [
60 {
61 fast = {
62 enabled = true;
63 filename = "fast.log";
64 append = "yes";
65 };
66 }
67 {
68 eve-log = {
69 enabled = true;
70 filetype = "regular";
71 filename = "eve.json";
72 community-id = true;
73 types = [
74 {
75 alert.tagged-packets = "yes";
76 }
77 ];
78 };
79 }
80 ];
81 af-packet = [
82 {
83 interface = "eth0";
84 cluster-id = "99";
85 cluster-type = "cluster_flow";
86 defrag = "yes";
87 }
88 {
89 interface = "default";
90 }
91 ];
92 af-xdp = [
93 {
94 interface = "eth1";
95 }
96 ];
97 dpdk.interfaces = [
98 {
99 interface = "eth2";
100 }
101 ];
102 pcap = [
103 {
104 interface = "eth3";
105 }
106 ];
107 app-layer.protocols = {
108 telnet.enabled = "yes";
109 dnp3.enabled = "yes";
110 modbus.enabled = "yes";
111 };
112 '';
113 description = "Suricata settings";
114 };
115
116 enabledSources = mkOption {
117 type = types.listOf types.str;
118 # see: nix-shell -p suricata python3Packages.pyyaml --command 'suricata-update list-sources'
119 default = [
120 "et/open"
121 "etnetera/aggressive"
122 "stamus/lateral"
123 "oisf/trafficid"
124 "tgreen/hunting"
125 "sslbl/ja3-fingerprints"
126 "sslbl/ssl-fp-blacklist"
127 "malsilo/win-malware"
128 "pawpatrules"
129 ];
130 description = ''
131 List of sources that should be enabled.
132 Currently sources which require a secret-code are not supported.
133 '';
134 };
135
136 disabledRules = mkOption {
137 type = types.listOf types.str;
138 # protocol dnp3 seams to be disabled, which causes the signature evaluation to fail, so we disable the
139 # dnp3 rules, see https://github.com/OISF/suricata/blob/master/rules/dnp3-events.rules for more details
140 default = [
141 "2270000"
142 "2270001"
143 "2270002"
144 "2270003"
145 "2270004"
146 ];
147 description = ''
148 List of rules that should be disabled.
149 '';
150 };
151 };
152
153 config =
154 let
155 captureInterfaces =
156 let
157 inherit (lists) unique optionals;
158 in
159 unique (
160 map (e: e.interface) (
161 (optionals (cfg.settings.af-packet != null) cfg.settings.af-packet)
162 ++ (optionals (cfg.settings.af-xdp != null) cfg.settings.af-xdp)
163 ++ (optionals (
164 cfg.settings.dpdk != null && cfg.settings.dpdk.interfaces != null
165 ) cfg.settings.dpdk.interfaces)
166 ++ (optionals (cfg.settings.pcap != null) cfg.settings.pcap)
167 )
168 );
169 in
170 mkIf cfg.enable {
171 assertions = [
172 {
173 assertion = (builtins.length captureInterfaces) > 0;
174 message = ''
175 At least one capture interface must be configured:
176 - `services.suricata.settings.af-packet`
177 - `services.suricata.settings.af-xdp`
178 - `services.suricata.settings.dpdk.interfaces`
179 - `services.suricata.settings.pcap`
180 '';
181 }
182 ];
183
184 boot.kernelModules = mkIf (cfg.settings.af-packet != null) [ "af_packet" ];
185
186 users = {
187 groups.${cfg.settings.run-as.group} = { };
188 users.${cfg.settings.run-as.user} = {
189 group = cfg.settings.run-as.group;
190 isSystemUser = true;
191 };
192 };
193
194 systemd.tmpfiles.rules = [
195 "d ${cfg.settings."default-log-dir"} 755 ${cfg.settings.run-as.user} ${cfg.settings.run-as.group}"
196 "d /var/lib/suricata 755 ${cfg.settings.run-as.user} ${cfg.settings.run-as.group}"
197 "d ${cfg.settings."default-rule-path"} 755 ${cfg.settings.run-as.user} ${cfg.settings.run-as.group}"
198 ];
199
200 systemd.services = {
201 suricata-update = {
202 description = "Update Suricata Rules";
203 wantedBy = [ "multi-user.target" ];
204 wants = [ "network-online.target" ];
205 after = [ "network-online.target" ];
206
207 script =
208 let
209 python = pkgs.python3.withPackages (ps: with ps; [ pyyaml ]);
210 enabledSourcesCmds = map (
211 src: "${python.interpreter} ${pkg}/bin/suricata-update enable-source ${src}"
212 ) cfg.enabledSources;
213 in
214 ''
215 ${concatStringsSep "\n" enabledSourcesCmds}
216 ${python.interpreter} ${pkg}/bin/suricata-update update-sources
217 ${python.interpreter} ${pkg}/bin/suricata-update update --suricata-conf ${cfg.configFile} --no-test \
218 --disable-conf ${pkgs.writeText "suricata-disable-conf" "${concatStringsSep "\n" cfg.disabledRules}"}
219 '';
220 serviceConfig = {
221 Type = "oneshot";
222
223 PrivateTmp = true;
224 PrivateDevices = true;
225 PrivateIPC = true;
226
227 DynamicUser = true;
228 User = cfg.settings.run-as.user;
229 Group = cfg.settings.run-as.group;
230
231 ReadOnlyPaths = cfg.configFile;
232 ReadWritePaths = [
233 "/var/lib/suricata"
234 cfg.settings."default-rule-path"
235 ];
236 };
237 };
238 suricata = {
239 description = "Suricata";
240 wantedBy = [ "multi-user.target" ];
241 after = [ "suricata-update.service" ];
242 serviceConfig =
243 let
244 interfaceOptions = strings.concatMapStrings (interface: " -i ${interface}") captureInterfaces;
245 in
246 {
247 ExecStartPre = "!${pkg}/bin/suricata -c ${cfg.configFile} -T";
248 ExecStart = "!${pkg}/bin/suricata -c ${cfg.configFile}${interfaceOptions}";
249 Restart = "on-failure";
250
251 User = cfg.settings.run-as.user;
252 Group = cfg.settings.run-as.group;
253
254 NoNewPrivileges = true;
255 PrivateTmp = true;
256 PrivateDevices = true;
257 PrivateIPC = true;
258 ProtectSystem = "strict";
259 DevicePolicy = "closed";
260 LockPersonality = true;
261 MemoryDenyWriteExecute = true;
262 ProtectHostname = true;
263 ProtectProc = true;
264 ProtectKernelLogs = true;
265 ProtectKernelModules = true;
266 ProtectKernelTunables = true;
267 ProtectControlGroups = true;
268 ProcSubset = "pid";
269 RestrictNamespaces = true;
270 RestrictRealtime = true;
271 RestrictSUIDSGID = true;
272 SystemCallArchitectures = "native";
273 RemoveIPC = true;
274
275 ReadOnlyPaths = cfg.configFile;
276 ReadWritePaths = cfg.settings."default-log-dir";
277 RuntimeDirectory = "suricata";
278 };
279 };
280 };
281 };
282}