From 208ee8b2e23d1a658a43e67aef38694de2219069 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sat, 19 Nov 2022 13:13:56 +0100 Subject: nixos/fail2ban: use attrsets for settings instead of strings --- nixos/modules/services/security/fail2ban.nix | 225 ++++++++++++++++----------- 1 file changed, 133 insertions(+), 92 deletions(-) (limited to 'nixos/modules/services/security') diff --git a/nixos/modules/services/security/fail2ban.nix b/nixos/modules/services/security/fail2ban.nix index eebf2a2f7b55..9393fa751288 100644 --- a/nixos/modules/services/security/fail2ban.nix +++ b/nixos/modules/services/security/fail2ban.nix @@ -3,23 +3,44 @@ with lib; let - cfg = config.services.fail2ban; - fail2banConf = pkgs.writeText "fail2ban.local" cfg.daemonConfig; + settingsFormat = pkgs.formats.keyValue { }; - jailConf = pkgs.writeText "jail.local" '' - [INCLUDES] + configFormat = pkgs.formats.ini { + mkKeyValue = generators.mkKeyValueDefault { } " = "; + }; - before = paths-nixos.conf + mkJailConfig = name: attrs: + optionalAttrs (name != "DEFAULT") { inherit (attrs) enabled; } // + optionalAttrs (attrs.filter != null) { filter = if (builtins.isString filter) then filter else name; } // + attrs.settings; - ${concatStringsSep "\n" (attrValues (flip mapAttrs cfg.jails (name: def: - optionalString (def != "") - '' - [${name}] - ${def} - '')))} - ''; + mkFilter = name: attrs: nameValuePair "fail2ban/filter.d/${name}.conf" { + source = configFormat.generate "filter.d/${name}.conf" attrs.filter; + }; + + fail2banConf = configFormat.generate "fail2ban.local" cfg.daemonSettings; + + strJails = filterAttrs (_: builtins.isString) cfg.jails; + attrsJails = filterAttrs (_: builtins.isAttrs) cfg.jails; + + jailConf = + let + configFile = configFormat.generate "jail.local" ( + { INCLUDES.before = "paths-nixos.conf"; } // (mapAttrs mkJailConfig attrsJails) + ); + extraConfig = concatStringsSep "\n" (attrValues (mapAttrs + (name: def: + optionalString (def != "") + '' + [${name}] + ${def} + '') + strJails)); + + in + pkgs.concatText "jail.local" [ configFile (pkgs.writeText "extra-jail.local" extraConfig) ]; pathsConf = pkgs.writeText "paths-nixos.conf" '' # NixOS @@ -32,15 +53,18 @@ let [DEFAULT] ''; - in { + imports = [ + (mkRemovedOptionModule [ "services" "fail2ban" "daemonConfig" ] "The daemon is now configured through the attribute set `services.fail2ban.daemonSettings`.") + (mkRemovedOptionModule [ "services" "fail2ban" "extraSettings" ] "The extra default configuration can now be set using `services.fail2ban.jails.DEFAULT.settings`.") + ]; + ###### interface options = { - services.fail2ban = { enable = mkOption { default = false; @@ -69,7 +93,7 @@ in }; extraPackages = mkOption { - default = []; + default = [ ]; type = types.listOf types.package; example = lib.literalExpression "[ pkgs.ipset ]"; description = lib.mdDoc '' @@ -180,7 +204,7 @@ in example = true; description = lib.mdDoc '' "bantime.overalljails" (if true) specifies the search of IP in the database will be executed - cross over all jails, if false (default), only current jail of the ban IP will be searched + cross over all jails, if false (default), only current jail of the ban IP will be searched. ''; }; @@ -194,60 +218,75 @@ in ''; }; - daemonConfig = mkOption { - default = '' - [Definition] - logtarget = SYSLOG - socket = /run/fail2ban/fail2ban.sock - pidfile = /run/fail2ban/fail2ban.pid - dbfile = /var/lib/fail2ban/fail2ban.sqlite3 - ''; - type = types.lines; - description = lib.mdDoc '' - The contents of Fail2ban's main configuration file. It's - generally not necessary to change it. - ''; - }; + daemonSettings = mkOption { + inherit (configFormat) type; - extraSettings = mkOption { - type = with types; attrsOf (oneOf [ bool ints.positive str ]); - default = {}; - description = lib.mdDoc '' - Extra default configuration for all jails (i.e. `[DEFAULT]`). See - for an overview. - ''; - example = literalExpression '' + defaultText = literalExpression '' { - findtime = "15m"; + Definition = { + logtarget = "SYSLOG"; + socket = "/run/fail2ban/fail2ban.sock"; + pidfile = "/run/fail2ban/fail2ban.pid"; + dbfile = "/var/lib/fail2ban/fail2ban.sqlite3"; + }; } ''; + description = lib.mdDoc '' + The contents of Fail2ban's main configuration file. + It's generally not necessary to change it. + ''; }; jails = mkOption { default = { }; example = literalExpression '' - { apache-nohome-iptables = ''' - # Block an IP address if it accesses a non-existent - # home directory more than 5 times in 10 minutes, - # since that indicates that it's scanning. - filter = apache-nohome - action = iptables-multiport[name=HTTP, port="http,https"] - logpath = /var/log/httpd/error_log* - backend = auto - findtime = 600 - bantime = 600 - maxretry = 5 - '''; - dovecot = ''' - # block IPs which failed to log-in - # aggressive mode add blocking for aborted connections - enabled = true - filter = dovecot[mode=aggressive] - maxretry = 3 - '''; - } + { + apache-nohome-iptables = { + settings = { + # Block an IP address if it accesses a non-existent + # home directory more than 5 times in 10 minutes, + # since that indicates that it's scanning. + filter = "apache-nohome"; + action = '''iptables-multiport[name=HTTP, port="http,https"]'''; + logpath = "/var/log/httpd/error_log*"; + backend = "auto"; + findtime = 600; + bantime = 600; + maxretry = 5; + }; + }; + dovecot = { + settings = { + # block IPs which failed to log-in + # aggressive mode add blocking for aborted connections + filter = "dovecot[mode=aggressive]"; + maxretry = 3; + }; + }; + }; ''; - type = types.attrsOf types.lines; + type = with types; attrsOf (either lines (submodule ({ name, ... }: { + options = { + enabled = mkEnableOption "this jail." // { + default = true; + readOnly = name == "DEFAULT"; + }; + + filter = mkOption { + type = nullOr (either str configFormat.type); + + default = null; + description = lib.mdDoc "Content of the filter used for this jail."; + }; + + settings = mkOption { + inherit (settingsFormat) type; + + default = { }; + description = lib.mdDoc "Additional settings for this jail."; + }; + }; + }))); description = lib.mdDoc '' The configuration of each Fail2ban “jail”. A jail consists of an action (such as blocking a port using @@ -278,7 +317,7 @@ in config = mkIf cfg.enable { assertions = [ { - assertion = (cfg.bantime-increment.formula == null || cfg.bantime-increment.multipliers == null); + assertion = cfg.bantime-increment.formula == null || cfg.bantime-increment.multipliers == null; message = '' Options `services.fail2ban.bantime-increment.formula` and `services.fail2ban.bantime-increment.multipliers` cannot be both specified. ''; @@ -300,7 +339,7 @@ in "fail2ban/paths-nixos.conf".source = pathsConf; "fail2ban/action.d".source = "${cfg.package}/etc/fail2ban/action.d/*.conf"; "fail2ban/filter.d".source = "${cfg.package}/etc/fail2ban/filter.d/*.conf"; - }; + } // (mapAttrs' mkFilter (filterAttrs (_: v: v.filter != null && !builtins.isString v.filter) attrsJails)); systemd.packages = [ cfg.package ]; systemd.services.fail2ban = { @@ -335,39 +374,41 @@ in }; }; + # Defaults for the daemon settings + services.fail2ban.daemonSettings.Definition = { + logtarget = mkDefault "SYSLOG"; + socket = mkDefault "/run/fail2ban/fail2ban.sock"; + pidfile = mkDefault "/run/fail2ban/fail2ban.pid"; + dbfile = mkDefault "/var/lib/fail2ban/fail2ban.sqlite3"; + }; + # Add some reasonable default jails. The special "DEFAULT" jail # sets default values for all other jails. - services.fail2ban.jails.DEFAULT = '' - # Bantime increment options - bantime.increment = ${boolToString cfg.bantime-increment.enable} - ${optionalString (cfg.bantime-increment.rndtime != null) "bantime.rndtime = ${cfg.bantime-increment.rndtime}"} - ${optionalString (cfg.bantime-increment.maxtime != null) "bantime.maxtime = ${cfg.bantime-increment.maxtime}"} - ${optionalString (cfg.bantime-increment.factor != null) "bantime.factor = ${cfg.bantime-increment.factor}"} - ${optionalString (cfg.bantime-increment.formula != null) "bantime.formula = ${cfg.bantime-increment.formula}"} - ${optionalString (cfg.bantime-increment.multipliers != null) "bantime.multipliers = ${cfg.bantime-increment.multipliers}"} - ${optionalString (cfg.bantime-increment.overalljails != null) "bantime.overalljails = ${boolToString cfg.bantime-increment.overalljails}"} - # Miscellaneous options - ignoreip = 127.0.0.1/8 ${optionalString config.networking.enableIPv6 "::1"} ${concatStringsSep " " cfg.ignoreIP} - ${optionalString (cfg.bantime != null) '' - bantime = ${cfg.bantime} - ''} - maxretry = ${toString cfg.maxretry} - backend = systemd - # Actions - banaction = ${cfg.banaction} - banaction_allports = ${cfg.banaction-allports} - ${optionalString (cfg.extraSettings != {}) '' - # Extra settings - ${generators.toKeyValue {} cfg.extraSettings} - ''} - ''; - # Block SSH if there are too many failing connection attempts. + services.fail2ban.jails = mkMerge [ + { + DEFAULT.settings = (optionalAttrs cfg.bantime-increment.enable + ({ "bantime.increment" = cfg.bantime-increment.enable; } // (mapAttrs' + (name: nameValuePair "bantime.${name}") + (filterAttrs (n: v: v != null && n != "enable") cfg.bantime-increment)) + ) + ) // { + # Miscellaneous options + inherit (cfg) banaction maxretry; + ignoreip = ''127.0.0.1/8 ${optionalString config.networking.enableIPv6 "::1"} ${concatStringsSep " " cfg.ignoreIP}''; + backend = "systemd"; + # Actions + banaction_allports = cfg.banaction-allports; + }; + } + + # Block SSH if there are too many failing connection attempts. + (mkIf config.services.openssh.enable { + sshd.settings.port = mkDefault (concatMapStringsSep "," builtins.toString config.services.openssh.ports); + }) + ]; + # Benefits from verbose sshd logging to observe failed login attempts, # so we set that here unless the user overrode it. - services.openssh.settings.LogLevel = lib.mkDefault "VERBOSE"; - services.fail2ban.jails.sshd = mkDefault '' - enabled = true - port = ${concatMapStringsSep "," (p: toString p) config.services.openssh.ports} - ''; + services.openssh.settings.LogLevel = mkDefault "VERBOSE"; }; } -- cgit 1.4.1