You boot a new Ubuntu server and everything that listens is reachable from the internet. That is the default. Uncomplicated Firewall (UFW) is Ubuntu’s readable front end to Linux netfilter; you describe what to allow, it writes the real rules underneath. We’ll set strict defaults, open only what you need, and teach you the patterns you’ll reuse when you add web servers, databases, or VPNs. If you are on DigitalOcean, you can also layer their Cloud Firewall in front of the Droplet; UFW still matters on the host for defense-in-depth and for outbound control.
What you’ll do in this guide
You’ll confirm UFW is installed, enable IPv6, set default policies to “deny incoming, allow outgoing,” and add rules for SSH and web traffic. You’ll learn rule scoping by IP, interface, port ranges, and application profiles, plus how to rate-limit SSH, inspect listening services, and remove or reorder rules safely. If you run Docker, you’ll also fix the common “UFW doesn’t block container ports” surprise using the DOCKER-USER
chain and safer publish patterns.
Prerequisites
You have an Ubuntu LTS server (e.g., on a DigitalOcean Droplet) and a sudo-capable user. DigitalOcean’s “Initial Server Setup” covers creating that user; UFW ships with Ubuntu, but you can install it if needed.
# Check availability and state
sudo ufw status || sudo apt update && sudo apt install -y ufw
Enable IPv6 support
Modern hosting gives you IPv4 and often IPv6. Tell UFW to manage both so rules apply consistently. Edit /etc/default/ufw
and set IPV6=yes
, then cycle UFW so the change takes effect.
sudo nano /etc/default/ufw
# set: IPV6=yes
# apply the change
sudo ufw disable
sudo ufw enable
Set strict defaults
Default policies should deny unsolicited inbound traffic and allow outbound so the server can update and reach APIs. We allow SSH before enabling the firewall so you do not lock yourself out.
# default stance
sudo ufw default deny incoming
sudo ufw default allow outgoing
# allow SSH (use the profile name OpenSSH or port/proto explicitly)
sudo ufw allow OpenSSH
# optional: rate-limit repeated SSH connection attempts
sudo ufw limit OpenSSH
What “limit” does: UFW will slow down hosts that open more than ~6 new connections in 30 seconds to the limited service. It is a simple, useful brake on brute-force attempts.
Now turn it on and verify:
sudo ufw enable
sudo ufw status verbose
sudo ufw show added # shows your added rules even if UFW were inactive
UFW’s show
reports and status
are the fastest way to review what will run; they complement status
by printing what you have configured, not just what is live.
Allow web traffic
If you installed Nginx or Apache, they register “application profiles” with UFW so you can allow a readable name instead of memorizing ports. Use ufw app list
to see what exists and ufw app info
to inspect exactly which ports a profile opens.
sudo ufw app list
sudo ufw app info "Nginx Full"
# allow both HTTP (80) and HTTPS (443) with one profile:
sudo ufw allow "Nginx Full"
# or explicitly by ports and protocol in one rule:
sudo ufw allow proto tcp from any to any port 80,443
Application profiles live under /etc/ufw/applications.d
and are plain-text; you can add a custom one if your app needs a set of ports. Profiles define title
, description
, and ports
.
Example: add a WireGuard profile
Create /etc/ufw/applications.d/wireguard
with:
[WireGuard]
title=WireGuard VPN
description=Secure UDP tunnel on 51820
ports=51820/udp
Then enable it:
sudo ufw app update WireGuard
sudo ufw allow WireGuard
Scope rules by IP, interface, or range
UFW’s “readable first” syntax scales to precise constraints. You will use these patterns often.
Allow only from a trusted IP to SSH:
sudo ufw allow proto tcp from 203.0.113.10 to any port 22
Allow PostgreSQL from your app server subnet, not the internet:
sudo ufw allow proto tcp from 10.0.0.0/24 to any port 5432
Bind a rule to a specific network interface (for multi-homed hosts):
sudo ufw allow in on eth0 to any port 80 proto tcp
Open a port range for passive FTP, game servers, or WebRTC:
sudo ufw allow 10000:20000/udp
These examples come straight from UFW’s documented extended syntax; scoping by interface or source network keeps private services private.
IPv6 works the same way once IPV6=yes
is set. For example, to allow from a link-local /64:
sudo ufw allow from fe80::/64
Edit the default first, then disable/enable UFW to load IPv6 handling.
Verify what is actually listening
A firewall complements secure services; it cannot save a daemon that binds to the wrong interface. Check open sockets, then match them to your rules.
# what is listening and on which address
sudo ss -tulpn
# correlate listening ports with firewall rules
sudo ufw show listening
Cleanly delete or reorder rules
When you are experimenting, remove mistakes by number so UFW renumbers safely. You can also insert precedence rules at a specific position.
# list rules with indices
sudo ufw status numbered
# delete rule #3 (answer y to confirm)
sudo ufw delete 3
# insert a new allow as the first rule
sudo ufw insert 1 allow proto tcp from 203.0.113.10 to any port 22
If you truly need to start over, ufw reset
purges user rules and disables the firewall; apply defaults again and re-add the essentials before re-enabling.
Logging and troubleshooting
Turn on UFW logging to record denied packets to syslog; “low” is the sane default. When in doubt, read /var/log/ufw.log
and journalctl
. If you block yourself from SSH, use your cloud provider’s web console to run sudo ufw allow OpenSSH
and recover.
sudo ufw logging on
tail -f /var/log/ufw.log
To confirm automatic rules Nginx or other packages installed, inspect their app profiles:
sudo ufw app list
sudo ufw app info "Nginx Full"
Docker: why UFW seems to “do nothing,” and how to fix it
By design, Docker edits netfilter tables and inserts its own rules ahead of yours. Publishing a container with -p
can bypass your UFW policy because packets hit Docker’s chains first. The two safe patterns are: 1) bind published ports only to localhost and front them with a reverse proxy, or 2) add policy in the DOCKER-USER
chain, which Docker documents for operator-defined filtering.
Safest publish pattern (bind to loopback only):
# container’s port 8080 is reachable only from this host
docker run -d --name app -p 127.0.0.1:8080:8080 your/image
Let UFW control public access to containers via DOCKER-USER:
Add a short block in /etc/ufw/after.rules
that hands public traffic back to UFW while allowing RFC1918 inter-container networking. Restart UFW afterward.
sudo nano /etc/ufw/after.rules
# append:
# BEGIN UFW AND DOCKER
*filter
:ufw-user-forward - [0:0]
:DOCKER-USER - [0:0]
-A DOCKER-USER -j RETURN -s 10.0.0.0/8
-A DOCKER-USER -j RETURN -s 172.16.0.0/12
-A DOCKER-USER -j RETURN -s 192.168.0.0/16
-A DOCKER-USER -j ufw-user-forward
COMMIT
# END UFW AND DOCKER
sudo ufw reload
This keeps private container networks reachable while letting UFW decide what the public internet can hit. If you prefer a turnkey helper, ufw-docker
packages this approach.
DigitalOcean Cloud Firewall + UFW
Cloud Firewalls stop traffic at the network edge before it touches your Droplet, and you can attach them to many Droplets by tag. Keep UFW on the host anyway: it controls egress, provides per-service logging, and still protects you if you temporarily remove a Droplet from a Cloud Firewall. This layered model is DigitalOcean’s documented pattern.
Beginner recipes you will reuse
1) Static website only (SSH + HTTP/HTTPS): allow OpenSSH, Nginx, and leave the rest closed.
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow "Nginx Full"
sudo ufw enable
sudo ufw status verbose
2) Database reachable only from another Droplet: scope by source subnet or IP.
# only app servers in 10.10.0.0/24 may connect to Postgres
sudo ufw allow proto tcp from 10.10.0.0/24 to any port 5432
3) Admin panel bound to a specific interface: only open on the public NIC.
sudo ufw allow in on eth0 to any port 8443 proto tcp
4) WireGuard VPN server: single UDP port plus SSH.
sudo ufw allow OpenSSH
sudo ufw allow 51820/udp
sudo ufw enable
5) Lock SSH to your office and rate-limit: combine source match with limit
.
sudo ufw allow proto tcp from 198.51.100.0/24 to any port 22
sudo ufw limit OpenSSH
Maintenance checklist
Review listening services after installs, not just the firewall, so you catch daemons that bind broadly by default. Use numbered deletes for cleanup and insert
when precedence matters. Keep IPv6 enabled so rules are symmetric. For Docker, either bind to 127.0.0.1
or move filtering into DOCKER-USER
. If you also use DigitalOcean Cloud Firewalls, treat UFW as your last line and your egress policy.
Appendix: quick reference
Status and reports
sudo ufw status verbose
sudo ufw show listening
sudo ufw show added
sudo ufw show raw
Rule management
sudo ufw allow 80/tcp
sudo ufw deny 23/tcp
sudo ufw allow proto tcp from 203.0.113.10 to any port 22
sudo ufw allow in on eth0 to any port 443 proto tcp
sudo ufw status numbered
sudo ufw delete 2
sudo ufw insert 1 allow OpenSSH
Reset or reload
sudo ufw reload
sudo ufw reset # disables and clears rules; re-add essentials before enabling
These commands and capabilities are covered in the Ubuntu docs and ufw(8) manual; the application-profile flow is standard across server packages like OpenSSH and Nginx.
If something goes wrong, you can recover using your provider’s out-of-band console. Re-open SSH, check ss
for listeners, and re-enable UFW with just the SSH and one service rule, then add the rest deliberately. The point is not memorizing every switch, but recognizing the small set of patterns that keep the attack surface tight as your server evolves.