at 25.11-pre 16 kB view raw
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}