From fd0aeacff4390e571bbc852daa3c081d86c347eb Mon Sep 17 00:00:00 2001
From: Tom Hubrecht <tom.hubrecht@dgnum.eu>
Date: Sun, 1 Sep 2024 22:47:56 +0200
Subject: [PATCH] feat(firewall): Sunset fail2ban and switch to reaction

---
 machines/compute01/_configuration.nix |  6 --
 machines/rescue01/_configuration.nix  |  6 --
 machines/storage01/_configuration.nix |  3 -
 machines/vault01/_configuration.nix   |  6 --
 machines/web01/_configuration.nix     |  1 -
 machines/web02/_configuration.nix     |  6 --
 modules/default.nix                   |  3 +-
 modules/dgn-fail2ban/default.nix      | 90 --------------------------
 modules/dgn-fail2ban/jails.nix        | 93 ---------------------------
 modules/dgn-firewall/default.nix      | 87 +++++++++++++++++++++++++
 modules/dgn-firewall/streams.nix      | 46 +++++++++++++
 npins/sources.json                    |  4 +-
 12 files changed, 137 insertions(+), 214 deletions(-)
 delete mode 100644 modules/dgn-fail2ban/default.nix
 delete mode 100644 modules/dgn-fail2ban/jails.nix
 create mode 100644 modules/dgn-firewall/default.nix
 create mode 100644 modules/dgn-firewall/streams.nix

