Files
nixpkgs/nixos/modules/services/networking/suricata/default.nix

329 lines
10 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.runCommand "suricata.yaml"
{
settingsYaml = yaml.generate "suricata-settings-raw.yaml" (
filterAttrsRecursive (name: value: value != null) cfg.settings
);
}
''
echo "%YAML 1.1" > $out
echo "---" >> $out
cat $settingsYaml >> $out
'';
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 = [
"abuse.ch/sslbl-blacklist"
"abuse.ch/sslbl-c2"
"abuse.ch/sslbl-ja3"
"et/open"
"etnetera/aggressive"
"stamus/lateral"
"oisf/trafficid"
"tgreen/hunting"
"pawpatrules"
"ptrules/open"
];
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"
"2270005"
"2270006"
"2270007"
];
description = ''
List of matchers specifying which rules should be disabled.
These can be raw SID numbers or something like "group:emerging-coinminer.rules".
'';
};
dropRules = mkOption {
type = types.listOf types.str;
default = [ ];
example = literalExpression ''
[ "2274852" "4327876" "902244405" ]
'';
description = ''
List of matchers specifying which rules should be converted to drop rules.
These can be raw SID numbers or something like "group:emerging-coinminer.rules".
'';
};
reloadOnRulesetUpdate = mkOption {
type = types.bool;
default = false;
description = ''
Whether to reload Suricata if it is running after an automated ruleset update.
This is a blocking reload, and may take some time depending on the number of rules and computational power of the host.
'';
};
};
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.timers = {
suricata-update = {
timerConfig = {
OnBootSec = lib.mkDefault "30s";
OnUnitActiveSec = lib.mkDefault "24h";
Persistent = true;
Unit = config.systemd.services.suricata-update.name;
};
};
};
systemd.services = {
suricata-blocking-reload = lib.mkIf cfg.reloadOnRulesetUpdate {
description = "Refresh Runtime Suricata Ruleset";
serviceConfig = {
Type = "oneshot";
ExecCondition = "systemctl is-active --quiet suricata.service";
ExecStart = "${pkg}/bin/suricatasc -c reload-rules";
};
};
suricata-update = {
description = "Update Suricata Rules";
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
onSuccess = lib.mkIf cfg.reloadOnRulesetUpdate [ "suricata-blocking-reload.service" ];
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}"} \
--drop-conf ${pkgs.writeText "suricata-drop.conf" "${concatStringsSep "\n" cfg.dropRules}"}
'';
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 = false; # pcre2 jit
ProtectHostname = true;
ProtectProc = "invisible";
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";
};
};
};
};
}