{ config, pkgs, ... }: { networking.firewall = { enable = true; # SSH is NOT public — only accessible via Tailscale (trustedInterfaces) allowedTCPPorts = [ 80 # HTTP (Caddy ACME + redirect) 443 # HTTPS ]; logReversePathDrops = true; # Kernel-level DDoS protection via iptables # These rules fire BEFORE Caddy even sees the packet, so they're very cheap. extraCommands = '' # ── SYN flood protection ── # Limit new TCP connections to 30/sec per source IP (burst 50). # Legitimate browsers open ~6 connections; scrapers open hundreds. iptables -N RATE_LIMIT 2>/dev/null || iptables -F RATE_LIMIT iptables -A RATE_LIMIT -m hashlimit \ --hashlimit-name syn_flood \ --hashlimit-above 30/sec \ --hashlimit-burst 50 \ --hashlimit-mode srcip \ --hashlimit-htable-expire 300000 \ -j DROP iptables -A RATE_LIMIT -j RETURN # Hook into INPUT chain for new TCP SYN packets to HTTP/HTTPS iptables -C INPUT -p tcp --syn -m multiport --dports 80,443 -j RATE_LIMIT 2>/dev/null || \ iptables -I INPUT -p tcp --syn -m multiport --dports 80,443 -j RATE_LIMIT # ── Connection limit ── # Max 200 concurrent connections per source IP to HTTP/HTTPS. # A single browser uses ~6-10; a scraper farm uses thousands. iptables -C INPUT -p tcp -m multiport --dports 80,443 -m connlimit --connlimit-above 200 --connlimit-mask 32 -j DROP 2>/dev/null || \ iptables -I INPUT -p tcp -m multiport --dports 80,443 -m connlimit --connlimit-above 200 --connlimit-mask 32 -j DROP # ── Same for IPv6 ── ip6tables -N RATE_LIMIT 2>/dev/null || ip6tables -F RATE_LIMIT ip6tables -A RATE_LIMIT -m hashlimit \ --hashlimit-name syn_flood_v6 \ --hashlimit-above 30/sec \ --hashlimit-burst 50 \ --hashlimit-mode srcip \ --hashlimit-htable-expire 300000 \ -j DROP ip6tables -A RATE_LIMIT -j RETURN ip6tables -C INPUT -p tcp --syn -m multiport --dports 80,443 -j RATE_LIMIT 2>/dev/null || \ ip6tables -I INPUT -p tcp --syn -m multiport --dports 80,443 -j RATE_LIMIT ip6tables -C INPUT -p tcp -m multiport --dports 80,443 -m connlimit --connlimit-above 200 --connlimit-mask 64 -j DROP 2>/dev/null || \ ip6tables -I INPUT -p tcp -m multiport --dports 80,443 -m connlimit --connlimit-above 200 --connlimit-mask 64 -j DROP ''; # Clean up custom chains on stop extraStopCommands = '' iptables -D INPUT -p tcp --syn -m multiport --dports 80,443 -j RATE_LIMIT 2>/dev/null || true iptables -D INPUT -p tcp -m multiport --dports 80,443 -m connlimit --connlimit-above 200 --connlimit-mask 32 -j DROP 2>/dev/null || true iptables -F RATE_LIMIT 2>/dev/null || true iptables -X RATE_LIMIT 2>/dev/null || true ip6tables -D INPUT -p tcp --syn -m multiport --dports 80,443 -j RATE_LIMIT 2>/dev/null || true ip6tables -D INPUT -p tcp -m multiport --dports 80,443 -m connlimit --connlimit-above 200 --connlimit-mask 64 -j DROP 2>/dev/null || true ip6tables -F RATE_LIMIT 2>/dev/null || true ip6tables -X RATE_LIMIT 2>/dev/null || true ''; }; services.openssh = { enable = true; settings = { PasswordAuthentication = false; KbdInteractiveAuthentication = false; PermitRootLogin = "prohibit-password"; X11Forwarding = false; MaxAuthTries = 3; }; # Do NOT open firewall — SSH only over Tailscale openFirewall = false; }; # Fail2ban for HTTP abuse (not SSH — SSH isn't public) services.fail2ban = { enable = true; maxretry = 5; bantime = "1h"; bantime-increment = { enable = true; maxtime = "48h"; }; }; boot.kernel.sysctl = { # Reverse path filtering "net.ipv4.conf.all.rp_filter" = 1; "net.ipv4.conf.default.rp_filter" = 1; # Ignore broadcast pings "net.ipv4.icmp_echo_ignore_broadcasts" = 1; # Don't accept or send redirects "net.ipv4.conf.all.accept_redirects" = 0; "net.ipv6.conf.all.accept_redirects" = 0; "net.ipv4.conf.all.send_redirects" = 0; # Reject source-routed packets "net.ipv4.conf.all.accept_source_route" = 0; "net.ipv6.conf.all.accept_source_route" = 0; # SYN flood protection (kernel-level SYN cookies) "net.ipv4.tcp_syncookies" = 1; "net.ipv4.tcp_max_syn_backlog" = 4096; # Reduce TIME_WAIT accumulation from abusive connections "net.ipv4.tcp_fin_timeout" = 15; # Connection tracking table size (default 65536 is too small under DDoS) "net.netfilter.nf_conntrack_max" = 262144; }; }