diff --git a/machines/compute01/_configuration.nix b/machines/compute01/_configuration.nix
index 59060d8..91fb3bb 100644
--- a/machines/compute01/_configuration.nix
+++ b/machines/compute01/_configuration.nix
@@ -4,7 +4,6 @@ lib.extra.mkConfig {
   enabledModules = [
     # List of modules to enable
     "dgn-backups"
-    "dgn-fail2ban"
     "dgn-web"
   ];
 
@@ -32,11 +31,6 @@ lib.extra.mkConfig {
   ];
 
   extraConfig = {
-    dgn-fail2ban.jails = lib.extra.enableAttrs' "enabled" [
-      "sshd-bruteforce"
-      "sshd-timeout"
-    ];
-
     dgn-hardware.useZfs = true;
 
     services.netbird.enable = true;
diff --git a/machines/rescue01/_configuration.nix b/machines/rescue01/_configuration.nix
index 6c50ba3..ca68e2e 100644
--- a/machines/rescue01/_configuration.nix
+++ b/machines/rescue01/_configuration.nix
@@ -3,7 +3,6 @@
 lib.extra.mkConfig {
   enabledModules = [
     # List of modules to enable
-    "dgn-fail2ban"
   ];
 
   enabledServices = [
@@ -12,11 +11,6 @@ lib.extra.mkConfig {
   ];
 
   extraConfig = {
-    dgn-fail2ban.jails = lib.extra.enableAttrs' "enabled" [
-      "sshd-bruteforce"
-      "sshd-timeout"
-    ];
-
     services.netbird.enable = true;
   };
 
diff --git a/machines/storage01/_configuration.nix b/machines/storage01/_configuration.nix
index 69f7880..a15da38 100644
--- a/machines/storage01/_configuration.nix
+++ b/machines/storage01/_configuration.nix
@@ -4,7 +4,6 @@ lib.extra.mkConfig {
   enabledModules = [
     # List of modules to enable
     "dgn-backups"
-    "dgn-fail2ban"
     "dgn-web"
   ];
 
@@ -22,8 +21,6 @@ lib.extra.mkConfig {
   ];
 
   extraConfig = {
-    dgn-fail2ban.jails.sshd-preauth.enabled = true;
-
     dgn-hardware.useZfs = true;
 
     services.netbird.enable = true;
diff --git a/machines/vault01/_configuration.nix b/machines/vault01/_configuration.nix
index bea9622..5e94808 100644
--- a/machines/vault01/_configuration.nix
+++ b/machines/vault01/_configuration.nix
@@ -3,7 +3,6 @@
 lib.extra.mkConfig {
   enabledModules = [
     # List of modules to enable
-    "dgn-fail2ban"
   ];
 
   enabledServices = [
@@ -14,11 +13,6 @@ lib.extra.mkConfig {
   ];
 
   extraConfig = {
-    dgn-fail2ban.jails = lib.extra.enableAttrs' "enabled" [
-      "sshd-bruteforce"
-      "sshd-timeout"
-    ];
-
     services.netbird.enable = true;
     services.nginx.enable = true;
     networking.firewall.allowedTCPPorts = [ 80 ];
diff --git a/machines/web01/_configuration.nix b/machines/web01/_configuration.nix
index 8cb9425..1e30feb 100644
--- a/machines/web01/_configuration.nix
+++ b/machines/web01/_configuration.nix
@@ -3,7 +3,6 @@
 lib.extra.mkConfig {
   enabledModules = [
     # List of modules to enable
-    "dgn-fail2ban"
     "dgn-web"
   ];
 
diff --git a/machines/web02/_configuration.nix b/machines/web02/_configuration.nix
index bb44155..898f7f6 100644
--- a/machines/web02/_configuration.nix
+++ b/machines/web02/_configuration.nix
@@ -3,7 +3,6 @@
 lib.extra.mkConfig {
   enabledModules = [
     # List of modules to enable
-    "dgn-fail2ban"
     "dgn-web"
   ];
 
@@ -14,11 +13,6 @@ lib.extra.mkConfig {
   ];
 
   extraConfig = {
-    dgn-fail2ban.jails = lib.extra.enableAttrs' "enabled" [
-      "sshd-bruteforce"
-      "sshd-timeout"
-    ];
-
     # Restrict access to this node
     dgn-access-control.users.root = [ "thubrecht" ];
 
diff --git a/modules/default.nix b/modules/default.nix
index 3047718..8e5d995 100644
--- a/modules/default.nix
+++ b/modules/default.nix
@@ -46,7 +46,7 @@
       "dgn-acme"
       "dgn-backups"
       "dgn-console"
-      "dgn-fail2ban"
+      "dgn-firewall"
       "dgn-hardware"
       "dgn-netbox-agent"
       "dgn-network"
@@ -67,6 +67,7 @@
       [
         "age-secrets"
         "services/bupstash"
+        "services/reaction"
         "services/systemd-notify"
       ]
       ++ nodeMeta.nix-modules
diff --git a/modules/dgn-fail2ban/default.nix b/modules/dgn-fail2ban/default.nix
deleted file mode 100644
index b2f3fe6..0000000
--- a/modules/dgn-fail2ban/default.nix
+++ /dev/null
@@ -1,90 +0,0 @@
-{
-  config,
-  lib,
-  pkgs,
-  ...
-}:
-
-let
-  inherit (lib)
-    mkDefault
-    mkEnableOption
-    mkIf
-    mkOption
-
-    types
-    ;
-
-  cfg = config.dgn-fail2ban;
-
-  settingsFormat = pkgs.formats.keyValue { };
-
-  configFormat = pkgs.formats.ini { };
-
-  jailOptions = {
-    options = {
-      enabled = mkOption {
-        type = types.bool;
-
-        default = true;
-        description = "Wether to enable this jail.";
-      };
-
-      filter = mkOption {
-        type = types.nullOr (types.submodule { freeformType = configFormat.type; });
-
-        description = "Content of the filter used for this jail.";
-      };
-
-      settings = mkOption {
-        type = types.submodule { freeformType = settingsFormat.type; };
-
-        default = { };
-        description = "Additional configuration for the jail.";
-      };
-    };
-  };
-in
-{
-  options.dgn-fail2ban = {
-    enable = mkEnableOption "fail2ban service.";
-
-    jails = mkOption {
-      type = types.attrsOf (types.submodule jailOptions);
-
-      default = { };
-      description = "Set of jails defined for fail2ban.";
-    };
-  };
-
-  config = mkIf cfg.enable {
-    dgn-fail2ban.jails = builtins.mapAttrs (_: j: j // { enabled = mkDefault false; }) (
-      import ./jails.nix { }
-    );
-
-    services.fail2ban = {
-      enable = true;
-
-      inherit (cfg) jails;
-
-      ignoreIP = [
-        "10.0.0.0/8"
-        "129.199.0.0/16"
-        "172.16.0.0/12"
-        "192.168.0.0/16"
-        "100.64.0.0/10"
-        "fd00::/8"
-      ];
-
-      bantime-increment = {
-        enable = true;
-
-        maxtime = "48h";
-        factor = "600";
-      };
-
-      extraPackages = [ pkgs.ipset ];
-      banaction = "iptables-ipset-proto6-allports";
-    };
-  };
-}
diff --git a/modules/dgn-fail2ban/jails.nix b/modules/dgn-fail2ban/jails.nix
deleted file mode 100644
index 49a5d4b..0000000
--- a/modules/dgn-fail2ban/jails.nix
+++ /dev/null
@@ -1,93 +0,0 @@
-# Copyright Tom Hubrecht, (2023)
-#
-# Tom Hubrecht <tom@hubrecht.ovh>
-#
-# This software is a computer program whose purpose is to configure
-# machines and servers with NixOS.
-#
-# This software is governed by the CeCILL  license under French law and
-# abiding by the rules of distribution of free software.  You can  use,
-# modify and/ or redistribute the software under the terms of the CeCILL
-# license as circulated by CEA, CNRS and INRIA at the following URL
-# "http://www.cecill.info".
-#
-# As a counterpart to the access to the source code and  rights to copy,
-# modify and redistribute granted by the license, users are provided only
-# with a limited warranty  and the software's author,  the holder of the
-# economic rights,  and the successive licensors  have only  limited
-# liability.
-#
-# In this respect, the user's attention is drawn to the risks associated
-# with loading,  using,  modifying and/or developing or reproducing the
-# software by the user in light of its specific status of free software,
-# that may mean  that it is complicated to manipulate,  and  that  also
-# therefore means  that it is reserved for developers  and  experienced
-# professionals having in-depth computer knowledge. Users are therefore
-# encouraged to load and test the software's suitability as regards their
-# requirements in conditions enabling the security of their systems and/or
-# data to be ensured and,  more generally, to use and operate it in the
-# same conditions as regards security.
-#
-# The fact that you are presently reading this means that you have had
-# knowledge of the CeCILL license and that you accept its terms.
-
-_: {
-  nginx-spam = {
-    filter.Definition.failregex = ''^<HOST>.*GET.*(matrix/server|\.php|admin|wp\-).* HTTP/\d.\d\" 404.*$'';
-
-    settings = {
-      logpath = "/var/log/nginx/access.log";
-      backend = "auto";
-      maxretry = 500;
-      findtime = 60;
-    };
-  };
-
-  postfix-bruteforce = {
-    filter.Definition = {
-      failregex = "warning: [\\w\\.\\-]+\\[<HOST>\\]: SASL LOGIN authentication failed.*$";
-      journalmatch = "_SYSTEMD_UNIT=postfix.service";
-    };
-
-    settings = {
-      findtime = 600;
-      maxretry = 1;
-    };
-  };
-
-  sshd-bruteforce = {
-    filter.Definition = {
-      failregex = "pam_unix\\(sshd:auth\\): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=<ADDR>.*$";
-      journalmatch = "_SYSTEMD_UNIT=sshd.service";
-    };
-
-    settings = {
-      findtime = 600;
-      maxretry = 1;
-    };
-  };
-
-  sshd-preauth = {
-    filter.Definition = {
-      failregex = "Received disconnect from <ADDR> port .* Bye Bye \\[preauth\\]$";
-      journalmatch = "_SYSTEMD_UNIT=sshd.service";
-    };
-
-    settings = {
-      findtime = 600;
-      maxretry = 1;
-    };
-  };
-
-  sshd-timeout = {
-    filter.Definition = {
-      failregex = "fatal: Timeout before authentication for <ADDR>.*$";
-      journalmatch = "_SYSTEMD_UNIT=sshd.service";
-    };
-
-    settings = {
-      findtime = 600;
-      maxretry = 1;
-    };
-  };
-}
diff --git a/modules/dgn-firewall/default.nix b/modules/dgn-firewall/default.nix
new file mode 100644
index 0000000..4d16617
--- /dev/null
+++ b/modules/dgn-firewall/default.nix
@@ -0,0 +1,87 @@
+{
+  pkgs,
+  lib,
+  name,
+  ...
+}:
+
+let
+  inherit (lib)
+    concatStringsSep
+    length
+    replicate
+    splitString
+    ;
+
+  inherit (lib.lists) map;
+
+  c4 = "(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)";
+
+  mkV4' = p: concatStringsSep "\\." (p ++ (replicate (4 - (length p)) c4));
+  mkV4 = s: mkV4' (splitString "." s);
+
+  nft = s: [ "nft" ] ++ [ s ];
+
+  streams' = import ./streams.nix;
+in
+
+{
+  # Switch to nftables
+  networking.nftables.enable = true;
+
+  services.reaction = {
+    enable = true;
+
+    extraPackages = [ pkgs.nftables ];
+
+    runAsRoot = true;
+
+    logLevel = "WARN";
+
+    settings = {
+      patterns = {
+        ip = {
+          regex = "(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}|(?:(?:[0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(?::[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(?:ffff(?::0{1,4}){0,1}:){0,1}(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])|(?:[0-9a-fA-F]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9]))";
+
+          ignoreregex = map mkV4 [
+            "10" # Legacy wireguard
+            "129.199" # ENS
+            "100.76" # Netbird internal
+          ];
+
+          ignore = [
+            "127.0.0.1"
+            "::1"
+          ];
+        };
+      };
+
+      start = [
+        (nft ''
+          table inet reaction {
+            set ipv4bans {
+              type ipv4_addr
+              flags interval
+              auto-merge
+            }
+            set ipv6bans {
+              type ipv6_addr
+              flags interval
+              auto-merge
+            }
+            chain input {
+              type filter hook input priority 0
+              policy accept
+              ip saddr @ipv4bans drop
+              ip6 saddr @ipv6bans drop
+            }
+          }
+        '')
+      ];
+
+      stop = [ (nft "delete table inet reaction") ];
+
+      streams = streams'.default // (streams'.${name} or { });
+    };
+  };
+}
diff --git a/modules/dgn-firewall/streams.nix b/modules/dgn-firewall/streams.nix
new file mode 100644
index 0000000..18b5caf
--- /dev/null
+++ b/modules/dgn-firewall/streams.nix
@@ -0,0 +1,46 @@
+let
+  act = a: [
+    "nft46"
+    "${a} element inet reaction ipvXbans { <ip> }"
+  ];
+
+  journalctl = u: [
+    "journalctl"
+    "-fn0"
+    "-u"
+    "${u}.service"
+  ];
+
+  ban = after: {
+    ban.cmd = act "add";
+    unban = {
+      inherit after;
+
+      cmd = act "delete";
+    };
+  };
+
+  available = {
+    ssh = {
+      cmd = journalctl "sshd";
+
+      filters = {
+        failedlogin = {
+          regex = [
+            "authentication failure;.*rhost=<ip>"
+            "Connection reset by authenticating user .* <ip>"
+            "Connection closed by invalid user .* <ip> port .*"
+            "Failed password for .* from <ip>"
+            "Invalid user .* from <ip> port .*"
+            "Unable to negotiate with <ip> port .*"
+          ];
+          actions = ban "48h";
+        };
+      };
+    };
+  };
+in
+
+builtins.mapAttrs (_: builtins.foldl' (a: s: a // { ${s} = available.${s}; }) { }) {
+  default = [ "ssh" ];
+}
diff --git a/npins/sources.json b/npins/sources.json
index 7560e8f..fa3443f 100644
--- a/npins/sources.json
+++ b/npins/sources.json
@@ -191,9 +191,9 @@
         "url": "https://git.hubrecht.ovh/hubrecht/nix-modules.git"
       },
       "branch": "main",
-      "revision": "a5645b5b3bed87ce9c9d633ad20537b0edd34e74",
+      "revision": "32e76ee64352587663766e1a3945a6fe0917e35d",
       "url": null,
-      "hash": "1vmi9ldmc1vibw21bv1lg41mbii8v0w9k3809awyf1ikir99jbyf"
+      "hash": "16vnpnby6s174y4nzb26z2pc49ba7lw7vpf6r7p4dqci92b0yg5j"
     },
     "nix-patches": {
       "type": "GitRelease",