diff --git a/configuration/default.nix b/configuration/default.nix
index c9c71ec..18d124e 100644
--- a/configuration/default.nix
+++ b/configuration/default.nix
@@ -18,7 +18,7 @@
     ./services/backups.nix
     ./services/battery-manager.nix
     ./services/conduit.nix
-    ./services/crowdsec.nix
+    ./services/fail2ban.nix
     ./services/foundryvtt.nix
     ./services/gitea.nix
     ./services/metrics
diff --git a/configuration/services/crowdsec.nix b/configuration/services/crowdsec.nix
deleted file mode 100644
index 110602c..0000000
--- a/configuration/services/crowdsec.nix
+++ /dev/null
@@ -1,35 +0,0 @@
-{ pkgs, ... }:
-{
-  security.crowdsec = {
-    enable = true;
-
-    parserWhitelist = [
-      "1.64.239.213"
-    ];
-
-    settings.crowdsec_service.acquisition_path = pkgs.writeText "crowdsec-acquisitions.yaml" ''
-      ---
-      source: journalctl
-      journalctl_filter:
-        - "SYSLOG_IDENTIFIER=Nextcloud"
-      labels:
-        type: syslog
-      ---
-      source: journalctl
-      journalctl_filter:
-        - "SYSLOG_IDENTIFIER=sshd-session"
-      labels:
-        type: syslog
-      ---
-    '';
-
-    remediationComponents.firewallBouncer = {
-      enable = true;
-      settings.prometheus = {
-        enabled = true;
-        listen_addr = "127.0.0.1";
-        listen_port = "60601";
-      };
-    };
-  };
-}
diff --git a/configuration/services/fail2ban.nix b/configuration/services/fail2ban.nix
new file mode 100644
index 0000000..f09668c
--- /dev/null
+++ b/configuration/services/fail2ban.nix
@@ -0,0 +1,43 @@
+{ pkgs, ... }:
+{
+  services.fail2ban = {
+    enable = true;
+    extraPackages = [ pkgs.ipset ];
+    banaction = "iptables-ipset-proto6-allports";
+    bantime-increment.enable = true;
+
+    jails = {
+      nginx-botsearch = ''
+        enabled = true
+        logpath = /var/log/nginx/access.log
+      '';
+    };
+
+    ignoreIP = [
+      "127.0.0.0/8"
+      "10.0.0.0/8"
+      "172.16.0.0/12"
+      "192.168.0.0/16"
+    ];
+  };
+
+  # Allow metrics services to connect to the socket as well
+  users.groups.fail2ban = { };
+  systemd.services.fail2ban.serviceConfig = {
+    ExecStartPost =
+      "+"
+      + (pkgs.writeShellScript "fail2ban-post-start" ''
+        while ! [ -S /var/run/fail2ban/fail2ban.sock ]; do
+            sleep 1
+        done
+
+        while ! ${pkgs.netcat}/bin/nc -zU /var/run/fail2ban/fail2ban.sock; do
+            sleep 1
+        done
+
+        ${pkgs.coreutils}/bin/chown root:fail2ban /var/run/fail2ban /var/run/fail2ban/fail2ban.sock
+        ${pkgs.coreutils}/bin/chmod 660 /var/run/fail2ban/fail2ban.sock
+        ${pkgs.coreutils}/bin/chmod 710 /var/run/fail2ban
+      '');
+  };
+}
diff --git a/configuration/services/gitea.nix b/configuration/services/gitea.nix
index da01cde..c88dd01 100644
--- a/configuration/services/gitea.nix
+++ b/configuration/services/gitea.nix
@@ -59,6 +59,24 @@ in
       };
     };
 
+  # Block repeated failed login attempts
+  #
+  # TODO(tlater): Update this - we switched to forgejo, who knows what
+  # the new matches are.
+  # environment.etc = {
+  #   "fail2ban/filter.d/gitea.conf".text = ''
+  #     [Definition]
+  #     failregex = .*(Failed authentication attempt|invalid credentials|Attempted access of unknown user).* from <HOST>
+  #     journalmatch = _SYSTEMD_UNIT=forgejo.service + _COMM=forgejo + SYSLOG_IDENTIFIER=forgejo
+  #   '';
+  # };
+
+  # services.fail2ban.jails = {
+  #   gitea = ''
+  #     enabled = true
+  #   '';
+  # };
+
   services.backups.forgejo = {
     user = "forgejo";
     paths = [
diff --git a/configuration/services/metrics/exporters.nix b/configuration/services/metrics/exporters.nix
index ecd69bd..e16b945 100644
--- a/configuration/services/metrics/exporters.nix
+++ b/configuration/services/metrics/exporters.nix
@@ -68,6 +68,34 @@ in
       };
     };
 
