Tools: UFW, fail2ban, and Banning Repeat Offenders

Tools: UFW, fail2ban, and Banning Repeat Offenders

UFW Beyond the Basics

Rule Ordering

Logging

How fail2ban Works

A fail2ban Jail for Caddy

Escalate With the Recidive Jail

CrowdSec: Collective Intelligence

Verify Your Jails

What's next This is part 3 of the Production Linux series. Previous: SSH Hardening. UFW blocks ports. fail2ban blocks behavior. Together they form your server's intrusion response layer — UFW narrows the attack surface, fail2ban watches the traffic that gets through and bans the IPs that misbehave. This post covers UFW rule ordering, building a fail2ban jail for Caddy's JSON access logs, and escalating repeat offenders to a week-long all-ports block with the recidive jail. If UFW isn't installed, add it: Install the package. On most Ubuntu VPS images it's already present. Allow SSH before enabling UFW. This is the most common mistake. If you enable UFW without allowing SSH first, you will lock yourself out of the server. These three rules cover SSH, HTTP, and HTTPS. Add any other ports your services need before the next step. Enabling UFW applies the default policy — deny incoming, allow outgoing — and activates your rules. UFW evaluates rules in order and stops at the first match. Check your current rules with their index numbers: This shows each rule prefixed with a number, which you'll need for deletions. To delete a rule, pass its number: UFW removes the rule at position 3 and renumbers the rest. UFW's default logging is sparse. Raise it to see blocked connection attempts: Logs go to /var/log/ufw.log. The medium level records blocked packets with source IP, destination port, and protocol — enough detail to spot scan patterns without flooding your disk. fail2ban watches log files through a pipeline: log → filter → jail → action. The second command ensures fail2ban starts on boot and is running now. Caddy writes structured JSON access logs. The filter below extracts the client IP and timestamp from that format. datepattern tells fail2ban where to find the timestamp — Caddy's "ts" field holds a Unix epoch float. failregex matches any log line where the client triggered a 400–405, 429, or 5xx response. <HOST> is fail2ban's placeholder for the IP it will extract and ban. Now create the jail that uses this filter: logpath uses a glob to cover every app's log directory under /home/deployer/. backend = auto lets fail2ban choose the most efficient log-watching method for your system. With these settings, an IP hitting 20 errors in 10 minutes earns a one-hour ban on ports 80 and 443. Test your filter against a real log file before reloading: This runs the regex against the log and reports how many lines match, how many were skipped, and the IPs it would have banned. Fix the filter until you see matches before deploying. Reload fail2ban to apply the new jail: A one-hour ban on web ports doesn't discourage determined attackers — they rotate IPs or wait it out. The recidive jail watches fail2ban's own log and escalates IPs that keep getting banned. banaction = nftables[type=allports] blocks every port, not just 80 and 443. nftables is the modern Linux firewall backend; iptables is the legacy compatibility layer and should be avoided on current systems. bantime = 604800 is seven days in seconds. An IP that triggers 5 separate bans within 24 hours gets blocked on all ports for a week. This jail requires no custom filter — it reads fail2ban's own log format out of the box. CrowdSec takes a different approach. Instead of reacting to behavior on your server, it uses a crowd-sourced blocklist built from reports across all CrowdSec users. When an IP attacks one server, every server in the network can block it proactively. For a single VPS, fail2ban is simpler and has lower resource requirements. CrowdSec becomes compelling when you manage multiple servers — the shared intelligence means a scanner that hits one box gets blocked on all of them. It also ships with pre-built parsers for Caddy, Nginx, SSH, and dozens of other services. CrowdSec isn't a drop-in fail2ban replacement; the two can run side by side. A common pattern is to run fail2ban for reactive banning and subscribe to CrowdSec's blocklist for proactive blocking. If you're scaling beyond a single VPS, it's worth evaluating. Check that fail2ban loaded your jails: This lists every active jail by name. Inspect a specific jail: The output shows the filter in use, current ban count, and the list of currently banned IPs. To see recent ban activity in the log: This filters the fail2ban log to show only ban and unban events, newest last. Confirm nftables is enforcing the recidive bans: If the recidive jail has fired, you'll see chains and rules named after fail2ban in the output. The next post covers Docker security on a shared VPS — running containers as non-root, isolating networks, and limiting what a compromised container can reach. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

