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 = ''
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 = mkPackageOption pkgs "fail2ban" {
81 example = "fail2ban_0_11";
82 };
83
84 packageFirewall = mkOption {
85 default = config.networking.firewall.package;
86 defaultText = literalExpression "config.networking.firewall.package";
87 type = types.package;
88 description = "The firewall package used by fail2ban service. Defaults to the package for your firewall (iptables or nftables).";
89 };
90
91 extraPackages = mkOption {
92 default = [ ];
93 type = types.listOf types.package;
94 example = lib.literalExpression "[ pkgs.ipset ]";
95 description = ''
96 Extra packages to be made available to the fail2ban service. The example contains
97 the packages needed by the `iptables-ipset-proto6` action.
98 '';
99 };
100
101 bantime = mkOption {
102 default = "10m";
103 type = types.str;
104 example = "1h";
105 description = "Number of seconds that a host is banned.";
106 };
107
108 maxretry = mkOption {
109 default = 3;
110 type = types.ints.unsigned;
111 description = "Number of failures before a host gets banned.";
112 };
113
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"'';
117 type = types.str;
118 description = ''
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
123 '';
124 };
125
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"'';
129 type = types.str;
130 description = ''
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
134 '';
135 };
136
137 bantime-increment.enable = mkOption {
138 default = false;
139 type = types.bool;
140 description = ''
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 ...
143 '';
144 };
145
146 bantime-increment.rndtime = mkOption {
147 default = null;
148 type = types.nullOr types.str;
149 example = "8m";
150 description = ''
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
153 '';
154 };
155
156 bantime-increment.maxtime = mkOption {
157 default = null;
158 type = types.nullOr types.str;
159 example = "48h";
160 description = ''
161 "bantime.maxtime" is the max number of seconds using the ban time can reach (don't grows further)
162 '';
163 };
164
165 bantime-increment.factor = mkOption {
166 default = null;
167 type = types.nullOr types.str;
168 example = "4";
169 description = ''
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 ...
172 '';
173 };
174
175 bantime-increment.formula = mkOption {
176 default = null;
177 type = types.nullOr types.str;
178 example = "ban.Time * math.exp(float(ban.Count+1)*banFactor)/math.exp(1*banFactor)";
179 description = ''
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 ...
182 '';
183 };
184
185 bantime-increment.multipliers = mkOption {
186 default = null;
187 type = types.nullOr types.str;
188 example = "1 2 4 8 16 32 64";
189 description = ''
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
194 '';
195 };
196
197 bantime-increment.overalljails = mkOption {
198 default = null;
199 type = types.nullOr types.bool;
200 example = true;
201 description = ''
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.
204 '';
205 };
206
207 ignoreIP = mkOption {
208 default = [ ];
209 type = types.listOf types.str;
210 example = [ "192.168.0.0/16" "2001:DB8::42" ];
211 description = ''
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.
214 '';
215 };
216
217 daemonSettings = mkOption {
218 inherit (configFormat) type;
219
220 defaultText = literalExpression ''
221 {
222 Definition = {
223 logtarget = "SYSLOG";
224 socket = "/run/fail2ban/fail2ban.sock";
225 pidfile = "/run/fail2ban/fail2ban.pid";
226 dbfile = "/var/lib/fail2ban/fail2ban.sqlite3";
227 };
228 }
229 '';
230 description = ''
231 The contents of Fail2ban's main configuration file.
232 It's generally not necessary to change it.
233 '';
234 };
235
236 jails = mkOption {
237 default = { };
238 example = literalExpression ''
239 {
240 apache-nohome-iptables = {
241 settings = {
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*";
248 backend = "auto";
249 findtime = 600;
250 bantime = 600;
251 maxretry = 5;
252 };
253 };
254 dovecot = {
255 settings = {
256 # block IPs which failed to log-in
257 # aggressive mode add blocking for aborted connections
258 filter = "dovecot[mode=aggressive]";
259 maxretry = 3;
260 };
261 };
262 };
263 '';
264 type = with types; attrsOf (either lines (submodule ({ name, ... }: {
265 options = {
266 enabled = mkEnableOption "this jail." // {
267 default = true;
268 readOnly = name == "DEFAULT";
269 };
270
271 filter = mkOption {
272 type = nullOr (either str configFormat.type);
273
274 default = null;
275 description = "Content of the filter used for this jail.";
276 };
277
278 settings = mkOption {
279 inherit (settingsFormat) type;
280
281 default = { };
282 description = "Additional settings for this jail.";
283 };
284 };
285 })));
286 description = ''
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`.
295
296 NixOS comes with a default `sshd` jail;
297 for it to work well,
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
303 more verbose.
304 '';
305 };
306
307 };
308
309 };
310
311 ###### implementation
312
313 config = mkIf cfg.enable {
314 assertions = [
315 {
316 assertion = cfg.bantime-increment.formula == null || cfg.bantime-increment.multipliers == null;
317 message = ''
318 Options `services.fail2ban.bantime-increment.formula` and `services.fail2ban.bantime-increment.multipliers` cannot be both specified.
319 '';
320 }
321 ];
322
323 warnings = mkIf (!config.networking.firewall.enable && !config.networking.nftables.enable) [
324 "fail2ban can not be used without a firewall"
325 ];
326
327 environment.systemPackages = [ cfg.package ];
328
329 environment.etc = {
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));
339
340 systemd.packages = [ cfg.package ];
341 systemd.services.fail2ban = {
342 wantedBy = [ "multi-user.target" ];
343 partOf = optional config.networking.firewall.enable "firewall.service";
344
345 restartTriggers = [ fail2banConf jailConf pathsConf ];
346
347 path = [ cfg.package cfg.packageFirewall pkgs.iproute2 ] ++ cfg.extraPackages;
348
349 serviceConfig = {
350 # Capabilities
351 CapabilityBoundingSet = [ "CAP_AUDIT_READ" "CAP_DAC_READ_SEARCH" "CAP_NET_ADMIN" "CAP_NET_RAW" ];
352 # Security
353 NoNewPrivileges = true;
354 # Directory
355 RuntimeDirectory = "fail2ban";
356 RuntimeDirectoryMode = "0750";
357 StateDirectory = "fail2ban";
358 StateDirectoryMode = "0750";
359 LogsDirectory = "fail2ban";
360 LogsDirectoryMode = "0750";
361 # Sandboxing
362 ProtectSystem = "strict";
363 ProtectHome = true;
364 PrivateTmp = true;
365 PrivateDevices = true;
366 ProtectHostname = true;
367 ProtectKernelTunables = true;
368 ProtectKernelModules = true;
369 ProtectControlGroups = true;
370 };
371 };
372
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";
379 };
380
381 # Add some reasonable default jails. The special "DEFAULT" jail
382 # sets default values for all other jails.
383 services.fail2ban.jails = mkMerge [
384 {
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))
389 )
390 ) // {
391 # Miscellaneous options
392 inherit (cfg) banaction maxretry bantime;
393 ignoreip = ''127.0.0.1/8 ${optionalString config.networking.enableIPv6 "::1"} ${concatStringsSep " " cfg.ignoreIP}'';
394 backend = "systemd";
395 # Actions
396 banaction_allports = cfg.banaction-allports;
397 };
398 }
399
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);
403 })
404 ];
405
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";
409 };
410}