From c0f2c97e8af7a1a0ebb800c03406078f2bdcee25 Mon Sep 17 00:00:00 2001 From: Kieran Klukas Date: Wed, 5 Aug 2026 01:55:30 -0400 Subject: [PATCH] feat: add fail2ban --- machines/terebithia/default.nix | 5 +++ modules/nixos/system/fail2ban.nix | 65 +++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 modules/nixos/system/fail2ban.nix diff --git a/machines/terebithia/default.nix b/machines/terebithia/default.nix index 5ec2d75..195d582 100644 --- a/machines/terebithia/default.nix +++ b/machines/terebithia/default.nix @@ -280,6 +280,11 @@ rejectPackets = true; }; + # Public IP, so sshd takes constant scanner traffic (~25k failed auths/week + # from ~380 distinct hosts). Key-only auth already makes those unwinnable; + # this just stops them burning CPU and filling the journal. + atelier.security.fail2ban.enable = true; + services.tailscale = { enable = true; useRoutingFeatures = "client"; diff --git a/modules/nixos/system/fail2ban.nix b/modules/nixos/system/fail2ban.nix new file mode 100644 index 0000000..3dbf2a5 --- /dev/null +++ b/modules/nixos/system/fail2ban.nix @@ -0,0 +1,65 @@ +{ + lib, + config, + ... +}: +let + cfg = config.atelier.security.fail2ban; +in +{ + options.atelier.security.fail2ban = { + enable = lib.mkEnableOption '' + fail2ban with an sshd jail. + + Worth enabling on any host with a public IP. It is not a substitute for + key-only auth -- it cannot stop an attack that key-only auth already + stops -- but it cuts the constant scanner traffic that otherwise fills + the journal and burns CPU on doomed handshakes. + ''; + + ignoreIP = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ + "127.0.0.0/8" + "::1" + # Tailscale CGNAT. Never ban over the tailnet -- it is the way back in + # if a ban ever goes wrong. + "100.64.0.0/10" + "fd7a:115c:a1e0::/48" + # RFC1918, so a flapping LAN client cannot lock the host out. + "10.0.0.0/8" + "172.16.0.0/12" + "192.168.0.0/16" + ]; + description = "CIDRs fail2ban will never ban."; + }; + }; + + config = lib.mkIf cfg.enable { + services.fail2ban = { + enable = true; + inherit (cfg) ignoreIP; + + # Repeat offenders earn progressively longer bans, so the handful of + # hosts responsible for most of the traffic drop off quickly. + bantime = "1h"; + bantime-increment = { + enable = true; + formula = "ban.Time * math.exp(float(ban.Count+1)*banFactor)/math.exp(1*banFactor)"; + maxtime = "168h"; # one week ceiling + overalljails = true; + }; + + jails.sshd.settings = { + enabled = true; + # Match sshd's real port. `mode = normal` catches invalid-user and + # failed-auth lines, which is what scanners generate against a + # key-only host. + mode = "normal"; + port = "ssh"; + maxretry = 5; + findtime = "10m"; + }; + }; + }; +} -- 2.51.2