If you followed our security hardening guide, you already have UFW enabled with basic rules — allow SSH, allow HTTP/HTTPS, deny everything else. That covers the basics, but a production VPS running multiple services needs more. You need rate limiting to stop brute-force attacks, application profiles for clean rule management, Docker-aware firewall rules (because Docker bypasses UFW by default — and most tutorials don't mention this), port forwarding, IPv6 configuration, and integration with Fail2Ban.
This guide takes you from basic UFW usage to advanced firewall management for a multi-service Ubuntu VPS. Every rule is explained, every command is tested, and every gotcha is documented.
MassiveGRID Ubuntu VPS includes: Ubuntu 24.04 LTS pre-installed · Proxmox HA cluster with automatic failover · Ceph 3x replicated NVMe storage · Independent CPU/RAM/storage scaling · 12 Tbps DDoS protection · 4 global datacenter locations · 100% uptime SLA · 24/7 human support rated 9.5/10
Deploy a self-managed VPS — from $1.99/mo
Need dedicated resources? — from $19.80/mo
Want fully managed hosting? — we handle everything
Understanding UFW, iptables, and nftables
UFW (Uncomplicated Firewall) is not a firewall itself — it's a frontend. Understanding the layers helps you debug problems:
| Layer | Component | Role |
|---|---|---|
| User interface | UFW | Translates simple commands into iptables/nftables rules |
| Rule engine | iptables / nftables | Manages kernel-level packet filtering rules |
| Kernel | netfilter | The actual packet filtering framework in the Linux kernel |
On Ubuntu 24.04, UFW uses iptables by default, which itself uses the nftables backend. You can verify:
# Check which backend iptables uses
sudo iptables --version
# Output: iptables v1.8.10 (nf_tables)
# See UFW's generated iptables rules
sudo iptables -L -n -v
# See the raw UFW rule files
ls -la /etc/ufw/
UFW stores its rules in several files:
/etc/ufw/before.rules— rules processed before user rules (NAT, ICMP, loopback)/etc/ufw/user.rules— your UFW rules (generated fromufw allow/denycommands)/etc/ufw/after.rules— rules processed after user rules/etc/ufw/before6.rules,/etc/ufw/user6.rules,/etc/ufw/after6.rules— IPv6 equivalents/etc/ufw/ufw.conf— UFW configuration (enabled/disabled, logging level)
When you run sudo ufw allow 80/tcp, UFW writes a rule to /etc/ufw/user.rules and reloads iptables. When you understand this, you can edit the rule files directly for advanced configurations that UFW's CLI doesn't support.
Application Profiles
UFW application profiles let you manage firewall rules by service name instead of port numbers. They're stored in /etc/ufw/applications.d/:
# List available profiles
sudo ufw app list
# Output:
# Available applications:
# Nginx Full
# Nginx HTTP
# Nginx HTTPS
# OpenSSH
View a profile's details:
sudo ufw app info "Nginx Full"
# Output:
# Profile: Nginx Full
# Title: Web Server (Nginx, HTTP + HTTPS)
# Description: Small, but very powerful and efficient web server
#
# Ports:
# 80,443/tcp
Use profiles in rules:
sudo ufw allow "Nginx Full"
sudo ufw allow OpenSSH
Creating Custom Application Profiles
Create profiles for your own services. This is especially useful for applications running on non-standard ports:
sudo nano /etc/ufw/applications.d/custom-apps
[NodeApp]
title=Node.js Application
description=Custom Node.js application server
ports=3000/tcp
[PostgreSQL]
title=PostgreSQL Database
description=PostgreSQL database server
ports=5432/tcp
[Redis]
title=Redis Cache
description=Redis in-memory cache server
ports=6379/tcp
[Grafana]
title=Grafana Dashboard
description=Grafana monitoring dashboard
ports=3000/tcp
[AppStack]
title=Full Application Stack
description=Nginx + Node.js + monitoring
ports=80,443/tcp|3000/tcp|9090/tcp
Update and use them:
# Reload application profiles
sudo ufw app update custom-apps
# Verify
sudo ufw app list
# Allow by profile
sudo ufw allow from 10.0.0.0/24 to any app PostgreSQL
sudo ufw allow "Nginx Full"
Rate Limiting
UFW's built-in rate limiting blocks IP addresses that make too many connections in a short period. It's a simple but effective defense against brute-force attacks.
SSH Rate Limiting
# Rate limit SSH (blocks IPs with 6+ connections in 30 seconds)
sudo ufw limit ssh
# Or with specific port
sudo ufw limit 2222/tcp
This creates an iptables rule that allows a maximum of 6 connections within 30 seconds from a single IP. After that, the IP is blocked until the rate drops.
Check the generated rule:
sudo iptables -L ufw-user-input -n -v | grep 22
HTTP Rate Limiting
UFW's limit command works for any port, but its fixed threshold (6 connections / 30 seconds) is too aggressive for HTTP. For web traffic rate limiting, you're better off using Nginx's built-in rate limiting or Fail2Ban. However, for API endpoints or admin panels:
# Rate limit a specific admin port
sudo ufw limit 8080/tcp comment 'Rate limit admin panel'
Advanced Rate Limiting with iptables
For custom thresholds, you need to drop to iptables directly. Add rules to /etc/ufw/before.rules before the COMMIT line in the *filter section:
sudo nano /etc/ufw/before.rules
Add before the final COMMIT:
# Custom rate limiting for port 443 (20 connections per minute per IP)
-A ufw-before-input -p tcp --dport 443 -m conntrack --ctstate NEW -m recent --set --name HTTPS
-A ufw-before-input -p tcp --dport 443 -m conntrack --ctstate NEW -m recent --update --seconds 60 --hitcount 20 --name HTTPS -j ufw-user-limit
-A ufw-before-input -p tcp --dport 443 -j ufw-user-limit-accept
# Reload UFW to apply
sudo ufw reload
UFW and Docker: The Critical Fix
This is the single most important section in this guide. Docker bypasses UFW entirely by default. When you publish a container port with -p 8080:80, Docker writes iptables rules directly, completely bypassing UFW. Your carefully crafted firewall rules don't apply to Docker containers.
Test it yourself:
# Start a container on port 8080
docker run -d -p 8080:80 nginx
# Check UFW status — port 8080 is NOT listed
sudo ufw status
# But it's accessible from the internet anyway
curl http://YOUR_SERVER_IP:8080
# Nginx welcome page loads — UFW was bypassed
This happens because Docker manipulates the DOCKER chain in iptables' nat and filter tables, which are processed before UFW's rules.
The Fix: Disable Docker's iptables Management
There are several approaches. Here's the most reliable one:
Option 1: Bind Containers to localhost
The simplest fix — bind containers to 127.0.0.1 instead of 0.0.0.0:
# Instead of this (accessible from internet):
docker run -d -p 8080:80 nginx
# Do this (only accessible from localhost):
docker run -d -p 127.0.0.1:8080:80 nginx
Then use Nginx as a reverse proxy to forward traffic from port 80/443 to 127.0.0.1:8080. See our Nginx reverse proxy guide for the complete setup. UFW controls access to Nginx on ports 80/443 as normal.
Option 2: Disable Docker iptables and Manage Manually
Create or edit the Docker daemon configuration:
sudo nano /etc/docker/daemon.json
{
"iptables": false
}
sudo systemctl restart docker
Warning: Disabling Docker's iptables management means container-to-container networking may break. You'll need to add manual iptables rules for inter-container communication. This approach is best for simple setups where all containers communicate through a reverse proxy.
Option 3: UFW-Docker Integration (Recommended for Complex Setups)
The ufw-docker utility adds UFW rules that work with Docker's networking. Install it:
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
# Install the UFW rules
sudo ufw-docker install
# Reload UFW
sudo ufw reload
Now manage Docker container access through UFW:
# Allow public access to a container's published port
sudo ufw-docker allow nginx-container 80/tcp
# Allow access only from specific IP
sudo ufw-docker allow nginx-container 80/tcp 10.0.0.50
# Delete the rule
sudo ufw-docker delete allow nginx-container 80/tcp
This is the cleanest solution for VPS setups running Docker alongside UFW. For a full Docker setup guide, see our Docker installation guide, and for Traefik-based setups, see our Traefik guide.
Subnet and IP Range Rules
Control access based on source IP addresses and subnets:
Allow Specific IPs
# Allow SSH from a specific IP
sudo ufw allow from 203.0.113.50 to any port 22
# Allow PostgreSQL from application server
sudo ufw allow from 10.0.1.10 to any port 5432
# Allow all traffic from a trusted IP
sudo ufw allow from 203.0.113.50
Allow Subnets
# Allow all traffic from private network
sudo ufw allow from 10.0.0.0/24
# Allow SSH from office subnet
sudo ufw allow from 192.168.1.0/24 to any port 22
# Allow database access from application subnet
sudo ufw allow from 10.0.1.0/24 to any port 5432 proto tcp
Deny Specific IPs
# Block a specific IP (insert at top of rules so it's evaluated first)
sudo ufw insert 1 deny from 198.51.100.99
# Block a subnet
sudo ufw deny from 198.51.100.0/24
# Block a specific IP from reaching a specific port
sudo ufw deny from 198.51.100.99 to any port 443
Interface-Specific Rules
If your VPS has multiple network interfaces (e.g., public and private):
# Allow PostgreSQL only on private interface
sudo ufw allow in on eth1 to any port 5432
# Allow all traffic on loopback
sudo ufw allow in on lo
# Allow monitoring on private interface only
sudo ufw allow in on eth1 to any port 9090 proto tcp
Port Forwarding with UFW
Port forwarding (NAT) requires editing UFW's rule files directly. Common use case: forwarding traffic from a public port to an internal service.
Enable IP Forwarding
sudo nano /etc/ufw/sysctl.conf
Uncomment or add:
net/ipv4/ip_forward=1
Configure NAT Rules
Edit /etc/ufw/before.rules and add NAT rules before the *filter section:
sudo nano /etc/ufw/before.rules
Add at the top of the file (before any existing content):
# NAT table rules
*nat
:PREROUTING ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
# Forward port 8443 to internal server 10.0.1.10:443
-A PREROUTING -p tcp --dport 8443 -j DNAT --to-destination 10.0.1.10:443
# Masquerade outgoing traffic
-A POSTROUTING -s 10.0.1.0/24 -o eth0 -j MASQUERADE
COMMIT
Then allow the forwarded traffic in the filter rules:
# Allow forwarded traffic to the internal server
sudo ufw route allow proto tcp from any to 10.0.1.10 port 443
# Reload
sudo ufw reload
IPv6 Configuration
By default, UFW manages both IPv4 and IPv6 rules if IPv6 is enabled. Verify:
# Check if IPv6 is enabled in UFW
grep IPV6 /etc/default/ufw
# Output: IPV6=yes
# View IPv6 rules
sudo ip6tables -L -n -v
IPv6 rules mirror IPv4 rules when you use ufw allow. For IPv6-specific rules:
# Allow SSH over IPv6 from a specific address
sudo ufw allow from 2001:db8::1 to any port 22
# Allow HTTP/HTTPS over IPv6
sudo ufw allow from ::/0 to any port 80 proto tcp
sudo ufw allow from ::/0 to any port 443 proto tcp
Disabling IPv6 (If Not Needed)
If your VPS doesn't use IPv6, disable it in UFW to reduce attack surface:
sudo nano /etc/default/ufw
Change:
IPV6=no
sudo ufw reload
Logging Configuration and Log Analysis
UFW logging helps you understand what traffic is being blocked and why.
Set Logging Level
# Available levels: off, low, medium, high, full
sudo ufw logging medium
# low: logs blocked packets matching default policy
# medium: logs blocked + rate-limited + invalid packets
# high: logs all blocked + all allowed packets (noisy)
# full: everything, no rate limiting on log entries
For most VPS setups, medium is the right balance.
Reading UFW Logs
Logs go to /var/log/ufw.log (or /var/log/kern.log on some systems):
# View recent firewall events
sudo tail -50 /var/log/ufw.log
# Watch logs in real time
sudo tail -f /var/log/ufw.log
# Count blocked connections by source IP (top 20)
sudo grep "BLOCK" /var/log/ufw.log | awk '{for(i=1;i<=NF;i++) if($i ~ /^SRC=/) print $i}' | sort | uniq -c | sort -rn | head -20
A typical UFW log entry:
Feb 28 14:23:17 vps kernel: [UFW BLOCK] IN=eth0 OUT= MAC=... SRC=198.51.100.99 DST=10.0.1.5 LEN=60 TOS=0x00 PREC=0x00 TTL=49 ID=12345 DF PROTO=TCP SPT=54321 DPT=22 WINDOW=65535 RES=0x00 SYN URGP=0
Key fields:
[UFW BLOCK]— action taken (BLOCK, ALLOW, AUDIT)SRC=198.51.100.99— source IP addressDST=10.0.1.5— destination IP addressDPT=22— destination port (this was an SSH attempt)PROTO=TCP— protocolSYN— TCP SYN flag (connection attempt)
Automated Log Analysis Script
#!/bin/bash
# /usr/local/bin/ufw-report.sh
# Generate daily UFW firewall report
LOG="/var/log/ufw.log"
DATE=$(date -d "yesterday" +"%b %d")
echo "=== UFW Firewall Report for $DATE ==="
echo ""
echo "--- Blocked Connections by Source IP (Top 20) ---"
grep "$DATE" "$LOG" | grep "BLOCK" | \
awk '{for(i=1;i<=NF;i++) if($i ~ /^SRC=/) print substr($i,5)}' | \
sort | uniq -c | sort -rn | head -20
echo ""
echo "--- Blocked Connections by Destination Port (Top 15) ---"
grep "$DATE" "$LOG" | grep "BLOCK" | \
awk '{for(i=1;i<=NF;i++) if($i ~ /^DPT=/) print substr($i,5)}' | \
sort | uniq -c | sort -rn | head -15
echo ""
echo "--- Total Blocked: $(grep "$DATE" "$LOG" | grep -c "BLOCK")"
echo "--- Total Allowed: $(grep "$DATE" "$LOG" | grep -c "ALLOW")"
sudo chmod +x /usr/local/bin/ufw-report.sh
Run it daily with cron. For comprehensive log management, see our log management guide.
Integrating UFW with Fail2Ban
UFW's rate limiting is basic — 6 connections in 30 seconds, fixed. Fail2Ban provides flexible, pattern-based banning. The two work together: UFW handles static rules, Fail2Ban handles dynamic responses to detected attacks.
For a complete Fail2Ban setup, see our Fail2Ban advanced configuration guide. Here's how to make Fail2Ban use UFW as its ban action:
Configure Fail2Ban to Use UFW
sudo nano /etc/fail2ban/jail.local
[DEFAULT]
banaction = ufw
banaction_allports = ufw
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 5
findtime = 600
bantime = 3600
[nginx-limit-req]
enabled = true
port = http,https
filter = nginx-limit-req
logpath = /var/log/nginx/error.log
maxretry = 10
findtime = 120
bantime = 600
[nginx-botsearch]
enabled = true
port = http,https
filter = nginx-botsearch
logpath = /var/log/nginx/access.log
maxretry = 2
findtime = 300
bantime = 86400
The Fail2Ban UFW action file is at /etc/fail2ban/action.d/ufw.conf. When Fail2Ban detects an attack, it runs ufw insert 1 deny from <ip> to block the attacker. The insert 1 ensures the deny rule is evaluated before allow rules.
# Restart Fail2Ban
sudo systemctl restart fail2ban
# Verify integration — check banned IPs appear in UFW
sudo fail2ban-client status sshd
sudo ufw status numbered
UFW for a Multi-Service VPS
Here's a complete firewall ruleset for a VPS running Nginx, Node.js, PostgreSQL, Redis, and Docker:
# Reset to clean state (CAREFUL — this disconnects SSH if you're remote)
# sudo ufw --force reset
# Set default policies
sudo ufw default deny incoming
sudo ufw default allow outgoing
# SSH (rate limited)
sudo ufw limit 22/tcp comment 'SSH rate limited'
# Web traffic
sudo ufw allow 80/tcp comment 'HTTP'
sudo ufw allow 443/tcp comment 'HTTPS'
# PostgreSQL — only from application servers
sudo ufw allow from 10.0.1.10 to any port 5432 proto tcp comment 'PostgreSQL from app-01'
sudo ufw allow from 10.0.1.11 to any port 5432 proto tcp comment 'PostgreSQL from app-02'
# Redis — only from localhost (already default, but explicit)
sudo ufw allow from 127.0.0.1 to any port 6379 proto tcp comment 'Redis localhost'
# Node.js app — only from localhost (Nginx proxies to it)
sudo ufw allow from 127.0.0.1 to any port 3000 proto tcp comment 'Node.js via Nginx proxy'
# Monitoring (Prometheus/Grafana) — only from monitoring server
sudo ufw allow from 10.0.2.5 to any port 9090 proto tcp comment 'Prometheus from monitor'
sudo ufw allow from 10.0.2.5 to any port 9100 proto tcp comment 'Node exporter from monitor'
# Enable
sudo ufw enable
# Verify
sudo ufw status verbose
Every MassiveGRID Cloud VPS is protected by 12 Tbps DDoS mitigation at the network level. UFW adds OS-level firewall control on top. Together, they provide defense in depth — MassiveGRID absorbs volumetric DDoS attacks before they reach your VPS, while UFW controls which services are accessible and from where. Deploy a Cloud VPS.
Auditing and Maintaining Firewall Rules
Firewall rules accumulate over time. Old rules for services you've decommissioned create unnecessary attack surface. Audit regularly.
List Rules with Numbers
sudo ufw status numbered
# Output:
# Status: active
#
# To Action From
# -- ------ ----
# [ 1] 22/tcp LIMIT IN Anywhere # SSH rate limited
# [ 2] 80/tcp ALLOW IN Anywhere # HTTP
# [ 3] 443/tcp ALLOW IN Anywhere # HTTPS
# [ 4] 5432/tcp ALLOW IN 10.0.1.10 # PostgreSQL from app-01
# [ 5] 5432/tcp ALLOW IN 10.0.1.11 # PostgreSQL from app-02
# ...
Delete Rules
# Delete by rule number
sudo ufw delete 5
# Delete by rule specification
sudo ufw delete allow 8080/tcp
Audit Script
#!/bin/bash
# /usr/local/bin/ufw-audit.sh
# Audit firewall rules against active services
echo "=== UFW Rule Audit ==="
echo ""
echo "--- Active UFW Rules ---"
sudo ufw status numbered
echo ""
echo "--- Listening Services ---"
sudo ss -tlnp | awk 'NR>1 {print $4, $6}' | column -t
echo ""
echo "--- Rules for ports with NO listening service ---"
while IFS= read -r line; do
PORT=$(echo "$line" | grep -oP '\d+(?=/tcp|/udp)' | head -1)
if [ -n "$PORT" ]; then
LISTENING=$(sudo ss -tlnp | grep ":$PORT ")
if [ -z "$LISTENING" ]; then
echo " UNUSED RULE: $line"
fi
fi
done < <(sudo ufw status | grep -E "ALLOW|LIMIT" | grep -v "(v6)")
sudo chmod +x /usr/local/bin/ufw-audit.sh
Run this monthly to identify rules for services that no longer exist.
Backup and Restore Rules
# Backup current rules
sudo cp /etc/ufw/user.rules /etc/ufw/user.rules.backup.$(date +%Y%m%d)
sudo cp /etc/ufw/user6.rules /etc/ufw/user6.rules.backup.$(date +%Y%m%d)
sudo cp /etc/ufw/before.rules /etc/ufw/before.rules.backup.$(date +%Y%m%d)
# Export rules as commands (for documentation)
sudo ufw status numbered > /root/ufw-rules-export-$(date +%Y%m%d).txt
# Restore from backup
sudo cp /etc/ufw/user.rules.backup.20260228 /etc/ufw/user.rules
sudo cp /etc/ufw/user6.rules.backup.20260228 /etc/ufw/user6.rules
sudo ufw reload
Common Scenarios and Rule Templates
VPN Server (WireGuard)
# Allow WireGuard UDP port
sudo ufw allow 51820/udp comment 'WireGuard VPN'
# Allow forwarding for VPN traffic (in /etc/ufw/before.rules)
# -A ufw-before-forward -i wg0 -j ACCEPT
# -A ufw-before-forward -o wg0 -j ACCEPT
Mail Server
sudo ufw allow 25/tcp comment 'SMTP'
sudo ufw allow 587/tcp comment 'SMTP submission'
sudo ufw allow 993/tcp comment 'IMAPS'
sudo ufw allow 465/tcp comment 'SMTPS'
Git Server
If you're running a self-hosted Git server (see our Git server guide):
sudo ufw allow 22/tcp comment 'Git over SSH'
sudo ufw allow 3000/tcp comment 'Gitea web interface'
Development Server
# Allow dev ports only from your IP
MY_IP="203.0.113.50"
sudo ufw allow from $MY_IP to any port 3000 proto tcp comment 'Dev server'
sudo ufw allow from $MY_IP to any port 5173 proto tcp comment 'Vite dev server'
sudo ufw allow from $MY_IP to any port 8080 proto tcp comment 'API dev server'
Performance Considerations
Firewall rules have a processing cost. Every packet is evaluated against the rule chain. On a VPS with hundreds of rules, this overhead becomes measurable.
Running a multi-service VPS with complex firewall rules? If running a multi-service VPS with complex firewall rules, dedicated resources ensure firewall processing overhead doesn't affect performance — from $19.80/mo.
Optimization tips:
- Order matters: Rules are evaluated top-to-bottom. Put the most frequently matched rules first (HTTP/HTTPS, then SSH, then application-specific).
- Use subnets instead of individual IPs: One rule for
10.0.1.0/24is faster than 254 rules for individual IPs. - Remove unused rules: Run the audit script above monthly.
- Use connection tracking: UFW does this by default — established connections bypass the full rule chain.
Emergency Procedures
Locked Out of SSH
If you accidentally block SSH access:
- Use your hosting provider's console access (MassiveGRID provides VNC/noVNC console access through the control panel)
- Log in via console and reset UFW:
sudo ufw disable sudo ufw reset sudo ufw allow 22/tcp sudo ufw enable
Disable UFW Temporarily
# Disable (removes all rules from iptables but preserves config)
sudo ufw disable
# Re-enable (restores all saved rules)
sudo ufw enable
Prefer Managed Firewall?
Configuring and maintaining firewall rules across multiple services, managing Docker integration, coordinating with Fail2Ban, and auditing rules — it's real, ongoing work. If you'd rather have experts handle your security infrastructure:
Let us manage your firewall and security. MassiveGRID Managed Dedicated Cloud Servers include firewall configuration, security hardening, intrusion detection, 24/7 monitoring, and incident response — all handled by our team.
Summary
UFW is more powerful than most guides reveal. Here's a reference of what we covered:
| Topic | Key Command / File |
|---|---|
| Application profiles | /etc/ufw/applications.d/ |
| Rate limiting | sudo ufw limit 22/tcp |
| Docker bypass fix | Bind to 127.0.0.1 or use ufw-docker |
| Subnet rules | sudo ufw allow from 10.0.0.0/24 to any port 5432 |
| Port forwarding | /etc/ufw/before.rules NAT section |
| IPv6 | /etc/default/ufw — IPV6=yes |
| Logging | sudo ufw logging medium |
| Fail2Ban integration | banaction = ufw in jail.local |
| Rule audit | sudo ufw status numbered + audit script |
| Emergency reset | sudo ufw disable && sudo ufw reset |
The combination of MassiveGRID's 12 Tbps DDoS protection and a properly configured UFW firewall gives you defense in depth. Start with a Cloud VPS, configure your firewall rules following this guide, and upgrade to a Dedicated VPS when your rule complexity demands guaranteed resources.