From 208ee8b2e23d1a658a43e67aef38694de2219069 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sat, 19 Nov 2022 13:13:56 +0100 Subject: [PATCH] nixos/fail2ban: use attrsets for settings instead of strings --- .../manual/release-notes/rl-2311.section.md | 2 + nixos/modules/services/security/fail2ban.nix | 225 +++++++++++------- nixos/tests/all-tests.nix | 1 + nixos/tests/fail2ban.nix | 18 ++ 4 files changed, 154 insertions(+), 92 deletions(-) create mode 100644 nixos/tests/fail2ban.nix diff --git a/nixos/doc/manual/release-notes/rl-2311.section.md b/nixos/doc/manual/release-notes/rl-2311.section.md index 50847f663f50..a1f2effda7c2 100644 --- a/nixos/doc/manual/release-notes/rl-2311.section.md +++ b/nixos/doc/manual/release-notes/rl-2311.section.md @@ -78,6 +78,8 @@ - DocBook option documentation is no longer supported, all module documentation now uses markdown. +- `services.fail2ban.jails` can now be configured with attribute sets defining settings and filters instead of lines. The stringed options `daemonConfig` and `extraSettings` have respectively been replaced by `daemonSettings` and `jails.DEFAULT.settings` which use attribute sets. + - `services.nginx` gained a `defaultListen` option at server-level with support for PROXY protocol listeners, also `proxyProtocol` is now exposed in `services.nginx.virtualHosts..listen` option. It is now possible to run PROXY listeners and non-PROXY listeners at a server-level, see [#213510](https://github.com/NixOS/nixpkgs/pull/213510/) for more details. - `services.prometheus.exporters` has a new exporter to monitor electrical power consumption based on PowercapRAPL sensor called [Scaphandre](https://github.com/hubblo-org/scaphandre), see [#239803](https://github.com/NixOS/nixpkgs/pull/239803) for more details. 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"; }; } diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 02bef2039075..7797391cadeb 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -256,6 +256,7 @@ in { etebase-server = handleTest ./etebase-server.nix {}; etesync-dav = handleTest ./etesync-dav.nix {}; evcc = handleTest ./evcc.nix {}; + fail2ban = handleTest ./fail2ban.nix { }; fakeroute = handleTest ./fakeroute.nix {}; fancontrol = handleTest ./fancontrol.nix {}; fcitx5 = handleTest ./fcitx5 {}; diff --git a/nixos/tests/fail2ban.nix b/nixos/tests/fail2ban.nix new file mode 100644 index 000000000000..c3708575b702 --- /dev/null +++ b/nixos/tests/fail2ban.nix @@ -0,0 +1,18 @@ +import ./make-test-python.nix ({ pkgs, ... }: { + name = "fail2ban"; + + nodes.machine = _: { + services.fail2ban = { + enable = true; + bantime-increment.enable = true; + }; + + services.openssh.enable = true; + }; + + testScript = '' + machine.wait_for_unit("multi-user.target") + + machine.wait_for_unit("fail2ban") + ''; +})