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 = "Whether to enable the fail2ban service.";
49 };
50
51 package = mkOption {
52 default = pkgs.fail2ban;
53 type = types.package;
54 example = "pkgs.fail2ban_0_11";
55 description = "The fail2ban package to use for running the fail2ban service.";
56 };
57
58 packageFirewall = mkOption {
59 default = pkgs.iptables;
60 type = types.package;
61 example = "pkgs.nftables";
62 description = "The firewall package used by fail2ban service.";
63 };
64
65 extraPackages = mkOption {
66 default = [];
67 type = types.listOf types.package;
68 example = lib.literalExample "[ pkgs.ipset ]";
69 description = ''
70 Extra packages to be made available to the fail2ban service. The example contains
71 the packages needed by the `iptables-ipset-proto6` action.
72 '';
73 };
74
75 maxretry = mkOption {
76 default = 3;
77 type = types.ints.unsigned;
78 description = "Number of failures before a host gets banned.";
79 };
80
81 banaction = mkOption {
82 default = "iptables-multiport";
83 type = types.str;
84 example = "nftables-multiport";
85 description = ''
86 Default banning action (e.g. iptables, iptables-new, iptables-multiport,
87 shorewall, etc) It is used to define action_* variables. Can be overridden
88 globally or per section within jail.local file
89 '';
90 };
91
92 banaction-allports = mkOption {
93 default = "iptables-allport";
94 type = types.str;
95 example = "nftables-allport";
96 description = ''
97 Default banning action (e.g. iptables, iptables-new, iptables-multiport,
98 shorewall, etc) It is used to define action_* variables. Can be overridden
99 globally or per section within jail.local file
100 '';
101 };
102
103 bantime-increment.enable = mkOption {
104 default = false;
105 type = types.bool;
106 description = ''
107 Allows to use database for searching of previously banned ip's to increase
108 a default ban time using special formula, default it is banTime * 1, 2, 4, 8, 16, 32...
109 '';
110 };
111
112 bantime-increment.rndtime = mkOption {
113 default = "4m";
114 type = types.str;
115 example = "8m";
116 description = ''
117 "bantime-increment.rndtime" is the max number of seconds using for mixing with random time
118 to prevent "clever" botnets calculate exact time IP can be unbanned again
119 '';
120 };
121
122 bantime-increment.maxtime = mkOption {
123 default = "10h";
124 type = types.str;
125 example = "48h";
126 description = ''
127 "bantime-increment.maxtime" is the max number of seconds using the ban time can reach (don't grows further)
128 '';
129 };
130
131 bantime-increment.factor = mkOption {
132 default = "1";
133 type = types.str;
134 example = "4";
135 description = ''
136 "bantime-increment.factor" is a coefficient to calculate exponent growing of the formula or common multiplier,
137 default value of factor is 1 and with default value of formula, the ban time grows by 1, 2, 4, 8, 16 ...
138 '';
139 };
140
141 bantime-increment.formula = mkOption {
142 default = "ban.Time * (1<<(ban.Count if ban.Count<20 else 20)) * banFactor";
143 type = types.str;
144 example = "ban.Time * math.exp(float(ban.Count+1)*banFactor)/math.exp(1*banFactor)";
145 description = ''
146 "bantime-increment.formula" used by default to calculate next value of ban time, default value bellow,
147 the same ban time growing will be reached by multipliers 1, 2, 4, 8, 16, 32...
148 '';
149 };
150
151 bantime-increment.multipliers = mkOption {
152 default = "1 2 4 8 16 32 64";
153 type = types.str;
154 example = "2 4 16 128";
155 description = ''
156 "bantime-increment.multipliers" used to calculate next value of ban time instead of formula, coresponding
157 previously ban count and given "bantime.factor" (for multipliers default is 1);
158 following example grows ban time by 1, 2, 4, 8, 16 ... and if last ban count greater as multipliers count,
159 always used last multiplier (64 in example), for factor '1' and original ban time 600 - 10.6 hours
160 '';
161 };
162
163 bantime-increment.overalljails = mkOption {
164 default = false;
165 type = types.bool;
166 example = true;
167 description = ''
168 "bantime-increment.overalljails" (if true) specifies the search of IP in the database will be executed
169 cross over all jails, if false (dafault), only current jail of the ban IP will be searched
170 '';
171 };
172
173 ignoreIP = mkOption {
174 default = [ ];
175 type = types.listOf types.str;
176 example = [ "192.168.0.0/16" "2001:DB8::42" ];
177 description = ''
178 "ignoreIP" can be a list of IP addresses, CIDR masks or DNS hosts. Fail2ban will not ban a host which
179 matches an address in this list. Several addresses can be defined using space (and/or comma) separator.
180 '';
181 };
182
183 daemonConfig = mkOption {
184 default = ''
185 [Definition]
186 logtarget = SYSLOG
187 socket = /run/fail2ban/fail2ban.sock
188 pidfile = /run/fail2ban/fail2ban.pid
189 dbfile = /var/lib/fail2ban/fail2ban.sqlite3
190 '';
191 type = types.lines;
192 description = ''
193 The contents of Fail2ban's main configuration file. It's
194 generally not necessary to change it.
195 '';
196 };
197
198 jails = mkOption {
199 default = { };
200 example = literalExample ''
201 { apache-nohome-iptables = '''
202 # Block an IP address if it accesses a non-existent
203 # home directory more than 5 times in 10 minutes,
204 # since that indicates that it's scanning.
205 filter = apache-nohome
206 action = iptables-multiport[name=HTTP, port="http,https"]
207 logpath = /var/log/httpd/error_log*
208 findtime = 600
209 bantime = 600
210 maxretry = 5
211 ''';
212 }
213 '';
214 type = types.attrsOf types.lines;
215 description = ''
216 The configuration of each Fail2ban “jail”. A jail
217 consists of an action (such as blocking a port using
218 <command>iptables</command>) that is triggered when a
219 filter applied to a log file triggers more than a certain
220 number of times in a certain time period. Actions are
221 defined in <filename>/etc/fail2ban/action.d</filename>,
222 while filters are defined in
223 <filename>/etc/fail2ban/filter.d</filename>.
224 '';
225 };
226
227 };
228
229 };
230
231 ###### implementation
232
233 config = mkIf cfg.enable {
234
235 warnings = mkIf (config.networking.firewall.enable == false && config.networking.nftables.enable == false) [
236 "fail2ban can not be used without a firewall"
237 ];
238
239 environment.systemPackages = [ cfg.package ];
240
241 environment.etc = {
242 "fail2ban/fail2ban.local".source = fail2banConf;
243 "fail2ban/jail.local".source = jailConf;
244 "fail2ban/fail2ban.conf".source = "${cfg.package}/etc/fail2ban/fail2ban.conf";
245 "fail2ban/jail.conf".source = "${cfg.package}/etc/fail2ban/jail.conf";
246 "fail2ban/paths-common.conf".source = "${cfg.package}/etc/fail2ban/paths-common.conf";
247 "fail2ban/paths-nixos.conf".source = pathsConf;
248 "fail2ban/action.d".source = "${cfg.package}/etc/fail2ban/action.d/*.conf";
249 "fail2ban/filter.d".source = "${cfg.package}/etc/fail2ban/filter.d/*.conf";
250 };
251
252 systemd.services.fail2ban = {
253 description = "Fail2ban Intrusion Prevention System";
254
255 wantedBy = [ "multi-user.target" ];
256 after = [ "network.target" ];
257 partOf = optional config.networking.firewall.enable "firewall.service";
258
259 restartTriggers = [ fail2banConf jailConf pathsConf ];
260 reloadIfChanged = true;
261
262 path = [ cfg.package cfg.packageFirewall pkgs.iproute2 ] ++ cfg.extraPackages;
263
264 unitConfig.Documentation = "man:fail2ban(1)";
265
266 serviceConfig = {
267 ExecStart = "${cfg.package}/bin/fail2ban-server -xf start";
268 ExecStop = "${cfg.package}/bin/fail2ban-server stop";
269 ExecReload = "${cfg.package}/bin/fail2ban-server reload";
270 Type = "simple";
271 Restart = "on-failure";
272 PIDFile = "/run/fail2ban/fail2ban.pid";
273 # Capabilities
274 CapabilityBoundingSet = [ "CAP_AUDIT_READ" "CAP_DAC_READ_SEARCH" "CAP_NET_ADMIN" "CAP_NET_RAW" ];
275 # Security
276 NoNewPrivileges = true;
277 # Directory
278 RuntimeDirectory = "fail2ban";
279 RuntimeDirectoryMode = "0750";
280 StateDirectory = "fail2ban";
281 StateDirectoryMode = "0750";
282 LogsDirectory = "fail2ban";
283 LogsDirectoryMode = "0750";
284 # Sandboxing
285 ProtectSystem = "strict";
286 ProtectHome = true;
287 PrivateTmp = true;
288 PrivateDevices = true;
289 ProtectHostname = true;
290 ProtectKernelTunables = true;
291 ProtectKernelModules = true;
292 ProtectControlGroups = true;
293 };
294 };
295
296 # Add some reasonable default jails. The special "DEFAULT" jail
297 # sets default values for all other jails.
298 services.fail2ban.jails.DEFAULT = ''
299 ${optionalString cfg.bantime-increment.enable ''
300 # Bantime incremental
301 bantime.increment = ${boolToString cfg.bantime-increment.enable}
302 bantime.maxtime = ${cfg.bantime-increment.maxtime}
303 bantime.factor = ${cfg.bantime-increment.factor}
304 bantime.formula = ${cfg.bantime-increment.formula}
305 bantime.multipliers = ${cfg.bantime-increment.multipliers}
306 bantime.overalljails = ${boolToString cfg.bantime-increment.overalljails}
307 ''}
308 # Miscellaneous options
309 ignoreip = 127.0.0.1/8 ${optionalString config.networking.enableIPv6 "::1"} ${concatStringsSep " " cfg.ignoreIP}
310 maxretry = ${toString cfg.maxretry}
311 backend = systemd
312 # Actions
313 banaction = ${cfg.banaction}
314 banaction_allports = ${cfg.banaction-allports}
315 '';
316 # Block SSH if there are too many failing connection attempts.
317 services.fail2ban.jails.sshd = mkDefault ''
318 enabled = true
319 port = ${concatMapStringsSep "," (p: toString p) config.services.openssh.ports}
320 '';
321 };
322}