$ -weight: 500;">apt -weight: 500;">install ufw -weight: 500;">apt -weight: 500;">install ufw -weight: 500;">apt -weight: 500;">install ufw ufw allow OpenSSH ufw allow 80/tcp ufw allow 443/tcp ufw allow OpenSSH ufw allow 80/tcp ufw allow 443/tcp ufw allow OpenSSH ufw allow 80/tcp ufw allow 443/tcp ufw -weight: 500;">status numbered ufw -weight: 500;">status numbered ufw -weight: 500;">status numbered ufw delete 3 ufw delete 3 ufw delete 3 ufw logging medium ufw logging medium ufw logging medium -weight: 500;">apt -weight: 500;">install fail2ban -weight: 500;">systemctl -weight: 500;">enable --now fail2ban -weight: 500;">apt -weight: 500;">install fail2ban -weight: 500;">systemctl -weight: 500;">enable --now fail2ban -weight: 500;">apt -weight: 500;">install fail2ban -weight: 500;">systemctl -weight: 500;">enable --now fail2ban # /etc/fail2ban/filter.d/caddy-security.conf [INCLUDES] before = common.conf [Definition] datepattern = "ts":<F-TIME>%%s</F-TIME> failregex = ^.*"remote_ip":"<HOST>".*"-weight: 500;">status":(?:40[0-5]|429|5\d\d).*$ ignoreregex = # /etc/fail2ban/filter.d/caddy-security.conf [INCLUDES] before = common.conf [Definition] datepattern = "ts":<F-TIME>%%s</F-TIME> failregex = ^.*"remote_ip":"<HOST>".*"-weight: 500;">status":(?:40[0-5]|429|5\d\d).*$ ignoreregex = # /etc/fail2ban/filter.d/caddy-security.conf [INCLUDES] before = common.conf [Definition] datepattern = "ts":<F-TIME>%%s</F-TIME> failregex = ^.*"remote_ip":"<HOST>".*"-weight: 500;">status":(?:40[0-5]|429|5\d\d).*$ ignoreregex = # /etc/fail2ban/jail.d/caddy.conf [caddy-security] enabled = true port = http,https filter = caddy-security logpath = /home/deployer/*/log/access.log backend = auto maxretry = 20 findtime = 600 bantime = 3600 # /etc/fail2ban/jail.d/caddy.conf [caddy-security] enabled = true port = http,https filter = caddy-security logpath = /home/deployer/*/log/access.log backend = auto maxretry = 20 findtime = 600 bantime = 3600 # /etc/fail2ban/jail.d/caddy.conf [caddy-security] enabled = true port = http,https filter = caddy-security logpath = /home/deployer/*/log/access.log backend = auto maxretry = 20 findtime = 600 bantime = 3600 fail2ban-regex /home/deployer/myapp/log/access.log /etc/fail2ban/filter.d/caddy-security.conf fail2ban-regex /home/deployer/myapp/log/access.log /etc/fail2ban/filter.d/caddy-security.conf fail2ban-regex /home/deployer/myapp/log/access.log /etc/fail2ban/filter.d/caddy-security.conf fail2ban-client reload fail2ban-client reload fail2ban-client reload # /etc/fail2ban/jail.d/recidive.conf [recidive] enabled = true logpath = /var/log/fail2ban.log banaction = nftables[type=allports] bantime = 604800 findtime = 86400 maxretry = 5 # /etc/fail2ban/jail.d/recidive.conf [recidive] enabled = true logpath = /var/log/fail2ban.log banaction = nftables[type=allports] bantime = 604800 findtime = 86400 maxretry = 5 # /etc/fail2ban/jail.d/recidive.conf [recidive] enabled = true logpath = /var/log/fail2ban.log banaction = nftables[type=allports] bantime = 604800 findtime = 86400 maxretry = 5 fail2ban-client -weight: 500;">status fail2ban-client -weight: 500;">status fail2ban-client -weight: 500;">status fail2ban-client -weight: 500;">status caddy-security fail2ban-client -weight: 500;">status caddy-security fail2ban-client -weight: 500;">status caddy-security grep "Ban\|Unban" /var/log/fail2ban.log | tail -20 grep "Ban\|Unban" /var/log/fail2ban.log | tail -20 grep "Ban\|Unban" /var/log/fail2ban.log | tail -20 nft list ruleset | grep fail2ban nft list ruleset | grep fail2ban nft list ruleset | grep fail2ban - Filter — a regex that extracts a client IP and timestamp from a log line - Jail — combines a filter with thresholds: how many matches (maxretry) within what window (findtime) triggers a ban, and how long that ban lasts (bantime) - Action — what happens when the threshold is crossed; typically an nftables or iptables rule that drops traffic from the offending IP