+    extraExporters = {
+      fail2ban =
+        let
+          cfg = config.services.prometheus.extraExporters.fail2ban;
+        in
+        {
+          port = 9191;
+          serviceOpts = {
+            after = [ "fail2ban.service" ];
+            requires = [ "fail2ban.service" ];
+            serviceConfig = {
+              Group = "fail2ban";
+              RestrictAddressFamilies = [
+                "AF_UNIX"
+                "AF_INET"
+                "AF_INET6"
+              ];
+              ExecStart = lib.concatStringsSep " " [
+                "${pkgs.local.prometheus-fail2ban-exporter}/bin/fail2ban-prometheus-exporter"
+                "--collector.f2b.socket=/var/run/fail2ban/fail2ban.sock"
+                "--web.listen-address='${cfg.listenAddress}:${toString cfg.port}'"
+                "--collector.f2b.exit-on-socket-connection-error=true"
+              ];
+            };
+          };
+        };
+    };
+
     # TODO(tlater):
     #   - wireguard (?)
     #   - postgres (?)
diff --git a/configuration/services/metrics/options.nix b/configuration/services/metrics/options.nix
index 8868c6c..69cbd6b 100644
--- a/configuration/services/metrics/options.nix
+++ b/configuration/services/metrics/options.nix
@@ -12,7 +12,6 @@ in
   options = {
     services.prometheus = {
       extraExporters = mkOption {
-        default = { };
         type = types.attrsOf (
           types.submodule {
             options = {
diff --git a/configuration/services/metrics/victoriametrics.nix b/configuration/services/metrics/victoriametrics.nix
index 5cfc614..710cf70 100644
--- a/configuration/services/metrics/victoriametrics.nix
+++ b/configuration/services/metrics/victoriametrics.nix
@@ -10,22 +10,6 @@
         extraSettings.authorization.credentials_file = config.sops.secrets."forgejo/metrics-token".path;
       };
       coturn.targets = [ "127.0.0.1:9641" ];
-
-      crowdsec.targets =
-        let
-          address = config.security.crowdsec.settings.prometheus.listen_addr;
-          port = config.security.crowdsec.settings.prometheus.listen_port;
-        in
-        [ "${address}:${toString port}" ];
-
-      csFirewallBouncer.targets =
-        let
-          address =
-            config.security.crowdsec.remediationComponents.firewallBouncer.settings.prometheus.listen_addr;
-          port =
-            config.security.crowdsec.remediationComponents.firewallBouncer.settings.prometheus.listen_port;
-        in
-        [ "${address}:${toString port}" ];
     };
   };
 }
diff --git a/configuration/services/nextcloud.nix b/configuration/services/nextcloud.nix
index b5cb691..e54df14 100644
--- a/configuration/services/nextcloud.nix
+++ b/configuration/services/nextcloud.nix
@@ -70,6 +70,29 @@ in
     # The upstream module already adds HSTS
   };
 
+  # Block repeated failed login attempts
+  environment.etc = {
+    "fail2ban/filter.d/nextcloud.conf".text = ''
+      [Definition]
+      _groupsre = (?:(?:,?\s*"\w+":(?:"[^"]+"|\w+))*)
+      failregex = \{%(_groupsre)s,?\s*"remoteAddr":"<HOST>"%(_groupsre)s,?\s*"message":"Login failed:
+                  \{%(_groupsre)s,?\s*"remoteAddr":"<HOST>"%(_groupsre)s,?\s*"message":"Trusted domain error.
+      datepattern = ,?\s*"time"\s*:\s*"%%Y-%%m-%%d[T ]%%H:%%M:%%S(%%z)?"
+      journalmatch = SYSLOG_IDENTIFIER=Nextcloud
+    '';
+  };
+
+  services.fail2ban.jails = {
+    nextcloud = ''
+      enabled = true
+
+      # Nextcloud does some throttling already, so we need to set
+      # these to something bigger.
+      findtime = 43200
+      bantime = 86400
+    '';
+  };
+
   services.backups.nextcloud = {
     user = "nextcloud";
     paths = [
diff --git a/flake.lock b/flake.lock
index d86b361..d349bea 100644
--- a/flake.lock
+++ b/flake.lock
@@ -114,10 +114,44 @@
         "type": "github"
       }
     },
+    "flake-compat_3": {
+      "flake": false,
+      "locked": {
+        "lastModified": 1696426674,
+        "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
+        "owner": "edolstra",
+        "repo": "flake-compat",
+        "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
+        "type": "github"
+      },
+      "original": {
+        "owner": "edolstra",
+        "repo": "flake-compat",
+        "type": "github"
+      }
+    },
     "flake-utils": {
       "inputs": {
         "systems": "systems_2"
       },
+      "locked": {
+        "lastModified": 1731533236,
+        "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "type": "github"
+      }
+    },
+    "flake-utils_2": {
+      "inputs": {
+        "systems": "systems_3"
+      },
       "locked": {
         "lastModified": 1726560853,
         "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
@@ -190,22 +224,6 @@
         "type": "github"
       }
     },
-    "nixpkgs-crowdsec": {
-      "locked": {
-        "lastModified": 1738085579,
-        "narHash": "sha256-7mLjMrOiiIi0vI7BJwbEipYQzwA7JF/NWHP+LM4q5S8=",
-        "owner": "tlater",
-        "repo": "nixpkgs",
-        "rev": "426a7afc9a6ecfdac544bda4022acef31e36df34",
-        "type": "github"
-      },
-      "original": {
-        "owner": "tlater",
-        "ref": "tlater/fix-crowdsec",
-        "repo": "nixpkgs",
-        "type": "github"
-      }
-    },
     "nixpkgs-unstable": {
       "locked": {
         "lastModified": 1737192615,
@@ -254,15 +272,37 @@
         "type": "github"
       }
     },
+    "nvfetcher": {
+      "inputs": {
+        "flake-compat": "flake-compat_2",
+        "flake-utils": "flake-utils",
+        "nixpkgs": [
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1732501185,
+        "narHash": "sha256-Z0BpHelaGQsE5VD9hBsBHsvMU9h+Xt0kfkDJyFivZOU=",
+        "owner": "berberman",
+        "repo": "nvfetcher",
+        "rev": "bdb14eab6fe9cefc29efe01e60c3a3f616d6b62a",
+        "type": "github"
+      },
+      "original": {
+        "owner": "berberman",
+        "repo": "nvfetcher",
+        "type": "github"
+      }
+    },
     "poetry2nixi": {
       "inputs": {
-        "flake-utils": "flake-utils",
+        "flake-utils": "flake-utils_2",
         "nix-github-actions": "nix-github-actions",
         "nixpkgs": [
           "sonnenshift",
           "nixpkgs"
         ],
-        "systems": "systems_3",
+        "systems": "systems_4",
         "treefmt-nix": "treefmt-nix"
       },
       "locked": {
@@ -281,7 +321,7 @@
     },
     "purescript-overlay": {
       "inputs": {
-        "flake-compat": "flake-compat_2",
+        "flake-compat": "flake-compat_3",
         "nixpkgs": [
           "tlaternet-webserver",
           "dream2nix",
@@ -326,8 +366,8 @@
         "disko": "disko",
         "foundryvtt": "foundryvtt",
         "nixpkgs": "nixpkgs_2",
-        "nixpkgs-crowdsec": "nixpkgs-crowdsec",
         "nixpkgs-unstable": "nixpkgs-unstable",
+        "nvfetcher": "nvfetcher",
         "sonnenshift": "sonnenshift",
         "sops-nix": "sops-nix",
         "tlaternet-webserver": "tlaternet-webserver"
@@ -445,6 +485,21 @@
       }
     },
     "systems_3": {
+      "locked": {
+        "lastModified": 1681028828,
+        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+        "owner": "nix-systems",
+        "repo": "default",
+        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-systems",
+        "repo": "default",
+        "type": "github"
+      }
+    },
+    "systems_4": {
       "locked": {
         "lastModified": 1681028828,
         "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
diff --git a/flake.nix b/flake.nix
index 3d04d7c..e6f1dcb 100644
--- a/flake.nix
+++ b/flake.nix
@@ -13,6 +13,10 @@
       url = "github:Mic92/sops-nix";
       inputs.nixpkgs.follows = "nixpkgs";
     };
+    nvfetcher = {
+      url = "github:berberman/nvfetcher";
+      inputs.nixpkgs.follows = "nixpkgs";
+    };
     tlaternet-webserver = {
       url = "git+https://gitea.tlater.net/tlaternet/tlaternet.git";
       inputs.nixpkgs.follows = "nixpkgs";
@@ -26,8 +30,6 @@
       url = "git+ssh://git@github.com/sonnenshift/battery-manager";
       inputs.nixpkgs.follows = "nixpkgs";
     };
-
-    nixpkgs-crowdsec.url = "github:tlater/nixpkgs/tlater/fix-crowdsec";
   };
 
   outputs =
@@ -35,6 +37,7 @@
       self,
       nixpkgs,
       sops-nix,
+      nvfetcher,
       deploy-rs,
       ...
     }@inputs:
@@ -100,16 +103,7 @@
       # Garbage collection root #
       ###########################
 
-      packages.${system} =
-        let
-          localPkgs = import ./pkgs { inherit pkgs; };
-        in
-        {
-          default = vm.config.system.build.vm;
-          crowdsec = pkgs.callPackage "${inputs.nixpkgs-crowdsec}/pkgs/by-name/cr/crowdsec/package.nix" { };
-          crowdsec-hub = localPkgs.crowdsec.hub;
-          crowdsec-firewall-bouncer = localPkgs.crowdsec.firewall-bouncer;
-        };
+      packages.${system}.default = vm.config.system.build.vm;
 
       ###################
       # Utility scripts #
@@ -127,21 +121,17 @@
             '').outPath;
         };
 
-        update-crowdsec-packages =
-          let
-            git = pkgs.lib.getExe pkgs.git;
-            nvfetcher = pkgs.lib.getExe pkgs.nvfetcher;
-          in
-          {
-            type = "app";
-            program =
-              (pkgs.writeShellScript "update-crowdsec-packages" ''
-                cd "$(${git} rev-parse --show-toplevel)"
-                cd ./pkgs/crowdsec
-                ${nvfetcher}
-                echo 'Remember to update the vendorHash of any go packages!'
-              '').outPath;
-          };
+        update-pkgs = {
+          type = "app";
+          program =
+            let
+              nvfetcher-bin = "${nvfetcher.packages.${system}.default}/bin/nvfetcher";
+            in
+            (pkgs.writeShellScript "update-pkgs" ''
+              cd "$(git rev-parse --show-toplevel)/pkgs"
+              ${nvfetcher-bin} -o _sources_pkgs -c nvfetcher.toml
+            '').outPath;
+        };
       };
 
       ###########################
diff --git a/modules/crowdsec/default.nix b/modules/crowdsec/default.nix
deleted file mode 100644
index 915ca0b..0000000
--- a/modules/crowdsec/default.nix
+++ /dev/null
@@ -1,361 +0,0 @@
-{
-  flake-inputs,
-  pkgs,
-  lib,
-  config,
-  ...
-}:
-let
-  cfg = config.security.crowdsec;
-  settingsFormat = pkgs.formats.yaml { };
-
-  crowdsec = flake-inputs.self.packages.${pkgs.system}.crowdsec;
-
-  hub = pkgs.fetchFromGitHub {
-    owner = "crowdsecurity";
-    repo = "hub";
-    rev = "7a3b4753f4577257c0cbeb8f8f90c7f17d2ae008";
-    hash = "sha256-HB4jHyhiO8gjBkLmpo6bDbwhfm5m5nAtNlKhDkZjt2I=";
-  };
-
-  cscli = pkgs.writeShellScriptBin "cscli" ''
-    export PATH="$PATH:${crowdsec}/bin/"
-
-    sudo=exec
-    if [ "$USER" != "crowdsec" ]; then
-        sudo='exec /run/wrappers/bin/sudo -u crowdsec'
-    fi
-
-    $sudo ${crowdsec}/bin/cscli "$@"
-  '';
-in
-{
-  imports = [ ./remediations ];
-
-  options.security.crowdsec =
-    let
-      inherit (lib.types)
-        nullOr
-        listOf
-        package
-        path
-        str
-        ;
-    in
-    {
-      enable = lib.mkEnableOption "crowdsec";
-
-      package = lib.mkOption {
-        type = package;
-        default = crowdsec;
-      };
-
-      stateDirectory = lib.mkOption {
-        type = path;
-        readOnly = true;
-
-        description = ''
-          The state directory of the crowdsec instance. Cannot be
-          changed, but is exposed for downstream use.
-        '';
-      };
-
-      settings = lib.mkOption {
-        inherit (settingsFormat) type;
-        default = { };
-
-        description = ''
-          The crowdsec configuration. Refer to
-          <https://docs.crowdsec.net/docs/next/configuration/crowdsec_configuration/>
-          for details on supported values.
-        '';
-      };
-
-      parserWhitelist = lib.mkOption {
-        type = listOf str;
-        default = [ ];
-        description = ''
-          Set of IP addresses to add to a parser-based whitelist.
-
-          Addresses can be specified either as plain IP addresses or
-          in CIDR notation.
-        '';
-      };
-
-      hubConfigurations = {
-        collections = lib.mkOption {
-          type = listOf str;
-          description = ''
-            List of pre-made crowdsec collections to install.
-          '';
-        };
-
-        scenarios = lib.mkOption {
-          type = listOf str;
-          description = ''
-            List of pre-made crowdsec scenarios to install.
-          '';
-        };
-
-        parsers = lib.mkOption {
-          type = listOf str;
-          description = ''
-            List of pre-made crowdsec parsers to install.
-          '';
-        };
-
-        postoverflows = lib.mkOption {
-          type = listOf str;
-          description = ''
-            List of pre-made crowdsec postoverflows to install.
-          '';
-        };
-
-        appsecConfigs = lib.mkOption {
-          type = listOf str;
-          description = ''
-            List of pre-made crowdsec appsec configurations to install.
-          '';
-        };
-
-        appsecRules = lib.mkOption {
-          type = listOf str;
-          description = ''
-            List of pre-made crowdsec appsec rules to install.
-          '';
-        };
-      };
-
-      centralApiCredentials = lib.mkOption {
-        type = nullOr path;
-        default = null;
-
-        description = ''
-          The API key to access crowdsec's central API - this is
-          required to access any of the shared blocklists.
-
-          Use of this feature is optional, entering no API key (the
-          default) turns all sharing or receiving of blocked IPs off.
-
-          Note that adding the API key by itself does not enable
-          sharing of blocked IPs with the central API. This limits the
-          types of blocklists this instance can access.
-
-          To also turn sharing blocked IPs on, set
-          `api.server.online_client.sharing = true;`.
-        '';
-      };
-
-      ctiApiKey = lib.mkOption {
-        type = nullOr path;
-        default = null;
-
-        description = ''
-          The API key for crowdsec's CTI offering.
-        '';
-      };
-    };
-
-  config = lib.mkIf cfg.enable {
-    # Set up default settings; anything that *shouldn't* be changed is
-    # set to the default priority so that users need to use
-    # `lib.mkForce`.
-    security.crowdsec = {
-      stateDirectory = "/var/lib/crowdsec";
-
-      settings = {
-        common = {
-          daemonize = true;
-          # The default logs to files, which isn't the preferred way
-          # on NixOS
-          log_media = "stdout";
-        };
-
-        config_paths = {
-          config_dir = "${cfg.stateDirectory}/config/";
-          data_dir = "${cfg.stateDirectory}/data/";
-          # This "config" file is intended to be written to using the
-          # cscli tool, so you can temporarily make it so rules don't
-          # do anything but log what they *would* do for
-          # experimentation.
-          simulation_path = "${cfg.stateDirectory}/config/simulation.yaml";
-
-          pattern_dir = lib.mkDefault "${cfg.package}/share/crowdsec/config/patterns";
-
-          hub_dir = hub;
-          index_path = "${hub}/.index.json";
-
-          # Integrations aren't supported for now
-          notification_dir = lib.mkDefault "/var/empty/";
-          plugin_dir = lib.mkDefault "/var/empty/";
-        };
-
-        crowdsec_service.acquisition_path = lib.mkDefault "${cfg.package}/share/crowdsec/config/acquis.yaml";
-
-        cscli = {
-          prometheus_uri = lib.mkDefault "127.0.0.1:6060";
-        };
-
-        db_config = {
-          type = lib.mkDefault "sqlite";
-          db_path = lib.mkDefault "${cfg.stateDirectory}/data/crowdsec.db";
-          use_wal = lib.mkDefault true;
-          flush = {
-            max_items = lib.mkDefault 5000;
-            max_age = lib.mkDefault "7d";
-          };
-        };
-
-        api = {
-          cti = {
-            enabled = cfg.ctiApiKey != null;
-            key = cfg.ctiApiKey;
-          };
-          client.credentials_path = "${cfg.stateDirectory}/local_credentials.yaml";
-          server = {
-            listen_uri = lib.mkDefault "127.0.0.1:8080";
-            profiles_path = lib.mkDefault "${cfg.package}/share/crowdsec/config/profiles.yaml";
-            console_path = lib.mkDefault "${cfg.package}/share/crowdsec/config/console.yaml";
-
-            online_client = {
-              # By default, we don't let crowdsec phone home, since
-              # this is usually within NixOS users' concerns.
-              #
-              # TODO: Enable when this option becomes available
-              # (1.6.4, current nixpkgs-unstable)
-              # sharing = lib.mkDefault false;
-              credentials_path = cfg.centralApiCredentials;
-            };
-          };
-        };
-
-        # We enable prometheus by default, since cscli relies on it
-        # for metrics
-        prometheus = {
-          enabled = lib.mkDefault true;
-          level = lib.mkDefault "full";
-          listen_addr = lib.mkDefault "127.0.0.1";
-          listen_port = lib.mkDefault 6060;
-        };
-      };
-    };
-
-    systemd.packages = [
-      cfg.package
-    ];
-
-    environment = {
-      systemPackages = [
-        # To add completions; sadly need to hand-roll this since
-        # neither `symlinkJoin` nor `buildEnv` have collision
-        # handling.
-        (pkgs.runCommandNoCCLocal "cscli" { } ''
-          mkdir -p $out
-          ln -s ${cscli}/bin $out/bin
-          ln -s ${cfg.package}/share $out/share
-        '')
-      ];
-
-      etc."crowdsec/config.yaml".source = settingsFormat.generate "crowdsec-settings.yaml" cfg.settings;
-    };
-
-    systemd = {
-      tmpfiles.settings."10-crowdsec" = {
-        "${cfg.stateDirectory}".d = {
-          user = "crowdsec";
-          group = "crowdsec";
-          mode = "0700";
-        };
-
-        # This must be created for the setup service to work
-        "${cfg.stateDirectory}/config".d = {
-          user = "crowdsec";
-          group = "crowdsec";
-          mode = "0700";
-        };
-
-        "${cfg.stateDirectory}/config/parsers".d = lib.mkIf (cfg.parserWhitelist != [ ]) {
-          user = "crowdsec";
-          group = "crowdsec";
-          mode = "0700";
-        };
-
-        "${cfg.stateDirectory}/config/parsers/s02-enrich".d = lib.mkIf (cfg.parserWhitelist != [ ]) {
-          user = "crowdsec";
-          group = "crowdsec";
-          mode = "0700";
-        };
-
-        "${cfg.stateDirectory}/config/parsers/s02-enrich/nixos-whitelist.yaml" =
-          lib.mkIf (cfg.parserWhitelist != [ ])
-            {
-              "L+".argument =
-                (settingsFormat.generate "crowdsec-nixos-whitelist.yaml" {
-                  name = "nixos/parser-whitelist";
-                  description = "Parser whitelist generated by the crowdsec NixOS module";
-                  whitelist = {
-                    reason = "Filtered by NixOS whitelist";
-                    ip = lib.lists.filter (ip: !(lib.hasInfix "/" ip)) cfg.parserWhitelist;
-                    cidr = lib.lists.filter (ip: lib.hasInfix "/" ip) cfg.parserWhitelist;
-                  };
-                }).outPath;
-            };
-      };
-
-      services = {
-        crowdsec-setup = {
-          # TODO(tlater): Depend on tmpfiles path for
-          # /var/lib/crowdsec/config
-          description = "Crowdsec database and config preparation";
-
-          script = ''
-            if [ ! -e '${cfg.settings.config_paths.simulation_path}' ]; then
-                cp '${cfg.package}/share/crowdsec/config/simulation.yaml' '${cfg.settings.config_paths.simulation_path}'
-            fi
-
-            if [ ! -e '${cfg.settings.api.client.credentials_path}' ]; then
-                ${cfg.package}/bin/cscli machines add --auto --file '${cfg.settings.api.client.credentials_path}'
-            fi
-          '';
-
-          serviceConfig = {
-            User = "crowdsec";
-            Group = "crowdsec";
-            StateDirectory = "crowdsec";
-
-            Type = "oneshot";
-            RemainAfterExit = true;
-          };
-        };
-
-        # Note that the service basics are already defined upstream
-        crowdsec = {
-          enable = true;
-
-          after = [ "crowdsec-setup.service" ];
-          bindsTo = [ "crowdsec-setup.service" ];
-          wantedBy = [ "multi-user.target" ];
-
-          serviceConfig = {
-            User = "crowdsec";
-            Group = "crowdsec";
-            SupplementaryGroups = [ "systemd-journal" ];
-
-            StateDirectory = "crowdsec";
-          };
-        };
-      };
-    };
-
-    users = {
-      users.crowdsec = {
-        isSystemUser = true;
-        home = cfg.stateDirectory;
-        group = "crowdsec";
-      };
-      groups = {
-        crowdsec = { };
-      };
-    };
-  };
-}
diff --git a/modules/crowdsec/remediations/cs-firewall-bouncer.nix b/modules/crowdsec/remediations/cs-firewall-bouncer.nix
deleted file mode 100644
index aa70552..0000000
--- a/modules/crowdsec/remediations/cs-firewall-bouncer.nix
+++ /dev/null
@@ -1,90 +0,0 @@
-{
-  flake-inputs,
-  pkgs,
-  lib,
-  config,
-  ...
-}:
-let
-  crowdsecCfg = config.security.crowdsec;
-  cfg = crowdsecCfg.remediationComponents.firewallBouncer;
-  settingsFormat = pkgs.formats.yaml { };
-  crowdsec-firewall-bouncer = flake-inputs.self.packages.${pkgs.system}.crowdsec-firewall-bouncer;
-in
-{
-  options.security.crowdsec.remediationComponents.firewallBouncer = {
-    enable = lib.mkEnableOption "cs-firewall-bouncer";
-
-    settings = lib.mkOption {
-      inherit (settingsFormat) type;
-      default = { };
-
-      description = ''
-        The bouncer configuration. Refer to
-        <https://docs.crowdsec.net/u/bouncers/firewall/> for details
-        on supported values.
-      '';
-    };
-  };
-
-  config = lib.mkIf cfg.enable {
-    security.crowdsec.remediationComponents.firewallBouncer.settings = {
-      mode = lib.mkDefault "${if config.networking.nftables.enable then "nftables" else "iptables"}";
-      log_mode = "stdout";
-      iptables_chains = [
-        "nixos-fw"
-      ];
-
-      # Don't let users easily override this; unfortunately we need to
-      # set up this key through substitution at runtime.
-      api_key = lib.mkForce "\${API_KEY}";
-      api_url = lib.mkDefault "http://${crowdsecCfg.settings.api.server.listen_uri}";
-    };
-
-    systemd = {
-      packages = [ crowdsec-firewall-bouncer ];
-
-      services = {
-        crowdsec-firewall-bouncer-setup = {
-          description = "Crowdsec firewall bouncer config preparation";
-          script = ''
-            if [ ! -e '${crowdsecCfg.stateDirectory}/firewall_bouncer_credentials.yaml' ]; then
-                ${crowdsecCfg.package}/bin/cscli -oraw bouncers add "cs-firewall-bouncer-$(${pkgs.coreutils}/bin/date +%s)" > \
-                  ${crowdsecCfg.stateDirectory}/firewall_bouncer_credentials.yaml
-            fi
-
-            # Stdout redirection is deliberately used to forcibly
-            # overwrite the file if it exists
-            API_KEY="$(<${crowdsecCfg.stateDirectory}/firewall_bouncer_credentials.yaml)" \
-              ${lib.getExe pkgs.envsubst} \
-              -i ${settingsFormat.generate "crowdsec-firewall-bouncer.yaml" cfg.settings} \
-              > /var/lib/crowdsec/config/crowdsec-firewall-bouncer.yaml
-          '';
-
-          serviceConfig = {
-            User = "crowdsec";
-            Group = "crowdsec";
-
-            Type = "oneshot";
-            RemainAfterExit = true;
-          };
-        };
-
-        crowdsec-firewall-bouncer = {
-          enable = true;
-
-          after = [ "crowdsec-firewall-bouncer-setup.service" ];
-          bindsTo = [ "crowdsec-firewall-bouncer-setup.service" ];
-          requiredBy = [ "crowdsec.service" ];
-
-          path =
-            lib.optionals (cfg.settings.mode == "ipset" || cfg.settings.mode == "iptables") [
-              pkgs.ipset
-            ]
-            ++ lib.optional (cfg.settings.mode == "iptables") pkgs.iptables
-            ++ lib.optional (cfg.settings.mode == "nftables") pkgs.nftables;
-        };
-      };
-    };
-  };
-}
diff --git a/modules/crowdsec/remediations/default.nix b/modules/crowdsec/remediations/default.nix
deleted file mode 100644
index 7df6ade..0000000
--- a/modules/crowdsec/remediations/default.nix
+++ /dev/null
@@ -1,5 +0,0 @@
-{
-  imports = [
-    ./cs-firewall-bouncer.nix
-  ];
-}
diff --git a/modules/default.nix b/modules/default.nix
index 89f1752..e1db4cc 100644
--- a/modules/default.nix
+++ b/modules/default.nix
@@ -1,6 +1 @@
-{
-  imports = [
-    ./crowdsec
-    ./nginxExtensions.nix
-  ];
-}
+{ imports = [ ./nginxExtensions.nix ]; }
diff --git a/pkgs/_sources_pkgs/generated.json b/pkgs/_sources_pkgs/generated.json
new file mode 100644
index 0000000..cec5a92
--- /dev/null
+++ b/pkgs/_sources_pkgs/generated.json
@@ -0,0 +1,22 @@
+{
+    "prometheus-fail2ban-exporter": {
+        "cargoLocks": null,
+        "date": null,
+        "extract": null,
+        "name": "prometheus-fail2ban-exporter",
+        "passthru": null,
+        "pinned": false,
+        "src": {
+            "deepClone": false,
+            "fetchSubmodules": false,
+            "leaveDotGit": false,
+            "name": null,
+            "rev": "v0.10.1",
+            "sha256": "sha256-zGEhDy3uXIbvx4agSA8Mx7bRtiZZtoDZGbNbHc9L+yI=",
+            "sparseCheckout": [],
+            "type": "git",
+            "url": "https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter"
+        },
+        "version": "v0.10.1"
+    }
+}
\ No newline at end of file
diff --git a/pkgs/_sources_pkgs/generated.nix b/pkgs/_sources_pkgs/generated.nix
new file mode 100644
index 0000000..95fd75e
--- /dev/null
+++ b/pkgs/_sources_pkgs/generated.nix
@@ -0,0 +1,17 @@
+# This file was generated by nvfetcher, please do not modify it manually.
+{ fetchgit, fetchurl, fetchFromGitHub, dockerTools }:
+{
+  prometheus-fail2ban-exporter = {
+    pname = "prometheus-fail2ban-exporter";
+    version = "v0.10.1";
+    src = fetchgit {
+      url = "https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter";
+      rev = "v0.10.1";
+      fetchSubmodules = false;
+      deepClone = false;
+      leaveDotGit = false;
+      sparseCheckout = [ ];
+      sha256 = "sha256-zGEhDy3uXIbvx4agSA8Mx7bRtiZZtoDZGbNbHc9L+yI=";
+    };
+  };
+}
diff --git a/pkgs/crowdsec/_sources/generated.json b/pkgs/crowdsec/_sources/generated.json
deleted file mode 100644
index 8485779..0000000
--- a/pkgs/crowdsec/_sources/generated.json
+++ /dev/null
@@ -1,42 +0,0 @@
-{
-    "crowdsec-firewall-bouncer": {
-        "cargoLocks": null,
-        "date": null,
-        "extract": null,
-        "name": "crowdsec-firewall-bouncer",
-        "passthru": null,
-        "pinned": false,
-        "src": {
-            "deepClone": false,
-            "fetchSubmodules": false,
-            "leaveDotGit": false,
-            "name": null,
-            "owner": "crowdsecurity",
-            "repo": "cs-firewall-bouncer",
-            "rev": "v0.0.31",
-            "sha256": "sha256-59MWll8v00CF4WA53gjHZSTFc8hpYaHENg9O7LgTCrA=",
-            "type": "github"
-        },
-        "version": "v0.0.31"
-    },
-    "crowdsec-hub": {
-        "cargoLocks": null,
-        "date": "2025-01-30",
-        "extract": null,
-        "name": "crowdsec-hub",
-        "passthru": null,
-        "pinned": false,
-        "src": {
-            "deepClone": false,
-            "fetchSubmodules": false,
-            "leaveDotGit": false,
-            "name": null,
-            "owner": "crowdsecurity",
-            "repo": "hub",
-            "rev": "8f102f5ac79af59d3024ca2771b65ec87411ac02",
-            "sha256": "sha256-8K1HkBg0++Au1dr2KMrl9b2ruqXdo+vqWngOCwL11Mo=",
-            "type": "github"
-        },
-        "version": "8f102f5ac79af59d3024ca2771b65ec87411ac02"
-    }
-}
\ No newline at end of file
diff --git a/pkgs/crowdsec/_sources/generated.nix b/pkgs/crowdsec/_sources/generated.nix
deleted file mode 100644
index 6f845ec..0000000
--- a/pkgs/crowdsec/_sources/generated.nix
+++ /dev/null
@@ -1,27 +0,0 @@
-# This file was generated by nvfetcher, please do not modify it manually.
-{ fetchgit, fetchurl, fetchFromGitHub, dockerTools }:
-{
-  crowdsec-firewall-bouncer = {
-    pname = "crowdsec-firewall-bouncer";
-    version = "v0.0.31";
-    src = fetchFromGitHub {
-      owner = "crowdsecurity";
-      repo = "cs-firewall-bouncer";
-      rev = "v0.0.31";
-      fetchSubmodules = false;
-      sha256 = "sha256-59MWll8v00CF4WA53gjHZSTFc8hpYaHENg9O7LgTCrA=";
-    };
-  };
-  crowdsec-hub = {
-    pname = "crowdsec-hub";
-    version = "8f102f5ac79af59d3024ca2771b65ec87411ac02";
-    src = fetchFromGitHub {
-      owner = "crowdsecurity";
-      repo = "hub";
-      rev = "8f102f5ac79af59d3024ca2771b65ec87411ac02";
-      fetchSubmodules = false;
-      sha256 = "sha256-8K1HkBg0++Au1dr2KMrl9b2ruqXdo+vqWngOCwL11Mo=";
-    };
-    date = "2025-01-30";
-  };
-}
diff --git a/pkgs/crowdsec/default.nix b/pkgs/crowdsec/default.nix
deleted file mode 100644
index 66faac3..0000000
--- a/pkgs/crowdsec/default.nix
+++ /dev/null
@@ -1,9 +0,0 @@
-{ pkgs }:
-let
-  sources = pkgs.callPackage ./_sources/generated.nix { };
-  callPackage = pkgs.lib.callPackageWith (pkgs // { inherit sources; });
-in
-{
-  hub = callPackage ./hub.nix { };
-  firewall-bouncer = callPackage ./firewall-bouncer.nix { };
-}
diff --git a/pkgs/crowdsec/firewall-bouncer.nix b/pkgs/crowdsec/firewall-bouncer.nix
deleted file mode 100644
index 86370c4..0000000
--- a/pkgs/crowdsec/firewall-bouncer.nix
+++ /dev/null
@@ -1,26 +0,0 @@
-{
-  lib,
-  sources,
-  buildGoModule,
-  envsubst,
-  coreutils,
-}:
-let
-  envsubstBin = lib.getExe envsubst;
-in
-buildGoModule {
-  inherit (sources.crowdsec-firewall-bouncer) pname version src;
-
-  vendorHash = "sha256-7Jxvg8UEjUxnIz1llvXyI2AefJ31OVdNzhWD/C8wU/Y=";
-
-  postInstall = ''
-    mkdir -p $out/lib/systemd/system
-
-    CFG=/var/lib/crowdsec/config BIN=$out/bin/cs-firewall-bouncer ${envsubstBin} \
-      -i ./config/crowdsec-firewall-bouncer.service \
-      -o $out/lib/systemd/system/crowdsec-firewall-bouncer.service
-
-    substituteInPlace $out/lib/systemd/system/crowdsec-firewall-bouncer.service \
-      --replace-fail /bin/sleep ${coreutils}/bin/sleep
-  '';
-}
diff --git a/pkgs/crowdsec/hub.nix b/pkgs/crowdsec/hub.nix
deleted file mode 100644
index d057ca8..0000000
--- a/pkgs/crowdsec/hub.nix
+++ /dev/null
@@ -1,4 +0,0 @@
-{
-  sources,
-}:
-sources.crowdsec-hub.src
diff --git a/pkgs/crowdsec/nvfetcher.toml b/pkgs/crowdsec/nvfetcher.toml
deleted file mode 100644
index 2287dba..0000000
--- a/pkgs/crowdsec/nvfetcher.toml
+++ /dev/null
@@ -1,7 +0,0 @@
-[crowdsec-hub]
-src.git = "https://github.com/crowdsecurity/hub.git"
-fetch.github = "crowdsecurity/hub"
-
-[crowdsec-firewall-bouncer]
-src.github = "crowdsecurity/cs-firewall-bouncer"
-fetch.github = "crowdsecurity/cs-firewall-bouncer"
diff --git a/pkgs/default.nix b/pkgs/default.nix
index 0e5de7a..131282d 100644
--- a/pkgs/default.nix
+++ b/pkgs/default.nix
@@ -1,5 +1,10 @@
 { pkgs }:
+let
+  inherit (pkgs) callPackage;
+in
 {
-  crowdsec = import ./crowdsec { inherit pkgs; };
-  starbound = pkgs.callPackage ./starbound { };
+  starbound = callPackage ./starbound { };
+  prometheus-fail2ban-exporter = callPackage ./prometheus/fail2ban-exporter.nix {
+    sources = pkgs.callPackage ./_sources_pkgs/generated.nix { };
+  };
 }
diff --git a/pkgs/nvfetcher.toml b/pkgs/nvfetcher.toml
new file mode 100644
index 0000000..d0dfbe5
--- /dev/null
+++ b/pkgs/nvfetcher.toml
@@ -0,0 +1,3 @@
+[prometheus-fail2ban-exporter]
+src.manual = "v0.10.1" # No gitlab support in nvfetcher
+fetch.git = "https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter"
diff --git a/pkgs/prometheus/fail2ban-exporter.nix b/pkgs/prometheus/fail2ban-exporter.nix
new file mode 100644
index 0000000..dc22b6c
--- /dev/null
+++ b/pkgs/prometheus/fail2ban-exporter.nix
@@ -0,0 +1,5 @@
+{ buildGoModule, sources }:
+buildGoModule {
+  inherit (sources.prometheus-fail2ban-exporter) pname src version;
+  vendorHash = "sha256-5o8p5p0U/c0WAIV5dACnWA3ThzSh2tt5LIFMb59i9GY=";
+}