From b6456d64faa0c45b32a88e3325cd9cf316ce4204 Mon Sep 17 00:00:00 2001 From: Bart Oostveen Date: Wed, 25 Feb 2026 22:34:36 +0100 Subject: [PATCH] nixos/prometheus-exporters/fail2ban: init --- .../monitoring/prometheus/exporters.nix | 26 ++++++ .../prometheus/exporters/fail2ban.nix | 82 +++++++++++++++++++ nixos/tests/prometheus-exporters.nix | 18 ++++ 3 files changed, 126 insertions(+) create mode 100644 nixos/modules/services/monitoring/prometheus/exporters/fail2ban.nix diff --git a/nixos/modules/services/monitoring/prometheus/exporters.nix b/nixos/modules/services/monitoring/prometheus/exporters.nix index 0e38fb27508f..82daa54d8342 100644 --- a/nixos/modules/services/monitoring/prometheus/exporters.nix +++ b/nixos/modules/services/monitoring/prometheus/exporters.nix @@ -66,6 +66,7 @@ let "domain" "dovecot" "ebpf" + "fail2ban" "fastly" "flow" "fritz" @@ -341,6 +342,31 @@ let services.udev.extraRules = mkIf (name == "smartctl") '' ACTION=="add", SUBSYSTEM=="nvme", KERNEL=="nvme[0-9]*", RUN+="${pkgs.acl}/bin/setfacl -m g:smartctl-exporter-access:rw /dev/$kernel" ''; + systemd.services.prometheus-fail2ban-exporter-setup = + mkIf (config.services.fail2ban.enable && name == "fail2ban") + { + description = "Set fail2ban socket ACLs"; + after = [ "fail2ban.service" ]; + requires = [ "fail2ban.service" ]; + before = [ "prometheus-fail2ban-exporter.service" ]; + wantedBy = [ "prometheus-fail2ban-exporter.service" ]; + path = [ + pkgs.acl + pkgs.coreutils + ]; + script = '' + while [ ! -S ${conf.fail2banSocket} ]; do + sleep 0.1 + done + + setfacl -m u:${conf.user}:x $(dirname ${conf.fail2banSocket}) + setfacl -m u:${conf.user}:rwx ${conf.fail2banSocket} + ''; + serviceConfig = { + Type = "oneshot"; + User = "root"; + }; + }; networking.firewall.extraCommands = mkIf (conf.openFirewall && !nftables) (concatStrings [ "ip46tables -A nixos-fw ${conf.firewallFilter} " "-m comment --comment ${name}-exporter -j nixos-fw-accept" diff --git a/nixos/modules/services/monitoring/prometheus/exporters/fail2ban.nix b/nixos/modules/services/monitoring/prometheus/exporters/fail2ban.nix new file mode 100644 index 000000000000..8e86ccb79605 --- /dev/null +++ b/nixos/modules/services/monitoring/prometheus/exporters/fail2ban.nix @@ -0,0 +1,82 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.services.prometheus.exporters.fail2ban; + + inherit (lib) + mkOption + types + getExe + optionalString + mkIf + ; +in +{ + port = 9191; + extraOpts = { + host = mkOption { + description = "The host that the fail2ban exporter should listen on"; + type = types.str; + default = "127.0.0.1"; + example = "0.0.0.0"; + }; + fail2banSocket = mkOption { + description = "Path to the fail2ban server socket. Permissions will be set automatically if fail2ban runs on this system."; + type = types.str; + default = config.services.fail2ban.daemonSettings.Definition.socket; + defaultText = "config.services.fail2ban.daemonSettings.Definition.socket"; + }; + exitOnError = mkOption { + description = "When set to true the exporter will immediately exit on a fail2ban socket connection error"; + type = types.bool; + default = true; + example = false; + }; + username = mkOption { + description = "Username to protect endpoints with HTTP basic authentication"; + type = types.nullOr types.str; + default = null; + example = "admin"; + }; + passwordFile = mkOption { + description = "File that contains the password to protect endpoints with HTTP basic authentication"; + type = types.nullOr types.path; + default = null; + example = "/run/secrets/prometheus-fail2ban-exporter-password.txt"; + }; + }; + + assertions = [ + { + assertion = (cfg.username != null) -> (cfg.passwordFile != null); + message = "Setting an http basic auth username requires the password to be non-null"; + } + ]; + + serviceOpts = { + requires = mkIf config.services.fail2ban.enable [ "prometheus-fail2ban-exporter-setup.service" ]; + serviceConfig = { + DynamicUser = false; + ExecStart = '' + ${getExe pkgs.prometheus-fail2ban-exporter} \ + ${optionalString cfg.exitOnError ''--collector.f2b.exit-on-socket-connection-error \''} + ${optionalString (cfg.username != null) '' + --web.basic-auth.username="${cfg.username}" \ + --web.basic-auth.password="$(cat ${cfg.passwordFile})" \ + ''} + --web.listen-address="${cfg.host}:${toString cfg.port}" \ + --collector.f2b.socket=${cfg.fail2banSocket} + ''; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ]; + }; + }; +} diff --git a/nixos/tests/prometheus-exporters.nix b/nixos/tests/prometheus-exporters.nix index 5374661cb4cf..e5624ef0f3cd 100644 --- a/nixos/tests/prometheus-exporters.nix +++ b/nixos/tests/prometheus-exporters.nix @@ -436,6 +436,24 @@ let ''; }; + fail2ban = + { ... }: + { + exporterConfig = { + enable = true; + exitOnError = true; + }; + metricProvider = { + services.fail2ban.enable = true; + }; + exporterTest = '' + wait_for_unit("fail2ban.service") + wait_for_unit("prometheus-fail2ban-exporter.service") + wait_for_open_port(9191) + succeed("curl -sSf http://localhost:9191/metrics | grep 'f2b_errors'") + ''; + }; + fastly = { pkgs, ... }: {