1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.services.fail2ban;
9
10 settingsFormat = pkgs.formats.keyValue { };
11
12 configFormat = pkgs.formats.ini {
13 mkKeyValue = lib.generators.mkKeyValueDefault { } " = ";
14 };
15
16 mkJailConfig =
17 name: attrs:
18 lib.optionalAttrs (name != "DEFAULT") { inherit (attrs) enabled; }
19 // lib.optionalAttrs (attrs.filter != null) {
20 filter = if (builtins.isString lib.filter) then lib.filter else name;
21 }
22 // attrs.settings;
23
24 mkFilter =
25 name: attrs:
26 lib.nameValuePair "fail2ban/filter.d/${name}.conf" {
27 source = configFormat.generate "filter.d/${name}.conf" attrs.filter;
28 };
29
30 fail2banConf = configFormat.generate "fail2ban.local" cfg.daemonSettings;
31
32 strJails = lib.filterAttrs (_: builtins.isString) cfg.jails;
33 attrsJails = lib.filterAttrs (_: builtins.isAttrs) cfg.jails;
34
35 jailConf =
36 let
37 configFile = configFormat.generate "jail.local" (
38 { INCLUDES.before = "paths-nixos.conf"; } // (lib.mapAttrs mkJailConfig attrsJails)
39 );
40 extraConfig = lib.concatStringsSep "\n" (
41 lib.attrValues (
42 lib.mapAttrs (
43 name: def:
44 lib.optionalString (def != "") ''
45 [${name}]
46 ${def}
47 ''
48 ) strJails
49 )
50 );
51
52 in
53 pkgs.concatText "jail.local" [
54 configFile
55 (pkgs.writeText "extra-jail.local" extraConfig)
56 ];
57
58 pathsConf = pkgs.writeText "paths-nixos.conf" ''
59 # NixOS
60
61 [INCLUDES]
62
63 before = paths-common.conf
64
65 after = paths-overrides.local
66
67 [DEFAULT]
68 '';
69in
70
71{
72
73 imports = [
74 (lib.mkRemovedOptionModule [
75 "services"
76 "fail2ban"
77 "daemonConfig"
78 ] "The daemon is now configured through the attribute set `services.fail2ban.daemonSettings`.")
79 (lib.mkRemovedOptionModule [ "services" "fail2ban" "extraSettings" ]
80 "The extra default configuration can now be set using `services.fail2ban.jails.DEFAULT.settings`."
81 )
82 ];
83
84 ###### interface
85
86 options = {
87 services.fail2ban = {
88 enable = lib.mkOption {
89 default = false;
90 type = lib.types.bool;
91 description = ''
92 Whether to enable the fail2ban service.
93
94 See the documentation of [](#opt-services.fail2ban.jails)
95 for what jails are enabled by default.
96 '';
97 };
98
99 package = lib.mkPackageOption pkgs "fail2ban" {
100 example = "fail2ban_0_11";
101 };
102
103 packageFirewall = lib.mkOption {
104 default = config.networking.firewall.package;
105 defaultText = lib.literalExpression "config.networking.firewall.package";
106 type = lib.types.package;
107 description = "The firewall package used by fail2ban service. Defaults to the package for your firewall (iptables or nftables).";
108 };
109
110 extraPackages = lib.mkOption {
111 default = [ ];
112 type = lib.types.listOf lib.types.package;
113 example = lib.literalExpression "[ pkgs.ipset ]";
114 description = ''
115 Extra packages to be made available to the fail2ban service. The example contains
116 the packages needed by the `iptables-ipset-proto6` action.
117 '';
118 };
119
120 bantime = lib.mkOption {
121 default = "10m";
122 type = lib.types.str;
123 example = "1h";
124 description = "Number of seconds that a host is banned.";
125 };
126
127 maxretry = lib.mkOption {
128 default = 3;
129 type = lib.types.ints.unsigned;
130 description = "Number of failures before a host gets banned.";
131 };
132
133 banaction = lib.mkOption {
134 default = if config.networking.nftables.enable then "nftables-multiport" else "iptables-multiport";
135 defaultText = lib.literalExpression ''if config.networking.nftables.enable then "nftables-multiport" else "iptables-multiport"'';
136 type = lib.types.str;
137 description = ''
138 Default banning action (e.g. iptables, iptables-new, iptables-multiport,
139 iptables-ipset-proto6-allports, shorewall, etc). It is used to
140 define action_* variables. Can be overridden globally or per
141 section within jail.local file
142 '';
143 };
144
145 banaction-allports = lib.mkOption {
146 default = if config.networking.nftables.enable then "nftables-allports" else "iptables-allports";
147 defaultText = lib.literalExpression ''if config.networking.nftables.enable then "nftables-allports" else "iptables-allports"'';
148 type = lib.types.str;
149 description = ''
150 Default banning action (e.g. iptables, iptables-new, iptables-multiport,
151 shorewall, etc) for "allports" jails. It is used to define action_* variables. Can be overridden
152 globally or per section within jail.local file
153 '';
154 };
155
156 bantime-increment.enable = lib.mkOption {
157 default = false;
158 type = lib.types.bool;
159 description = ''
160 "bantime.increment" allows to use database for searching of previously banned ip's to increase
161 a default ban time using special formula, default it is banTime * 1, 2, 4, 8, 16, 32 ...
162 '';
163 };
164
165 bantime-increment.rndtime = lib.mkOption {
166 default = null;
167 type = lib.types.nullOr lib.types.str;
168 example = "8m";
169 description = ''
170 "bantime.rndtime" is the max number of seconds using for mixing with random time
171 to prevent "clever" botnets calculate exact time IP can be unbanned again
172 '';
173 };
174
175 bantime-increment.maxtime = lib.mkOption {
176 default = null;
177 type = lib.types.nullOr lib.types.str;
178 example = "48h";
179 description = ''
180 "bantime.maxtime" is the max number of seconds using the ban time can reach (don't grows further)
181 '';
182 };
183
184 bantime-increment.factor = lib.mkOption {
185 default = null;
186 type = lib.types.nullOr lib.types.str;
187 example = "4";
188 description = ''
189 "bantime.factor" is a coefficient to calculate exponent growing of the formula or common multiplier,
190 default value of factor is 1 and with default value of formula, the ban time grows by 1, 2, 4, 8, 16 ...
191 '';
192 };
193
194 bantime-increment.formula = lib.mkOption {
195 default = null;
196 type = lib.types.nullOr lib.types.str;
197 example = "ban.Time * math.exp(float(ban.Count+1)*banFactor)/math.exp(1*banFactor)";
198 description = ''
199 "bantime.formula" used by default to calculate next value of ban time, default value below,
200 the same ban time growing will be reached by multipliers 1, 2, 4, 8, 16, 32 ...
201 '';
202 };
203
204 bantime-increment.multipliers = lib.mkOption {
205 default = null;
206 type = lib.types.nullOr lib.types.str;
207 example = "1 2 4 8 16 32 64";
208 description = ''
209 "bantime.multipliers" used to calculate next value of ban time instead of formula, corresponding
210 previously ban count and given "bantime.factor" (for multipliers default is 1);
211 following example grows ban time by 1, 2, 4, 8, 16 ... and if last ban count greater as multipliers count,
212 always used last multiplier (64 in example), for factor '1' and original ban time 600 - 10.6 hours
213 '';
214 };
215
216 bantime-increment.overalljails = lib.mkOption {
217 default = null;
218 type = lib.types.nullOr lib.types.bool;
219 example = true;
220 description = ''
221 "bantime.overalljails" (if true) specifies the search of IP in the database will be executed
222 cross over all jails, if false (default), only current jail of the ban IP will be searched.
223 '';
224 };
225
226 ignoreIP = lib.mkOption {
227 default = [ ];
228 type = lib.types.listOf lib.types.str;
229 example = [
230 "192.168.0.0/16"
231 "2001:DB8::42"
232 ];
233 description = ''
234 "ignoreIP" can be a list of IP addresses, CIDR masks or DNS hosts. Fail2ban will not ban a host which
235 matches an address in this list. Several addresses can be defined using space (and/or comma) separator.
236 '';
237 };
238
239 daemonSettings = lib.mkOption {
240 inherit (configFormat) type;
241
242 defaultText = lib.literalExpression ''
243 {
244 Definition = {
245 logtarget = "SYSLOG";
246 socket = "/run/fail2ban/fail2ban.sock";
247 pidfile = "/run/fail2ban/fail2ban.pid";
248 dbfile = "/var/lib/fail2ban/fail2ban.sqlite3";
249 };
250 }
251 '';
252 description = ''
253 The contents of Fail2ban's main configuration file.
254 It's generally not necessary to change it.
255 '';
256 };
257
258 jails = lib.mkOption {
259 default = { };
260 example = lib.literalExpression ''
261 {
262 apache-nohome-iptables = {
263 settings = {
264 # Block an IP address if it accesses a non-existent
265 # home directory more than 5 times in 10 minutes,
266 # since that indicates that it's scanning.
267 filter = "apache-nohome";
268 action = '''iptables-multiport[name=HTTP, port="http,https"]''';
269 logpath = "/var/log/httpd/error_log*";
270 backend = "auto";
271 findtime = 600;
272 bantime = 600;
273 maxretry = 5;
274 };
275 };
276 dovecot = {
277 settings = {
278 # block IPs which failed to log-in
279 # aggressive mode add blocking for aborted connections
280 filter = "dovecot[mode=aggressive]";
281 maxretry = 3;
282 };
283 };
284 };
285 '';
286 type =
287 with lib.types;
288 attrsOf (
289 either lines (
290 submodule (
291 { name, ... }:
292 {
293 options = {
294 enabled = lib.mkEnableOption "this jail" // {
295 default = true;
296 readOnly = name == "DEFAULT";
297 };
298
299 filter = lib.mkOption {
300 type = nullOr (either str configFormat.type);
301
302 default = null;
303 description = "Content of the filter used for this jail.";
304 };
305
306 settings = lib.mkOption {
307 inherit (settingsFormat) type;
308
309 default = { };
310 description = "Additional settings for this jail.";
311 };
312 };
313 }
314 )
315 )
316 );
317 description = ''
318 The configuration of each Fail2ban “jail”. A jail
319 consists of an action (such as blocking a port using
320 {command}`iptables`) that is triggered when a
321 filter applied to a log file triggers more than a certain
322 number of times in a certain time period. Actions are
323 defined in {file}`/etc/fail2ban/action.d`,
324 while filters are defined in
325 {file}`/etc/fail2ban/filter.d`.
326
327 NixOS comes with a default `sshd` jail;
328 for it to work well,
329 [](#opt-services.openssh.settings.LogLevel) should be set to
330 `"VERBOSE"` or higher so that fail2ban
331 can observe failed login attempts.
332 This module sets it to `"VERBOSE"` if
333 not set otherwise, so enabling fail2ban can make SSH logs
334 more verbose.
335 '';
336 };
337
338 };
339
340 };
341
342 ###### implementation
343
344 config = lib.mkIf cfg.enable {
345 assertions = [
346 {
347 assertion = cfg.bantime-increment.formula == null || cfg.bantime-increment.multipliers == null;
348 message = ''
349 Options `services.fail2ban.bantime-increment.formula` and `services.fail2ban.bantime-increment.multipliers` cannot be both specified.
350 '';
351 }
352 ];
353
354 warnings = lib.mkIf (!config.networking.firewall.enable && !config.networking.nftables.enable) [
355 "fail2ban can not be used without a firewall"
356 ];
357
358 environment.systemPackages = [ cfg.package ];
359
360 environment.etc =
361 {
362 "fail2ban/fail2ban.local".source = fail2banConf;
363 "fail2ban/jail.local".source = jailConf;
364 "fail2ban/fail2ban.conf".source = "${cfg.package}/etc/fail2ban/fail2ban.conf";
365 "fail2ban/jail.conf".source = "${cfg.package}/etc/fail2ban/jail.conf";
366 "fail2ban/paths-common.conf".source = "${cfg.package}/etc/fail2ban/paths-common.conf";
367 "fail2ban/paths-nixos.conf".source = pathsConf;
368 "fail2ban/action.d".source = "${cfg.package}/etc/fail2ban/action.d/*.conf";
369 "fail2ban/filter.d".source = "${cfg.package}/etc/fail2ban/filter.d/*.conf";
370 }
371 // (lib.mapAttrs' mkFilter (
372 lib.filterAttrs (_: v: v.filter != null && !builtins.isString v.filter) attrsJails
373 ));
374
375 systemd.packages = [ cfg.package ];
376 systemd.services.fail2ban = {
377 wantedBy = [ "multi-user.target" ];
378 partOf = lib.optional config.networking.firewall.enable "firewall.service";
379
380 restartTriggers = [
381 fail2banConf
382 jailConf
383 pathsConf
384 ];
385
386 path = [
387 cfg.package
388 cfg.packageFirewall
389 pkgs.iproute2
390 ] ++ cfg.extraPackages;
391
392 serviceConfig = {
393 # Capabilities
394 CapabilityBoundingSet = [
395 "CAP_AUDIT_READ"
396 "CAP_DAC_READ_SEARCH"
397 "CAP_NET_ADMIN"
398 "CAP_NET_RAW"
399 ];
400 # Security
401 NoNewPrivileges = true;
402 # Directory
403 RuntimeDirectory = "fail2ban";
404 RuntimeDirectoryMode = "0750";
405 StateDirectory = "fail2ban";
406 StateDirectoryMode = "0750";
407 LogsDirectory = "fail2ban";
408 LogsDirectoryMode = "0750";
409 # Sandboxing
410 ProtectSystem = "strict";
411 ProtectHome = true;
412 PrivateTmp = true;
413 PrivateDevices = true;
414 ProtectHostname = true;
415 ProtectKernelTunables = true;
416 ProtectKernelModules = true;
417 ProtectControlGroups = true;
418 };
419 };
420
421 # Defaults for the daemon settings
422 services.fail2ban.daemonSettings.Definition = {
423 logtarget = lib.mkDefault "SYSLOG";
424 socket = lib.mkDefault "/run/fail2ban/fail2ban.sock";
425 pidfile = lib.mkDefault "/run/fail2ban/fail2ban.pid";
426 dbfile = lib.mkDefault "/var/lib/fail2ban/fail2ban.sqlite3";
427 };
428
429 # Add some reasonable default jails. The special "DEFAULT" jail
430 # sets default values for all other jails.
431 services.fail2ban.jails = lib.mkMerge [
432 {
433 DEFAULT.settings =
434 (lib.optionalAttrs cfg.bantime-increment.enable (
435 {
436 "bantime.increment" = cfg.bantime-increment.enable;
437 }
438 // (lib.mapAttrs' (name: lib.nameValuePair "bantime.${name}") (
439 lib.filterAttrs (n: v: v != null && n != "enable") cfg.bantime-increment
440 ))
441 ))
442 // {
443 # Miscellaneous options
444 inherit (cfg) banaction maxretry bantime;
445 ignoreip = ''127.0.0.1/8 ${lib.optionalString config.networking.enableIPv6 "::1"} ${lib.concatStringsSep " " cfg.ignoreIP}'';
446 backend = "systemd";
447 # Actions
448 banaction_allports = cfg.banaction-allports;
449 };
450 }
451
452 # Block SSH if there are too many failing connection attempts.
453 (lib.mkIf config.services.openssh.enable {
454 sshd.settings.port = lib.mkDefault (
455 lib.concatMapStringsSep "," builtins.toString config.services.openssh.ports
456 );
457 })
458 ];
459
460 # Benefits from verbose sshd logging to observe failed login attempts,
461 # so we set that here unless the user overrode it.
462 services.openssh.settings.LogLevel = lib.mkDefault "VERBOSE";
463 };
464}