Uncomplicated Firewall — a human-friendly front-end for iptables and nftables, designed to make host-based firewalling on Ubuntu and Debian approachable without sacrificing capability.
allow · deny · limit · app profiles · NAT · logging · IPv4 · IPv6 · Ubuntu · Debian
01 — What is ufw?
ufw is the default firewall tool on Ubuntu. It wraps iptables (and nftables on newer systems) in a straightforward command interface, eliminating the need to understand chain syntax for common tasks. Rules are expressed as plain English: allow ssh, deny 3306, limit 22/tcp.
ufw makes the 90% case — open a port, close a port, rate-limit SSH — a single readable command rather than an iptables essay.
For anything ufw can't express directly — complex NAT, per-interface rules, advanced conntrack — its underlying rule files (/etc/ufw/before.rules, after.rules) accept raw iptables syntax that is merged in automatically.
| Concept | Meaning |
| Rule | An allow, deny, reject, or limit statement applied to a port, service, IP, or app profile. |
| App profile | A named set of ports defined in /etc/ufw/applications.d/. Similar to firewalld services. |
| Default policy | What happens to traffic that matches no rule: allow, deny, or reject. |
| before.rules / after.rules | Raw iptables rules merged before and after ufw's own rules. Used for NAT and advanced config. |
| Numbered rules | Every rule has an index. Used to insert or delete rules at specific positions. |
02 — Installation & First Enable
Debian / Ubuntu
# Pre-installed on Ubuntu. If missing:
sudo apt update && sudo apt install ufw
Arch Linux
sudo pacman -S ufw
sudo systemctl enable --now ufw
First-time setup — safe sequence
# 1. Set default policies BEFORE enabling — deny all inbound, allow all outbound
sudo ufw default deny incoming
sudo ufw default allow outgoing
# 2. Allow SSH BEFORE enabling — otherwise you lock yourself out
sudo ufw allow ssh
# 3. Enable the firewall
sudo ufw enable
# 4. Check status
sudo ufw status verbose
⚠ Always allow SSH before running ufw enable on a remote server. The default incoming deny policy will block your own connection the instant the firewall activates.
Enable / disable / reset
# Enable (persists across reboots)
sudo ufw enable
# Disable completely (rules preserved but not applied)
sudo ufw disable
# Reset — wipe all rules and start fresh
sudo ufw reset
# Check status
sudo ufw status
sudo ufw status verbose # shows default policies and all rule details
sudo ufw status numbered # shows rules with index numbers
03 — Default Policies
Default policies determine what happens to packets that match no rule. The recommended starting point is deny incoming, allow outgoing — then selectively open inbound services.
# Standard server defaults
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Stricter — also restrict outbound (adds rules as you go)
sudo ufw default deny outgoing
# Reject instead of drop (sends ICMP error to sender)
sudo ufw default reject incoming
# Forward policy — relevant if this host routes traffic
sudo ufw default deny routed
| Policy | Effect on unmatched packets |
| deny | Silently drop — sender gets no response (stealth). |
| reject | Drop and send ICMP "port unreachable" — sender knows it was blocked. |
| allow | Pass through — use for outgoing or in fully trusted environments. |
04 — Allowing & Denying Traffic
Rules can be specified by service name, port number, protocol, IP address, or any combination. ufw resolves service names from /etc/services.
By service name
# Allow SSH (resolves to TCP 22)
sudo ufw allow ssh
# Allow HTTP and HTTPS
sudo ufw allow http
sudo ufw allow https
# Deny FTP
sudo ufw deny ftp
By port number
# Allow a specific TCP port
sudo ufw allow 8080/tcp
# Allow a specific UDP port
sudo ufw allow 51820/udp
# Allow a port range
sudo ufw allow 60000:61000/udp
# Allow both TCP and UDP on a port (omit protocol)
sudo ufw allow 53
By source IP
# Allow all traffic from a specific IP
sudo ufw allow from 203.0.113.10
# Allow all traffic from a subnet
sudo ufw allow from 10.0.0.0/8
# Allow a specific IP to reach a specific port
sudo ufw allow from 203.0.113.10 to any port 22
# Allow a subnet to reach a specific port and protocol
sudo ufw allow from 10.0.0.0/24 to any port 5432 proto tcp
# Deny a specific IP
sudo ufw deny from 198.51.100.99
# Deny a subnet
sudo ufw deny from 198.51.100.0/24
By destination interface
# Allow HTTP only on a specific network interface
sudo ufw allow in on eth0 to any port 80
# Allow traffic on the loopback interface
sudo ufw allow in on lo
# Deny traffic arriving on eth1
sudo ufw deny in on eth1 to any port 3306
Reject (sends ICMP error instead of silent drop)
sudo ufw reject 23/tcp # Telnet — reject with a message
sudo ufw reject smtp
05 — The limit Rule (Rate Limiting)
The limit rule is ufw's built-in brute-force mitigation. It allows connections but blocks source IPs that make more than 6 connection attempts within 30 seconds — no additional tools required.
# Rate-limit SSH (the most common use case)
sudo ufw limit ssh
# Rate-limit by port number
sudo ufw limit 22/tcp
# Rate-limit a custom port
sudo ufw limit 2222/tcp
# Rate-limit a web application login endpoint (any TCP port)
sudo ufw limit 8080/tcp
ℹ The limit threshold is fixed at 6 connections in 30 seconds — it cannot be tuned through ufw. For finer-grained rate limiting (per-IP thresholds, longer windows), use fail2ban alongside ufw or drop down to raw iptables hashlimit rules in /etc/ufw/before.rules.
06 — Application Profiles
Application profiles are named port groups defined in /etc/ufw/applications.d/ — the ufw equivalent of firewalld services. Many packages install their own profiles automatically.
Listing and using profiles
# List all available profiles
sudo ufw app list
# Show what ports a profile includes
sudo ufw app info 'Nginx Full'
sudo ufw app info OpenSSH
# Allow by profile name
sudo ufw allow 'Nginx Full' # HTTP + HTTPS
sudo ufw allow 'Nginx HTTP' # HTTP only
sudo ufw allow 'Nginx HTTPS' # HTTPS only
sudo ufw allow OpenSSH
# Deny a profile
sudo ufw deny 'Apache Full'
Common built-in profiles
| Profile name | Ports |
| OpenSSH | TCP 22 |
| Nginx Full | TCP 80, 443 |
| Nginx HTTP | TCP 80 |
| Nginx HTTPS | TCP 443 |
| Apache Full | TCP 80, 443 |
| Apache | TCP 80 |
| Apache Secure | TCP 443 |
| Postfix | TCP 25, 587 |
| Postfix SMTPS | TCP 465 |
| Dovecot IMAP | TCP 143, 993 |
| Dovecot POP3 | TCP 110, 995 |
| Samba | TCP 139, 445 · UDP 137, 138 |
Creating a custom profile
# /etc/ufw/applications.d/myapp
[MyApp]
title=My Application
description=Custom web application API and admin interface
ports=8080,8443/tcp
# Reload profiles and use it
sudo ufw app update MyApp
sudo ufw allow MyApp
07 — Managing Rules
Rules are evaluated in order — the first matching rule wins. Managing order matters when allow and deny rules could conflict.
Viewing rules
# Simple view
sudo ufw status
# Verbose — shows policies and direction
sudo ufw status verbose
# Numbered — shows index for each rule
sudo ufw status numbered
Deleting rules
# Delete by rule specification (exact match)
sudo ufw delete allow ssh
sudo ufw delete allow 8080/tcp
sudo ufw delete deny from 198.51.100.99
# Delete by rule number (get numbers from: sudo ufw status numbered)
sudo ufw delete 3
# Delete both IPv4 and IPv6 versions of a rule
sudo ufw delete allow 80/tcp
Inserting rules at a specific position
# Insert a DENY rule at position 1 (top — evaluated first)
sudo ufw insert 1 deny from 198.51.100.99
# Insert an ALLOW rule at position 2
sudo ufw insert 2 allow from 10.0.0.5 to any port 22
IPv6 rules
# ufw manages IPv4 and IPv6 simultaneously by default
# Confirm IPv6 is enabled in /etc/default/ufw:
grep IPV6 /etc/default/ufw
# Should show: IPV6=yes
# Rules specified without a family apply to both IPv4 and IPv6
sudo ufw allow https # opens TCP 443 for both
# Family-specific rules
sudo ufw allow proto tcp from 2001:db8::/32 to any port 22
ℹ If IPV6=yes is set in /etc/default/ufw (the default), every rule you add automatically creates both an IPv4 and an IPv6 version. You can see both when running ufw status numbered.
08 — Common Service Configurations
SSH
# Allow SSH from anywhere (with rate limiting — recommended)
sudo ufw limit ssh
# Allow SSH from a specific IP or subnet only
sudo ufw allow from 203.0.113.0/24 to any port 22
# Custom SSH port
sudo ufw allow 2222/tcp
sudo ufw limit 2222/tcp
Web server
# Nginx — HTTP and HTTPS
sudo ufw allow 'Nginx Full'
# Apache — HTTP and HTTPS
sudo ufw allow 'Apache Full'
# Manual — if no profile exists
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
Mail server
sudo ufw allow 25/tcp # SMTP
sudo ufw allow 587/tcp # Submission
sudo ufw allow 465/tcp # SMTPS
sudo ufw allow 143/tcp # IMAP
sudo ufw allow 993/tcp # IMAPS
sudo ufw allow 110/tcp # POP3
sudo ufw allow 995/tcp # POP3S
FTP
sudo ufw allow 21/tcp # FTP control
sudo ufw allow 20/tcp # FTP active data
sudo ufw allow 60000:61000/tcp # Passive FTP data range
# Also configure your FTP daemon to use this passive port range
DNS server
sudo ufw allow 53/tcp
sudo ufw allow 53/udp
Databases — restricted to internal network
# MySQL — app servers only
sudo ufw allow from 10.0.0.0/24 to any port 3306
# PostgreSQL — specific host only
sudo ufw allow from 10.0.0.20 to any port 5432
# Redis — localhost only (typically no firewall rule needed)
sudo ufw deny 6379 # belt-and-suspenders block
VPN
# WireGuard
sudo ufw allow 51820/udp
# OpenVPN
sudo ufw allow 1194/udp
Monitoring & internal tools
# Prometheus node exporter — internal only
sudo ufw allow from 10.0.0.0/8 to any port 9100
# Grafana — internal only
sudo ufw allow from 10.0.0.0/8 to any port 3000
09 — NAT & IP Forwarding
ufw doesn't expose NAT through its CLI — but it fully supports masquerade and DNAT through its underlying rule files. Edit /etc/ufw/before.rules and /etc/default/ufw directly.
Step 1 — enable IP forwarding
# /etc/default/ufw — change the forward policy
DEFAULT_FORWARD_POLICY="ACCEPT"
# /etc/ufw/sysctl.conf — uncomment these lines
net/ipv4/ip_forward=1
net/ipv6/conf/default/forwarding=1
net/ipv6/conf/all/forwarding=1
Step 2 — add NAT rules to before.rules
# /etc/ufw/before.rules
# Add this block at the very TOP of the file, before the *filter section
*nat
:PREROUTING ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
# Masquerade — all traffic leaving eth0 gets the host's public IP
-A POSTROUTING -s 192.168.1.0/24 -o eth0 -j MASQUERADE
# DNAT — forward external TCP:80 to an internal web server
-A PREROUTING -i eth0 -p tcp --dport 80 -j DNAT --to-destination 192.168.1.10:80
COMMIT
# The existing *filter section continues below...
*filter
...
Step 3 — allow forwarded traffic
# Allow forwarding from LAN to WAN
sudo ufw allow in on eth1 out on eth0
sudo ufw allow in on eth1 out on eth0 from 192.168.1.0/24
# Reload to apply
sudo ufw disable && sudo ufw enable
ℹ Changes to before.rules take effect on the next ufw disable && ufw enable cycle or system reboot. A simple ufw reload does not re-read the raw rule files.
10 — Logging
ufw can log dropped and allowed packets at several verbosity levels. Logs go to /var/log/ufw.log and are also visible in the system journal.
Log levels
# Disable logging
sudo ufw logging off
# Log blocked packets only (default — recommended for most servers)
sudo ufw logging on
# Log all packets matching a rule that has logging enabled
sudo ufw logging low # blocked packets
sudo ufw logging medium # blocked + some allowed
sudo ufw logging high # all packets — very verbose
sudo ufw logging full # maximum detail
Per-rule logging
# Log SSH connection attempts explicitly
sudo ufw allow log 22/tcp
# Log and rate-limit SSH
sudo ufw limit log ssh
# Log denied traffic from a specific subnet
sudo ufw deny log from 198.51.100.0/24
Reading the log
# Follow the ufw log live
sudo tail -f /var/log/ufw.log
# Via journald
sudo journalctl -k | grep UFW
# Filter for blocked packets only
sudo grep "UFW BLOCK" /var/log/ufw.log
# Filter for allowed traffic
sudo grep "UFW ALLOW" /var/log/ufw.log
# Count blocked IPs — top offenders
sudo grep "UFW BLOCK" /var/log/ufw.log \
| grep -oP 'SRC=\S+' | sort | uniq -c | sort -rn | head -20
Log entry anatomy
# Example log line:
Mar 17 10:23:01 host kernel: [UFW BLOCK] IN=eth0 OUT= MAC=... \
SRC=198.51.100.99 DST=10.0.0.1 LEN=60 TOS=0x00 PREC=0x00 \
TTL=49 ID=12345 DF PROTO=TCP SPT=54321 DPT=22 WINDOW=64240 \
RES=0x00 SYN URGP=0
| Field | Meaning |
| UFW BLOCK / ALLOW | Whether the packet was dropped or passed. |
| IN= / OUT= | Incoming and outgoing interface names. |
| SRC= / DST= | Source and destination IP addresses. |
| PROTO= | Protocol: TCP, UDP, ICMP. |
| SPT= / DPT= | Source port and destination port. |
| SYN | TCP SYN flag set — a new connection attempt. |
11 — /etc/default/ufw & Advanced Config
The /etc/default/ufw file controls global ufw behaviour — the knobs that the CLI doesn't expose.
/etc/default/ufw — key settings
# /etc/default/ufw
# Enable IPv6 rules (strongly recommended — leave as yes)
IPV6=yes
# Default policies (also settable via CLI, but this is the persistent source)
DEFAULT_INPUT_POLICY="DROP"
DEFAULT_OUTPUT_POLICY="ACCEPT"
DEFAULT_FORWARD_POLICY="DROP" # Set to ACCEPT for routing/NAT
# Logging level
LOGLEVEL="low"
# Manage built-in chains — leave as yes unless you know why you'd change it
MANAGE_BUILTINS=no
Raw rule files
| File | Purpose |
| /etc/ufw/before.rules | Raw iptables rules added before ufw's own rules. Used for NAT (PREROUTING / POSTROUTING) and early custom rules. |
| /etc/ufw/after.rules | Raw iptables rules added after ufw's own rules. Used for LOGGING rules and catch-all overrides. |
| /etc/ufw/before6.rules | IPv6 equivalent of before.rules. |
| /etc/ufw/after6.rules | IPv6 equivalent of after.rules. |
| /etc/ufw/user.rules | ufw's own generated rules — do not edit manually. |
| /etc/ufw/applications.d/ | App profile definitions. |
| /etc/ufw/sysctl.conf | Kernel parameters applied when ufw starts (forwarding, rp_filter, etc.). |
12 — Docker & ufw
Docker bypasses ufw by writing its own iptables rules directly — published ports (-p) become accessible from the internet even if ufw has no allow rule for them. This is a well-known and long-standing footgun.
The problem
# ufw blocks port 8080:
sudo ufw deny 8080
# But Docker publishes it anyway — 8080 is publicly reachable:
docker run -p 8080:80 nginx # ← bypasses ufw entirely
Fix A — disable Docker's iptables management (simplest, some caveats)
# /etc/docker/daemon.json
{
"iptables": false
}
# Then restart Docker
sudo systemctl restart docker
# Now manage container networking manually through ufw
Fix B — restrict published ports to localhost only
# Bind to 127.0.0.1 — only local processes can reach the container
docker run -p 127.0.0.1:8080:80 nginx
# Then use a reverse proxy (nginx, Caddy) on the host to expose selectively
# ufw controls access to the reverse proxy port (80/443) normally
Fix C — ufw-docker (third-party tool)
# Install ufw-docker — patches the DOCKER-USER chain to respect ufw rules
sudo wget -O /usr/local/bin/ufw-docker \
https://github.com/chaifeng/ufw-docker/raw/master/ufw-docker
sudo chmod +x /usr/local/bin/ufw-docker
sudo ufw-docker install
sudo systemctl restart ufw
⚠ Fix B (bind to 127.0.0.1) is the most reliable approach for production. Fix A can break container-to-container networking. Fix C works well but adds a third-party dependency.
13 — Troubleshooting
| Symptom | Cause & Fix |
| Locked out after enable | SSH rule not added before ufw enable. Use out-of-band console, run ufw allow ssh, reconnect. |
| Rule added but traffic still blocked | An earlier deny rule is matching first. Check order with ufw status numbered and insert an allow at a lower number. |
| Rule added but traffic still allowed | A deny rule needs to come before the allow. Use ufw insert 1 deny .... |
| IPv6 not being filtered | IPV6=yes missing or not set in /etc/default/ufw. Run ufw disable && ufw enable after fixing. |
| Docker ports exposed despite deny rules | Docker bypasses ufw — see section 12. |
| NAT rules not taking effect | Changes to before.rules require ufw disable && ufw enable — ufw reload is not enough. |
| ufw status shows inactive | Run sudo ufw enable. After reboot, check sudo systemctl status ufw. |
| App profile not found | Run sudo ufw app update all to refresh the profile list after installing a new package. |
| Deleting a rule doesn't work | The rule specification must match exactly. Use ufw status numbered and delete by number instead. |
Diagnostic commands
# Show full status with policies
sudo ufw status verbose
# Show rules with index numbers
sudo ufw status numbered
# See the actual iptables rules ufw generated
sudo iptables -L -v -n --line-numbers
sudo ip6tables -L -v -n --line-numbers
# Check ufw service status
sudo systemctl status ufw
# Follow the ufw log
sudo tail -f /var/log/ufw.log
sudo journalctl -k -f | grep UFW
# Dry-run check — what would happen without applying changes?
# (ufw has no dry-run, so review numbered rules before deleting)
sudo ufw status numbered
14 — Quick Reference
| Command | What it does |
| ufw enable | Activate the firewall (persists across reboots) |
| ufw disable | Deactivate without deleting rules |
| ufw reset | Wipe all rules and return to defaults |
| ufw status verbose | Show all rules and default policies |
| ufw status numbered | Show rules with index numbers |
| ufw default deny incoming | Drop all inbound traffic by default |
| ufw allow ssh | Allow inbound SSH (TCP 22) |
| ufw limit ssh | Allow SSH with rate limiting (6 conn / 30s) |
| ufw allow 8080/tcp | Open TCP port 8080 |
| ufw allow from 10.0.0.0/8 to any port 22 | Allow SSH from a subnet only |
| ufw deny from 198.51.100.99 | Block all traffic from an IP |
| ufw insert 1 deny from <IP> | Insert a deny rule at the top |
| ufw delete allow 8080/tcp | Delete a rule by specification |
| ufw delete 3 | Delete rule number 3 |
| ufw allow 'Nginx Full' | Allow by app profile name |
| ufw app list | List all available app profiles |
| ufw logging on | Enable logging of blocked packets |
✔ The safe sequence for any new server: ufw default deny incoming → ufw default allow outgoing → ufw limit ssh → open whatever your service needs → ufw enable. In that order, every time.