How to Set Up a UFW Firewall on Ubuntu
In this article15 sections
To set up a UFW firewall on Ubuntu, allow SSH first, set the defaults to deny incoming and allow outgoing, allow the ports your services need, and only then switch the firewall on. Four commands do it: sudo ufw allow OpenSSH, sudo ufw default deny incoming, sudo ufw allow 'Nginx Full' and sudo ufw enable. The order matters. Enabling the firewall before allowing SSH is the most common way people lock themselves out of a new server.
This guide is for Ubuntu 22.04 and 24.04, and applies to Debian with UFW installed. It covers the first setup, everyday rule changes, limiting SSH to your own address, and the two things UFW does not protect you from unless you know about them: Docker and IPv6.
Step 1: Check the current state
sudo ufw status verbose
Status: inactive means no rules are being enforced. If it says active, the list below it shows what is allowed, and you can skip to the section on changing rules.
Step 2: Allow SSH before anything else
sudo ufw allow OpenSSH
This opens port 22. If your SSH runs on a different port, allow that port instead, for example sudo ufw allow 2222/tcp. Check which port it is with sudo ss -tlnp | grep sshd.
Warning. Do not run ufw enable until this rule is in place. Rules can be added while the firewall is off, and that is exactly when to add this one.
Step 3: Set the defaults
sudo ufw default deny incoming
sudo ufw default allow outgoing
From now on, anything you have not explicitly allowed is refused, and the server itself can still reach the internet for updates, DNS and outgoing mail.
Step 4: Allow your services
For a web server:
sudo ufw allow 'Nginx Full'
That opens ports 80 and 443. 'Nginx Full' is an application profile, a named set of ports that a package installs. See which profiles exist with sudo ufw app list. You can always allow by number instead:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
Open only what something is actually listening on and the public needs to reach. A database, Redis or an admin tool should not be in this list at all.
Step 5: Switch it on
sudo ufw enable
sudo ufw status numbered
UFW warns that existing SSH connections may be disrupted. With the rule from step 2 in place, answer y. The firewall is now active and starts automatically at boot. The status looks like this:
Status: active
To Action From
-- ------ ----
[ 1] OpenSSH ALLOW IN Anywhere
[ 2] Nginx Full ALLOW IN Anywhere
[ 3] OpenSSH (v6) ALLOW IN Anywhere (v6)
[ 4] Nginx Full (v6) ALLOW IN Anywhere (v6)
Before you close your current session, open a second terminal and confirm you can still log in.
Everyday changes
| Task | Command |
|---|---|
| Allow a port | sudo ufw allow 8080/tcp |
| Allow a range | sudo ufw allow 6000:6010/tcp |
| Allow one address to one port | sudo ufw allow from 203.0.113.45 to any port 3306 proto tcp |
| Block an address | sudo ufw insert 1 deny from 198.51.100.7 |
| Remove a rule | sudo ufw status numbered, then sudo ufw delete 3 |
| Remove a rule by what it says | sudo ufw delete allow 8080/tcp |
| Reload after editing files | sudo ufw reload |
Two details save trouble. Rules are checked from the top, and the first match wins, so a deny for one address must sit above the general allow for that port. That is why the block command uses insert 1. And after you delete a rule, the numbers change, so run status numbered again before deleting another.
Limit SSH to your own address
If you always connect from a fixed address, such as an office or a VPN, allow SSH from there only and remove the open rule:
sudo ufw allow from 203.0.113.45 to any port 22 proto tcp
sudo ufw delete allow OpenSSH
Do it in that order, and test a new login in between. If your home or mobile address changes, do not use this: you will lock yourself out the next time it does. Use rate limiting instead, which slows down password guessing without tying you to one address:
sudo ufw limit OpenSSH
limit refuses an address that opens six or more connections within 30 seconds. It replaces the plain allow rule for SSH.
What UFW does not cover: Docker
Docker manages its own firewall rules, and they are checked before UFW’s. A container started with -p 8080:8080 is reachable from the internet even when ufw status shows no rule for port 8080. UFW is not broken; the traffic never reaches it.
The fix is to publish container ports on the loopback address only, and let Nginx on the host forward to them:
docker run -d -p 127.0.0.1:8080:8080 my-image
In a Compose file, write the port as "127.0.0.1:8080:8080". Then check from another machine, not from the server itself:
nc -zv your-server-ip 8080
It should fail to connect.
What UFW does not cover: a disabled IPv6
Most servers have an IPv6 address as well as an IPv4 one. UFW protects both only if this line in /etc/default/ufw says yes, which is the default on Ubuntu:
grep IPV6 /etc/default/ufw
IPV6=yes
If it says no, your IPv4 side is filtered and your IPv6 side is wide open. Change it to yes and run sudo ufw reload. The (v6) lines in the status output are the proof it is working.
See what is being blocked
sudo ufw logging low
sudo tail -f /var/log/ufw.log
Each [UFW BLOCK] line shows the source address (SRC) and the port it tried (DPT). A steady stream of blocked attempts on ports 22, 23, 3306 and 6379 is normal background noise on any public server. Use the log when a legitimate connection is failing: if the attempt shows up as blocked, you need a rule; if it does not show up at all, the problem is before the server, usually a provider firewall or DNS.
Test from outside
Rules that look right can still be wrong. From your own computer, check one port that should be open and one that should not:
nc -zv your-server-ip 443
nc -zv your-server-ip 3306
The first should succeed and the second should time out or be refused. On Windows, Test-NetConnection your-server-ip -Port 443 in PowerShell does the same.
Start again
If the rules have become a tangle, reset and rebuild them. The reset switches the firewall off, so you cannot lock yourself out by doing it:
sudo ufw reset
sudo ufw allow OpenSSH
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 'Nginx Full'
sudo ufw enable
Common mistakes to avoid
- Enabling before allowing SSH. If it happens, see how to recover a locked-out server.
- Changing the SSH port in the config and forgetting the firewall. Allow the new port first, then restart SSH.
- Opening the database port to the world. If another server needs it, allow that server’s address only.
- Trusting UFW to cover Docker. Bind published ports to
127.0.0.1. - Adding a deny rule at the bottom. An earlier allow rule wins. Use
insert 1. - Testing from the server itself. Local connections do not pass through the same rules. Test from another machine.
The firewall on a SiteHarbour VPS
Every SiteHarbour VPS is handed over with UFW already active and open only for the ports your stack uses, alongside SSH key login, Fail2ban and automatic security updates. This guide is for the changes you make afterwards, when you add a service or want to tighten SSH. To confirm the whole setup is still as it should be, run the VPS security audit.
Frequently asked questions
Does UFW slow the server down?
No. It is a front end for the kernel’s own packet filter, which handles far more traffic than a single server sees without measurable cost.
Do I need UFW if my provider has a firewall?
Use both if you can. The provider’s firewall stops traffic before it reaches the server; UFW still protects you if that outer rule is changed by mistake, and it travels with the server.
Are my rules kept after a reboot?
Yes. Once enabled, UFW starts at boot with the same rules.
Should I allow port 80 if my site only uses HTTPS?
Yes. Visitors who type the address without https arrive on port 80 and are redirected, and Let’s Encrypt uses port 80 to renew certificates.
What is the difference between deny and reject?
deny silently drops the connection, so the other side waits and times out. reject answers that the port is closed. For a public server, deny is the usual choice.
The short version: allow SSH, deny everything else coming in, open only the ports the public needs, then enable. Bind Docker ports to the loopback address, check that IPv6 is covered, and always test from another machine.