You can block a source IP range in UFW with a single command: sudo ufw deny from 203.0.113.0/24. This guide shows that command, then builds a small script and cron job that read a blocklist file and keep those ranges blocked automatically, so you are not editing rules by hand every time a feed changes.
The approach works on Ubuntu 22.04/24.04, Debian 12 and AlmaLinux/Rocky 8–9. It is aimed at ranges you already know are hostile or unwanted — scanners, abusive subnets, a country block you decided on. It is not a replacement for rate limiting or Fail2ban, which handle hosts that misbehave after connecting.
Before you start
- A Linux VPS with root or sudo access and UFW installed. On Ubuntu and Debian:
sudo apt install ufw. On AlmaLinux and Rocky, UFW comes from EPEL:sudo dnf install epel-release && sudo dnf install ufw. Check your distribution's documentation if the package name differs. - Your own admin IP written down. You need it to avoid blocking yourself.
- SSH access tested, plus the browser console in your VPS provider's control panel open in a second tab. The console is your way back in if a rule goes wrong.
Warning: A deny rule that matches your own IP or your office range will lock you out of SSH immediately. Keep your VPS provider's console open until you have confirmed the rule does what you expect.
Warning: sudo ufw reload and sudo ufw reset flush and rebuild the rule set. Established SSH sessions can drop during a reload. Prefer adding rules with the ufw command, which applies them without a full flush.
Block a source IP range with UFW
Start by looking at what is already loaded.
sudo ufw status verbose
sudo ufw status numberedThen deny the range. deny drops packets silently; reject sends a refusal back, which tells the sender the host exists.
sudo ufw deny from 203.0.113.0/24To block the range only on one service, scope the rule to a port:
sudo ufw deny from 203.0.113.0/24 to any port 22 proto tcpUFW evaluates rules in order, so a rule appended at the end sits below any existing allow. If you already allow SSH from Anywhere, a later deny will never be reached. Insert the block at the top instead:
sudo ufw insert 1 deny from 203.0.113.0/24Preview what a rule will do before you commit it:
sudo ufw --dry-run deny from 203.0.113.0/24Remove a rule by number from ufw status numbered, or by repeating the rule text:
sudo ufw delete 3
sudo ufw delete deny from 203.0.113.0/24Keep a blocklist file
Typing rules one at a time does not scale. Put the ranges in a file, one CIDR per line, with # for comments.
sudo install -m 600 /dev/null /etc/ufw/blocklist.txt
sudo nano /etc/ufw/blocklist.txt# Known hostile ranges - review monthly
203.0.113.0/24
198.51.100.0/25
192.0.2.128/26Keep the file mode at 600 and owned by root. Anyone who can write to it can change your firewall.
Tip: Only add ranges you can justify. Blocking a large cloud provider range will also block legitimate users and monitoring services that live there.
Automate the blocklist with a script and cron
The script below reads the file, skips ranges that are already blocked, and inserts new ones at the top of the rule set.
sudo nano /usr/local/sbin/ufw-blocklist.sh#!/usr/bin/env bash
set -euo pipefail
LIST="/etc/ufw/blocklist.txt"
UFW="/usr/sbin/ufw"
[[ -f "$LIST" ]] || { echo "blocklist: $LIST not found"; exit 1; }
while read -r line; do
cidr="${line%%#*}"
cidr="$(echo "$cidr" | tr -d '[:space:]')"
[[ -z "$cidr" ]] && continue
if "$UFW" status | grep -qF "$cidr"; then
continue
fi
echo "blocklist: adding deny for $cidr"
if "$UFW" status numbered | grep -q '^\['; then
"$UFW" insert 1 deny from "$cidr" >/dev/null
else
"$UFW" deny from "$cidr" >/dev/null
fi
done < "$LIST"Make it executable and run it once by hand to confirm it behaves.
sudo chmod 700 /usr/local/sbin/ufw-blocklist.sh
sudo /usr/local/sbin/ufw-blocklist.shThen schedule it. Use the root crontab so the script has the privileges it needs.
sudo crontab -e17 4 * * * /usr/local/sbin/ufw-blocklist.sh >> /var/log/ufw-blocklist.log 2>&1If you pull ranges from a third-party feed, download to a temporary file, validate that every line looks like a CIDR, and only then move it into place. Never pipe a remote list straight into ufw.
Warning: A bad feed can contain 0.0.0.0/0 or your own subnet. Validate the file before the script reads it, and keep a copy of the last known good list.
Check it works
Confirm the rules are present and in the order you expect.
sudo ufw status numbered | head -20
sudo ufw status verboseFrom a host inside the blocked range, a connection attempt should time out:
curl --max-time 5 http://203.0.113.10/
nc -vz 203.0.113.10 22Turn on logging and watch the drops arrive:
sudo ufw logging on
sudo tail -f /var/log/ufw.logBlocked packets appear with the [UFW BLOCK] prefix, including the source address and destination port. If nothing shows up, the traffic is not reaching the rule — see the next section.
Troubleshooting
- The rule exists but traffic still gets through. An
allowrule sits above it in the numbered list. Runsudo ufw status numberedand either insert the deny at position 1 or delete the broader allow. - You locked yourself out of SSH. Your own IP matched the range. Open your VPS provider's console, run
sudo ufw disable, then delete the offending rule withsudo ufw delete <number>and re-enable UFW. - Docker containers are still reachable from blocked ranges. Docker writes its own iptables rules that bypass UFW's chains. See Stop Docker from bypassing your UFW firewall.
- The cron job runs but no rules are added. Cron has a minimal
PATH, soufwmay not resolve. The script above uses the full path/usr/sbin/ufwfor this reason. Check/var/log/ufw-blocklist.logfor errors. - Reloads have become slow. Each rule is a separate iptables entry. Past a few hundred ranges, switch to a set-based approach with ipset for high-performance IP blacklisting.
Next steps
- Block an IP address or subnet with UFW for the single-host and one-off cases.
- Read and tune UFW firewall logs to see what your new rules are catching.
- Combining Fail2ban with UFW and firewalld to block hosts that misbehave after connecting.
