ufw and distribution firewall wrappers
What you'll learn
- Configure ufw on Ubuntu/Debian
- Recognise when ufw is the right choice
- Understand the layered model: ufw over nftables over kernel
- Avoid common ufw mistakes
Prerequisites
Verified against Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL 9.x · Rocky Linux 9.x · AlmaLinux 9.x · Linux kernel 6.1 LTS / 6.6 LTS · systemd 255+ · OpenSSH 8.7p1 (RHEL 9) / 9.6p1 (Ubuntu 24.04) · nftables 1.0.x · chrony 4.x · Pacemaker 2.1.x · Corosync 3.1.x · 2026-08-09
Several Linux distributions ship simplified firewall wrappers that translate high-level commands into nftables rules. The most common is ufw on Ubuntu and Debian. This lesson covers ufw and when to use it.
What ufw is
ufw (Uncomplicated Firewall) is Ubuntu’s default firewall manager. It provides:
- Simple rules:
ufw allow 22/tcp. - Service aliases:
ufw allow OpenSSH. - Application profiles:
ufw allow 'nginx full'. - Default policies:
ufw default deny incoming.
Under the hood, ufw generates iptables-nft rules.
Basic usage
# Check status
sudo ufw status
sudo ufw status verbose
sudo ufw status numbered
# Set default policies
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Allow a port
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Allow a service by name
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
# Allow from a specific source
sudo ufw allow from 10.0.0.0/24 to any port 22
# Deny
sudo ufw deny 23/tcp
# Delete a rule
sudo ufw delete allow 80/tcp
sudo ufw status numbered # find the number
sudo ufw delete 3 # delete by number
# Enable / disable
sudo ufw enable
sudo ufw disable
sudo ufw reload
Application profiles
ufw reads application profiles from
/etc/ufw/applications.d/. Each profile defines the ports
a service uses:
sudo ufw app list # list available profiles
sudo ufw app info 'Nginx Full' # show details
sudo ufw allow 'Nginx Full' # allow the whole profile
To create a custom profile:
# /etc/ufw/applications.d/myapp
[MyApp]
title=My Application
description=My application listening on 8080
ports=8080/tcp
Reload ufw to pick up:
sudo ufw reload
Rate limiting
ufw supports rate limiting, which is useful for SSH:
sudo ufw limit ssh
This allows SSH but limits new connections to 6 per 30 seconds from a single IP - protection against SSH brute force.
Logging
sudo ufw logging on
sudo ufw logging low # low | medium | high | full
Logs go to /var/log/ufw.log. Levels:
off: no logging.low: blocked packets logged.medium: low + allowed packets with rate-limited logging.high: medium + packets without rate limit.full: like high with prefix on every packet.
The layering
ufw command
-> translates to iptables rules
-> iptables-nft translates to nftables rules
-> kernel netfilter applies rules
You can see what ufw produced:
sudo iptables -L -n -v
# or
sudo nft list ruleset
If you edit nftables directly, ufw may overwrite your changes
on reload. Use ufw for everything or nft for everything,
not both.
When to use ufw vs nftables
Use ufw when:
- The host is Ubuntu or Debian desktop/server.
- The ruleset is simple (allow a few ports, default deny).
- Operators are not network specialists.
Use raw nftables when:
- The ruleset is complex (sets, maps, expressions).
- You need features ufw does not expose.
- The host is multi-homed with complex zone logic.
Common pitfalls
- ufw is disabled by default on Ubuntu Server (changed in
Ubuntu 20.04+). Enable with
sudo ufw enable. - Docker bypasses ufw. See the section below - this one is a security exposure, not an inconvenience.
- IPv6 is enabled by default in ufw. The
/etc/ufw/ufw.confIPV6=yessetting controls this. - Application profile changes require
ufw reload, not justufw app update.
Docker bypasses ufw
Docker writes DNAT rules into the nat PREROUTING chain and
accept rules into its own FORWARD chains. Both are evaluated
before ufw’s filter rules. A container started with
-p 8080:80 is therefore reachable from the internet even with
ufw default deny incoming in force, and ufw status will never
mention it.
The supported control point is the DOCKER-USER chain. Docker
evaluates it before its own rules and never overwrites it, which
is exactly why it exists:
# Only the management network may reach published ports
sudo iptables -I DOCKER-USER -i eth0 ! -s 10.0.0.0/24 -j DROP
Two alternatives, depending on the design:
- Publish to the loopback only, and put a reverse proxy in front:
-p 127.0.0.1:8080:80. The port is then genuinely local and ufw governs the proxy’s port as normal. - Set
"iptables": falsein/etc/docker/daemon.jsonand manage all forwarding and NAT yourself. This is honest but total: you own container connectivity from that point on.
Rules in DOCKER-USER are not persisted by ufw, so save them
with the host’s usual mechanism (iptables-persistent, a
systemd unit, or your configuration management) or they vanish on
reboot.
Knowledge check
Knowledge check · 4 questions
Q1. A host runs ufw default deny incoming, and ufw status lists only 22/tcp. A container is then started with -p 5432:5432. Who can reach PostgreSQL?
Q2. Setting icc=false in the Docker daemon closes the ufw bypass for published ports.
Q3. Which check would have caught the exposed container port?
Q4. A DOCKER-USER rule added with iptables -I is gone after the next reboot unless something is configured to restore it.
Passing score: 75%. Answers are checked in this browser.