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