❄️ Dotfiles for our NixOS system configuration.
1{ pkgs, ... }:
2
3{
4 age.secrets.abuseipdb = {
5 file = ../../secrets/abuseipdb.age;
6 mode = "600";
7 };
8
9 services.fail2ban = {
10 enable = true;
11
12 # Include curl for making HTTP requests to AbuseIPDB
13 extraPackages = [ pkgs.curl ];
14
15 # Globally applicable settings for all jails
16 daemonSettings = {
17 Definition = {
18 # How long to keep an IP in the ban list (in seconds)
19 # 1 day = 86400 seconds
20 bantime = "86400";
21
22 # How far back to look for failures (in seconds)
23 # 1 hour = 3600 seconds
24 findtime = "3600";
25
26 # Number of failures before banning
27 maxretry = 3;
28
29 # Allow fail2ban to write to syslog
30 logtarget = "SYSLOG";
31 };
32 };
33
34 # Ignore local networks and trusted services
35 ignoreIP = [
36 "127.0.0.1/8"
37 "::1"
38 "100.64.0.0/10" # Tailscale IP range
39 ];
40
41 # Jails for protecting various services
42 jails = {
43 # SSH protection - monitor failed login attempts using systemd journal
44 sshd.settings = {
45 enabled = true;
46 port = "ssh";
47 filter = "sshd";
48 backend = "systemd";
49 maxretry = 5;
50 findtime = "3600";
51 bantime = "86400";
52 action = "iptables-multiport[name=SSH, port='ssh'] abuseipdb-agenix[abuseipdb_category='18,22']";
53 };
54
55 # Caddy HTTP/HTTPS protection - monitor for repeated 4xx/5xx errors
56 caddy-http.settings = {
57 enabled = true;
58 port = "http,https";
59 filter = "caddy-http";
60 logpath = "/var/log/caddy/access-*.log";
61 backend = "auto";
62 maxretry = 10;
63 findtime = "600";
64 bantime = "3600";
65 action = "iptables-multiport[name=Caddy, port='http,https'] abuseipdb-agenix[abuseipdb_category='21']";
66 };
67
68 # Rate-based protection - ban on excessive requests
69 caddy-ratelimit.settings = {
70 enabled = true;
71 port = "http,https";
72 filter = "caddy-ratelimit";
73 logpath = "/var/log/caddy/access-*.log";
74 backend = "auto";
75 maxretry = 50;
76 findtime = "60";
77 bantime = "1800";
78 action = "iptables-multiport[name=Caddy-RateLimit, port='http,https'] abuseipdb-agenix[abuseipdb_category='21']";
79 };
80 };
81 };
82
83 # Custom filters for Fail2Ban
84 environment.etc = {
85 # Caddy HTTP error monitoring filter
86 "fail2ban/filter.d/caddy-http.conf".text = ''
87 [Definition]
88 failregex = ^<HOST> -.*" (?:400|401|403|404|405|429|500|502|503|504) .*$
89 ignoreregex =
90 '';
91
92 # Caddy rate limiting filter - detects repeated requests within short timeframe
93 "fail2ban/filter.d/caddy-ratelimit.conf".text = ''
94 [Definition]
95 failregex = ^<HOST> -.*" \d{3} .*$
96 ignoreregex =
97 '';
98
99 # Custom abuseipdb action that reads API key from file
100 "fail2ban/action.d/abuseipdb-agenix.conf".text = ''
101 [Definition]
102 # Report IP to AbuseIPDB, reading API key from Agenix secret file
103 # Uses double quotes to allow shell expansion of $(cat /run/agenix/abuseipdb)
104 # Sleep 12 seconds to respect AbuseIPDB rate limit (~5 requests per minute)
105 # Note: Don't use <matches> - fail2ban's wrapper causes issues with multiline content
106 # Don't use --fail so 429 rate limit errors don't mark action as failed
107 actionban = sleep 12; curl 'https://api.abuseipdb.com/api/v2/report' -H 'Accept: application/json' -H "Key: $(cat /run/agenix/abuseipdb)" --data-urlencode 'ip=<ip>' --data 'categories=<abuseipdb_category>' > /dev/null 2>&1
108
109 actionstart =
110 actionstop =
111 actioncheck =
112 actionunban =
113
114 [Init]
115 abuseipdb_category = 18
116 '';
117 };
118
119 # Ensure the log directory exists
120 systemd.tmpfiles.rules = [
121 "d /var/log/fail2ban 0755 root root -"
122 ];
123}