nixpkgs/nixos/modules/services/networking/suricata/default.nix
2024-10-03 22:50:30 +02:00

283 lines
8.2 KiB
Nix

{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.suricata;
pkg = cfg.package;
yaml = pkgs.formats.yaml { };
inherit (lib)
mkEnableOption
mkPackageOption
mkOption
types
literalExpression
filterAttrsRecursive
concatStringsSep
strings
lists
mkIf
;
in
{
meta.maintainers = with lib.maintainers; [ felbinger ];
options.services.suricata = {
enable = mkEnableOption "Suricata";
package = mkPackageOption pkgs "suricata" { };
configFile = mkOption {
type = types.path;
visible = false;
default = pkgs.writeTextFile {
name = "suricata.yaml";
text = ''
%YAML 1.1
---
${builtins.readFile (
yaml.generate "suricata-settings-raw.yaml" (
filterAttrsRecursive (name: value: value != null) cfg.settings
)
)}
'';
};
description = ''
Configuration file for suricata.
It is not usual to override the default values; it is recommended to use `settings`.
If you want to include extra configuration to the file, use the `settings.includes`.
'';
};
settings = mkOption {
type = types.submodule (import ./settings.nix { inherit config lib yaml; });
example = literalExpression ''
vars.address-groups.HOME_NET = "192.168.178.0/24";
outputs = [
{
fast = {
enabled = true;
filename = "fast.log";
append = "yes";
};
}
{
eve-log = {
enabled = true;
filetype = "regular";
filename = "eve.json";
community-id = true;
types = [
{
alert.tagged-packets = "yes";
}
];
};
}
];
af-packet = [
{
interface = "eth0";
cluster-id = "99";
cluster-type = "cluster_flow";
defrag = "yes";
}
{
interface = "default";
}
];
af-xdp = [
{
interface = "eth1";
}
];
dpdk.interfaces = [
{
interface = "eth2";
}
];
pcap = [
{
interface = "eth3";
}
];
app-layer.protocols = {
telnet.enabled = "yes";
dnp3.enabled = "yes";
modbus.enabled = "yes";
};
'';
description = "Suricata settings";
};
enabledSources = mkOption {
type = types.listOf types.str;
# see: nix-shell -p suricata python3Packages.pyyaml --command 'suricata-update list-sources'
default = [
"et/open"
"etnetera/aggressive"
"stamus/lateral"
"oisf/trafficid"
"tgreen/hunting"
"sslbl/ja3-fingerprints"
"sslbl/ssl-fp-blacklist"
"malsilo/win-malware"
"pawpatrules"
];
description = ''
List of sources that should be enabled.
Currently sources which require a secret-code are not supported.
'';
};
disabledRules = mkOption {
type = types.listOf types.str;
# protocol dnp3 seams to be disabled, which causes the signature evaluation to fail, so we disable the
# dnp3 rules, see https://github.com/OISF/suricata/blob/master/rules/dnp3-events.rules for more details
default = [
"2270000"
"2270001"
"2270002"
"2270003"
"2270004"
];
description = ''
List of rules that should be disabled.
'';
};
};
config =
let
captureInterfaces =
let
inherit (lists) unique optionals;
in
unique (
map (e: e.interface) (
(optionals (cfg.settings.af-packet != null) cfg.settings.af-packet)
++ (optionals (cfg.settings.af-xdp != null) cfg.settings.af-xdp)
++ (optionals (
cfg.settings.dpdk != null && cfg.settings.dpdk.interfaces != null
) cfg.settings.dpdk.interfaces)
++ (optionals (cfg.settings.pcap != null) cfg.settings.pcap)
)
);
in
mkIf cfg.enable {
assertions = [
{
assertion = (builtins.length captureInterfaces) > 0;
message = ''
At least one capture interface must be configured:
- `services.suricata.settings.af-packet`
- `services.suricata.settings.af-xdp`
- `services.suricata.settings.dpdk.interfaces`
- `services.suricata.settings.pcap`
'';
}
];
boot.kernelModules = mkIf (cfg.settings.af-packet != null) [ "af_packet" ];
users = {
groups.${cfg.settings.run-as.group} = { };
users.${cfg.settings.run-as.user} = {
group = cfg.settings.run-as.group;
isSystemUser = true;
};
};
systemd.tmpfiles.rules = [
"d ${cfg.settings."default-log-dir"} 755 ${cfg.settings.run-as.user} ${cfg.settings.run-as.group}"
"d /var/lib/suricata 755 ${cfg.settings.run-as.user} ${cfg.settings.run-as.group}"
"d ${cfg.settings."default-rule-path"} 755 ${cfg.settings.run-as.user} ${cfg.settings.run-as.group}"
];
systemd.services = {
suricata-update = {
description = "Update Suricata Rules";
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
script =
let
python = pkgs.python3.withPackages (ps: with ps; [ pyyaml ]);
enabledSourcesCmds = map (
src: "${python.interpreter} ${pkg}/bin/suricata-update enable-source ${src}"
) cfg.enabledSources;
in
''
${concatStringsSep "\n" enabledSourcesCmds}
${python.interpreter} ${pkg}/bin/suricata-update update-sources
${python.interpreter} ${pkg}/bin/suricata-update update --suricata-conf ${cfg.configFile} --no-test \
--disable-conf ${pkgs.writeText "suricata-disable-conf" "${concatStringsSep "\n" cfg.disabledRules}"}
'';
serviceConfig = {
Type = "oneshot";
PrivateTmp = true;
PrivateDevices = true;
PrivateIPC = true;
DynamicUser = true;
User = cfg.settings.run-as.user;
Group = cfg.settings.run-as.group;
ReadOnlyPaths = cfg.configFile;
ReadWritePaths = [
"/var/lib/suricata"
cfg.settings."default-rule-path"
];
};
};
suricata = {
description = "Suricata";
wantedBy = [ "multi-user.target" ];
after = [ "suricata-update.service" ];
serviceConfig =
let
interfaceOptions = strings.concatMapStrings (interface: " -i ${interface}") captureInterfaces;
in
{
ExecStartPre = "!${pkg}/bin/suricata -c ${cfg.configFile} -T";
ExecStart = "!${pkg}/bin/suricata -c ${cfg.configFile}${interfaceOptions}";
Restart = "on-failure";
User = cfg.settings.run-as.user;
Group = cfg.settings.run-as.group;
NoNewPrivileges = true;
PrivateTmp = true;
PrivateDevices = true;
PrivateIPC = true;
ProtectSystem = "strict";
DevicePolicy = "closed";
LockPersonality = true;
MemoryDenyWriteExecute = true;
ProtectHostname = true;
ProtectProc = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectControlGroups = true;
ProcSubset = "pid";
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
RemoveIPC = true;
ReadOnlyPaths = cfg.configFile;
ReadWritePaths = cfg.settings."default-log-dir";
RuntimeDirectory = "suricata";
};
};
};
};
}