diff --git a/nixos/hosts/framework-server/default.nix b/nixos/hosts/framework-server/default.nix
index b7ed9c5e..fd58a129 100644
--- a/nixos/hosts/framework-server/default.nix
+++ b/nixos/hosts/framework-server/default.nix
@@ -14,6 +14,7 @@
./wireguard.nix
./cron.nix
./firewall.nix
+ ./fail2ban/traefik.nix
];
environment.systemPackages = [
diff --git a/nixos/hosts/framework-server/fail2ban/traefik.nix b/nixos/hosts/framework-server/fail2ban/traefik.nix
new file mode 100644
index 00000000..c39b2ad3
--- /dev/null
+++ b/nixos/hosts/framework-server/fail2ban/traefik.nix
@@ -0,0 +1,39 @@
+{ ... }: {
+
+ # References:
+ # https://blog.lrvt.de/configuring-fail2ban-with-traefik/
+ # https://nixos.wiki/wiki/Fail2ban#Extending_Fail2ban
+
+ services.fail2ban.jails = {
+ traefik-general-forceful-browsing = {
+ settings = {
+ enabled = true;
+ filter = "traefik-general-forceful-browsingo";
+ action = ''action-ban-docker-forceful-browsing'';
+ logpath = "/var/log/traefik/access.log";
+ backend = "auto";
+ findtime = 600;
+ bantime = 600;
+ maxretry = 5;
+ };
+ };
+ };
+
+ environment.etc= {
+ "fail2ban/filter.d/raefik-general-forceful-browsing.conf".text = pkgs.lib.mkDefault (pkgs.lib.mkAfter ''
+ [INCLUDES]
+
+ [Definition]
+
+ # fail regex based on traefik JSON access logs with enabled user agent logging
+ failregex = ^{"ClientAddr":".*","ClientHost":"","ClientPort":".*","ClientUsername":".*","DownstreamContentSize":.*,"DownstreamStatus":.*,"Duration":.*,"OriginContentSize":.*,"OriginDuration":.*,"OriginStatus":(405|404|403|402|401),"Overhead":.*,"RequestAddr":".*","RequestContentSize":.*,"RequestCount":.*,"RequestHost":".*","RequestMethod":".*","RequestPath":".*","RequestPort":".*","RequestProtocol":".*","RequestScheme":".*","RetryAttempts":.*,.*"StartLocal":".*","StartUTC":".*","TLSCipher":".*","TLSVersion":".*","entryPointName":".*","level":".*","msg":".*",("request_User-Agent":".*",){0,1}?"time":".*"}$
+
+ # custom date pattern for traefik JSON access logs
+ # based on https://github.com/fail2ban/fail2ban/issues/2558#issuecomment-546738270
+ datepattern = "StartLocal"\s*:\s*"%%Y-%%m-%%d[T]%%H:%%M:%%S\.%%f\d*(%%z)?",
+
+ # ignore common errors like missing media files or JS/CSS/TXT/ICO stuff
+ ignoreregex = ^{"ClientAddr":".*","ClientHost":"","ClientPort":".*","ClientUsername":".*","DownstreamContentSize":.*,"DownstreamStatus":.*,"Duration":.*,"OriginContentSize":.*,"OriginDuration":.*,"OriginStatus":(405|404|403|402|401),"Overhead":.*,"RequestAddr":".*","RequestContentSize":.*,"RequestCount":.*,"RequestHost":".*","RequestMethod":".*","RequestPath":".*(\.png|\.txt|\.jpg|\.ico|\.js|\.css|\.ttf|\.woff|\.woff2)(/)*?","RequestPort":".*","RequestProtocol":".*","RequestScheme":".*","RetryAttempts":.*,.*"StartLocal":".*","StartUTC":".*","TLSCipher":".*","TLSVersion":".*","entryPointName":".*","level":".*","msg":".*",("request_User-Agent":".*",){0,1}?"time":".*"}$
+ '');
+ };
+}