1 { config, lib, pkgs, ... }:
6 cfg = config.services.fail2ban;
8 settingsFormat = pkgs.formats.keyValue { };
10 configFormat = pkgs.formats.ini {
11 mkKeyValue = generators.mkKeyValueDefault { } " = ";
14 mkJailConfig = name: attrs:
15 optionalAttrs (name != "DEFAULT") { inherit (attrs) enabled; } //
16 optionalAttrs (attrs.filter != null) { filter = if (builtins.isString filter) then filter else name; } //
19 mkFilter = name: attrs: nameValuePair "fail2ban/filter.d/${name}.conf" {
20 source = configFormat.generate "filter.d/${name}.conf" attrs.filter;
23 fail2banConf = configFormat.generate "fail2ban.local" cfg.daemonSettings;
25 strJails = filterAttrs (_: builtins.isString) cfg.jails;
26 attrsJails = filterAttrs (_: builtins.isAttrs) cfg.jails;
30 configFile = configFormat.generate "jail.local" (
31 { INCLUDES.before = "paths-nixos.conf"; } // (mapAttrs mkJailConfig attrsJails)
33 extraConfig = concatStringsSep "\n" (attrValues (mapAttrs
35 optionalString (def != "")
43 pkgs.concatText "jail.local" [ configFile (pkgs.writeText "extra-jail.local" extraConfig) ];
45 pathsConf = pkgs.writeText "paths-nixos.conf" ''
50 before = paths-common.conf
52 after = paths-overrides.local
61 (mkRemovedOptionModule [ "services" "fail2ban" "daemonConfig" ] "The daemon is now configured through the attribute set `services.fail2ban.daemonSettings`.")
62 (mkRemovedOptionModule [ "services" "fail2ban" "extraSettings" ] "The extra default configuration can now be set using `services.fail2ban.jails.DEFAULT.settings`.")
73 Whether to enable the fail2ban service.
75 See the documentation of {option}`services.fail2ban.jails`
76 for what jails are enabled by default.
80 package = mkPackageOption pkgs "fail2ban" {
81 example = "fail2ban_0_11";
84 packageFirewall = mkOption {
85 default = config.networking.firewall.package;
86 defaultText = literalExpression "config.networking.firewall.package";
88 description = "The firewall package used by fail2ban service. Defaults to the package for your firewall (iptables or nftables).";
91 extraPackages = mkOption {
93 type = types.listOf types.package;
94 example = lib.literalExpression "[ pkgs.ipset ]";
96 Extra packages to be made available to the fail2ban service. The example contains
97 the packages needed by the `iptables-ipset-proto6` action.
105 description = "Number of seconds that a host is banned.";
108 maxretry = mkOption {
110 type = types.ints.unsigned;
111 description = "Number of failures before a host gets banned.";
114 banaction = mkOption {
115 default = if config.networking.nftables.enable then "nftables-multiport" else "iptables-multiport";
116 defaultText = literalExpression ''if config.networking.nftables.enable then "nftables-multiport" else "iptables-multiport"'';
119 Default banning action (e.g. iptables, iptables-new, iptables-multiport,
120 iptables-ipset-proto6-allports, shorewall, etc). It is used to
121 define action_* variables. Can be overridden globally or per
122 section within jail.local file
126 banaction-allports = mkOption {
127 default = if config.networking.nftables.enable then "nftables-allports" else "iptables-allports";
128 defaultText = literalExpression ''if config.networking.nftables.enable then "nftables-allports" else "iptables-allports"'';
131 Default banning action (e.g. iptables, iptables-new, iptables-multiport,
132 shorewall, etc) for "allports" jails. It is used to define action_* variables. Can be overridden
133 globally or per section within jail.local file
137 bantime-increment.enable = mkOption {
141 "bantime.increment" allows to use database for searching of previously banned ip's to increase
142 a default ban time using special formula, default it is banTime * 1, 2, 4, 8, 16, 32 ...
146 bantime-increment.rndtime = mkOption {
148 type = types.nullOr types.str;
151 "bantime.rndtime" is the max number of seconds using for mixing with random time
152 to prevent "clever" botnets calculate exact time IP can be unbanned again
156 bantime-increment.maxtime = mkOption {
158 type = types.nullOr types.str;
161 "bantime.maxtime" is the max number of seconds using the ban time can reach (don't grows further)
165 bantime-increment.factor = mkOption {
167 type = types.nullOr types.str;
170 "bantime.factor" is a coefficient to calculate exponent growing of the formula or common multiplier,
171 default value of factor is 1 and with default value of formula, the ban time grows by 1, 2, 4, 8, 16 ...
175 bantime-increment.formula = mkOption {
177 type = types.nullOr types.str;
178 example = "ban.Time * math.exp(float(ban.Count+1)*banFactor)/math.exp(1*banFactor)";
180 "bantime.formula" used by default to calculate next value of ban time, default value bellow,
181 the same ban time growing will be reached by multipliers 1, 2, 4, 8, 16, 32 ...
185 bantime-increment.multipliers = mkOption {
187 type = types.nullOr types.str;
188 example = "1 2 4 8 16 32 64";
190 "bantime.multipliers" used to calculate next value of ban time instead of formula, corresponding
191 previously ban count and given "bantime.factor" (for multipliers default is 1);
192 following example grows ban time by 1, 2, 4, 8, 16 ... and if last ban count greater as multipliers count,
193 always used last multiplier (64 in example), for factor '1' and original ban time 600 - 10.6 hours
197 bantime-increment.overalljails = mkOption {
199 type = types.nullOr types.bool;
202 "bantime.overalljails" (if true) specifies the search of IP in the database will be executed
203 cross over all jails, if false (default), only current jail of the ban IP will be searched.
207 ignoreIP = mkOption {
209 type = types.listOf types.str;
210 example = [ "192.168.0.0/16" "2001:DB8::42" ];
212 "ignoreIP" can be a list of IP addresses, CIDR masks or DNS hosts. Fail2ban will not ban a host which
213 matches an address in this list. Several addresses can be defined using space (and/or comma) separator.
217 daemonSettings = mkOption {
218 inherit (configFormat) type;
220 defaultText = literalExpression ''
223 logtarget = "SYSLOG";
224 socket = "/run/fail2ban/fail2ban.sock";
225 pidfile = "/run/fail2ban/fail2ban.pid";
226 dbfile = "/var/lib/fail2ban/fail2ban.sqlite3";
231 The contents of Fail2ban's main configuration file.
232 It's generally not necessary to change it.
238 example = literalExpression ''
240 apache-nohome-iptables = {
242 # Block an IP address if it accesses a non-existent
243 # home directory more than 5 times in 10 minutes,
244 # since that indicates that it's scanning.
245 filter = "apache-nohome";
246 action = '''iptables-multiport[name=HTTP, port="http,https"]''';
247 logpath = "/var/log/httpd/error_log*";
256 # block IPs which failed to log-in
257 # aggressive mode add blocking for aborted connections
258 filter = "dovecot[mode=aggressive]";
264 type = with types; attrsOf (either lines (submodule ({ name, ... }: {
266 enabled = mkEnableOption "this jail" // {
268 readOnly = name == "DEFAULT";
272 type = nullOr (either str configFormat.type);
275 description = "Content of the filter used for this jail.";
278 settings = mkOption {
279 inherit (settingsFormat) type;
282 description = "Additional settings for this jail.";
287 The configuration of each Fail2ban “jail”. A jail
288 consists of an action (such as blocking a port using
289 {command}`iptables`) that is triggered when a
290 filter applied to a log file triggers more than a certain
291 number of times in a certain time period. Actions are
292 defined in {file}`/etc/fail2ban/action.d`,
293 while filters are defined in
294 {file}`/etc/fail2ban/filter.d`.
296 NixOS comes with a default `sshd` jail;
298 {option}`services.openssh.logLevel` should be set to
299 `"VERBOSE"` or higher so that fail2ban
300 can observe failed login attempts.
301 This module sets it to `"VERBOSE"` if
302 not set otherwise, so enabling fail2ban can make SSH logs
311 ###### implementation
313 config = mkIf cfg.enable {
316 assertion = cfg.bantime-increment.formula == null || cfg.bantime-increment.multipliers == null;
318 Options `services.fail2ban.bantime-increment.formula` and `services.fail2ban.bantime-increment.multipliers` cannot be both specified.
323 warnings = mkIf (!config.networking.firewall.enable && !config.networking.nftables.enable) [
324 "fail2ban can not be used without a firewall"
327 environment.systemPackages = [ cfg.package ];
330 "fail2ban/fail2ban.local".source = fail2banConf;
331 "fail2ban/jail.local".source = jailConf;
332 "fail2ban/fail2ban.conf".source = "${cfg.package}/etc/fail2ban/fail2ban.conf";
333 "fail2ban/jail.conf".source = "${cfg.package}/etc/fail2ban/jail.conf";
334 "fail2ban/paths-common.conf".source = "${cfg.package}/etc/fail2ban/paths-common.conf";
335 "fail2ban/paths-nixos.conf".source = pathsConf;
336 "fail2ban/action.d".source = "${cfg.package}/etc/fail2ban/action.d/*.conf";
337 "fail2ban/filter.d".source = "${cfg.package}/etc/fail2ban/filter.d/*.conf";
338 } // (mapAttrs' mkFilter (filterAttrs (_: v: v.filter != null && !builtins.isString v.filter) attrsJails));
340 systemd.packages = [ cfg.package ];
341 systemd.services.fail2ban = {
342 wantedBy = [ "multi-user.target" ];
343 partOf = optional config.networking.firewall.enable "firewall.service";
345 restartTriggers = [ fail2banConf jailConf pathsConf ];
347 path = [ cfg.package cfg.packageFirewall pkgs.iproute2 ] ++ cfg.extraPackages;
351 CapabilityBoundingSet = [ "CAP_AUDIT_READ" "CAP_DAC_READ_SEARCH" "CAP_NET_ADMIN" "CAP_NET_RAW" ];
353 NoNewPrivileges = true;
355 RuntimeDirectory = "fail2ban";
356 RuntimeDirectoryMode = "0750";
357 StateDirectory = "fail2ban";
358 StateDirectoryMode = "0750";
359 LogsDirectory = "fail2ban";
360 LogsDirectoryMode = "0750";
362 ProtectSystem = "strict";
365 PrivateDevices = true;
366 ProtectHostname = true;
367 ProtectKernelTunables = true;
368 ProtectKernelModules = true;
369 ProtectControlGroups = true;
373 # Defaults for the daemon settings
374 services.fail2ban.daemonSettings.Definition = {
375 logtarget = mkDefault "SYSLOG";
376 socket = mkDefault "/run/fail2ban/fail2ban.sock";
377 pidfile = mkDefault "/run/fail2ban/fail2ban.pid";
378 dbfile = mkDefault "/var/lib/fail2ban/fail2ban.sqlite3";
381 # Add some reasonable default jails. The special "DEFAULT" jail
382 # sets default values for all other jails.
383 services.fail2ban.jails = mkMerge [
385 DEFAULT.settings = (optionalAttrs cfg.bantime-increment.enable
386 ({ "bantime.increment" = cfg.bantime-increment.enable; } // (mapAttrs'
387 (name: nameValuePair "bantime.${name}")
388 (filterAttrs (n: v: v != null && n != "enable") cfg.bantime-increment))
391 # Miscellaneous options
392 inherit (cfg) banaction maxretry bantime;
393 ignoreip = ''127.0.0.1/8 ${optionalString config.networking.enableIPv6 "::1"} ${concatStringsSep " " cfg.ignoreIP}'';
396 banaction_allports = cfg.banaction-allports;
400 # Block SSH if there are too many failing connection attempts.
401 (mkIf config.services.openssh.enable {
402 sshd.settings.port = mkDefault (concatMapStringsSep "," builtins.toString config.services.openssh.ports);
406 # Benefits from verbose sshd logging to observe failed login attempts,
407 # so we set that here unless the user overrode it.
408 services.openssh.settings.LogLevel = mkDefault "VERBOSE";