caddy: `withPlugins` better error messages for untagged plugins (#368657)
[NixPkgs.git] / nixos / modules / services / networking / suricata / default.nix
blob5473fc913ebc6f549560a4b407ac7f4a7d637a9a
2   config,
3   pkgs,
4   lib,
5   ...
6 }:
7 let
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     ;
25   meta.maintainers = with lib.maintainers; [ felbinger ];
27   options.services.suricata = {
28     enable = mkEnableOption "Suricata";
30     package = mkPackageOption pkgs "suricata" { };
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.
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     };
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     };
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     };
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   };
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       ];
184       boot.kernelModules = mkIf (cfg.settings.af-packet != null) [ "af_packet" ];
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       };
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       ];
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" ];
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";
223             PrivateTmp = true;
224             PrivateDevices = true;
225             PrivateIPC = true;
227             DynamicUser = true;
228             User = cfg.settings.run-as.user;
229             Group = cfg.settings.run-as.group;
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";
251               User = cfg.settings.run-as.user;
252               Group = cfg.settings.run-as.group;
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;
275               ReadOnlyPaths = cfg.configFile;
276               ReadWritePaths = cfg.settings."default-log-dir";
277               RuntimeDirectory = "suricata";
278             };
279         };
280       };
281     };