UFW was invented so that a firewall could be configured in three commands, and at that it succeeds. The problem is elsewhere: ufw status shows intentions, not results. A rule can be in the list and close absolutely nothing — for three different reasons, all of which turn up on real servers regularly.
A start that does not lock you out
The order of the commands matters. Allow SSH first, then enable:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw enable
ufw enable before allowing SSH cuts off your own session, and from then on it is the provider's console only. If the server is already configured and you are not certain, keep a second connection open — it survives a failed edit.
Look at the verbose status; the short one hides the default policy:
sudo ufw status verbose
Mistake one: the rule exists and the port is open to the world
The most common line in other people's configurations:
sudo ufw allow 3306
That is how MySQL gets opened "so I can connect from home" — and opened to the entire internet. There are two correct variants, and both are better:
sudo ufw allow from 203.0.113.25 to any port 3306
or, more reliably, do not let the service out at all: bind-address = 127.0.0.1 in the MySQL config, with external access through an SSH tunnel. The firewall is the second line; the first is that the service does not listen on a public address. A rule in UFW can be deleted by accident, whereas bind-address does not change on its own.
Mistake two: IPv6
A rule such as ufw allow from 203.0.113.25 applies to IPv4 only. If the server has an IPv6 address — and most VPSes do, enabled — the service remains reachable over it. The provider handed out an address, you do not remember it, the scanner does.
Check that v6 filtering is on at all (IPV6=yes in /etc/default/ufw) and that the service is not listening on :: without need:
ss -tulpn | grep ':::'
Rules that name an address explicitly have to be written separately for each version of the protocol.
Mistake three: Docker
The most galling one. Docker publishes ports by adding its own rules to the iptables chains before the ones UFW writes to. As a result a container started with -p 5432:5432 is reachable from the internet even though UFW reports Status: active and a deny incoming policy. The firewall is not broken — it simply never gets its turn.
The cure is not in UFW but in how the port is published:
ports:
- "127.0.0.1:5432:5432"
Binding to the local address is the simplest and most dependable solution. Only what genuinely serves visitors should face outwards: usually ports 80 and 443 of a reverse proxy.
Rule order
UFW applies the first matching rule and stops there. So a deny added after an allow will not work: it never gets reached. Look at the numbering and insert where needed:
sudo ufw status numbered
sudo ufw insert 1 deny from 198.51.100.0/24
sudo ufw delete 7
Rules are deleted by number, but the numbers shift after every deletion — delete one at a time and re-read the list.
Checking from outside
Local commands show what is configured. What actually came out is visible only from another machine:
nmap -Pn -p- 203.0.113.25
nc -zv 203.0.113.25 3306
Any other server or your home computer will do. This is the one check that does not lie, and it is worth running after every noticeable reconfiguration — especially after installing something that "configures the network itself": Docker, control panels, VPNs.
Logs
By default UFW writes almost nothing. Switch it on with:
sudo ufw logging low
The entries go to /var/log/ufw.log — but only if rsyslog is present in the system. On Debian 12 and Ubuntu 24.04 it may not be, in which case everything goes to the systemd journal:
sudo journalctl -k | grep -i '\[UFW'
An empty /var/log/ufw.log means nothing by itself — look in the journal first.
What to expect from a firewall
UFW closes what should not be reachable. It does not inspect the contents of requests to what is open: port 443 is open to everyone, and whatever arrives at the site arrives unimpeded. That is the work of other tools — a WAF at the application level, an IDS at the traffic level. The firewall's job is humbler and more important: that the list of open ports matches what you believe about it. What that list looks like together with the active rules on one page is on the demo below.