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