UFW on Linux: Secure Your VPS Like a Pro

You just spun up a fresh VPS. You SSH in, deploy your app, and everything works. Life is good — until you check the logs and find thousands of brute-force attempts on port 22, random bots scanning every port, and suspicious traffic from IPs you've never seen.
Welcome to the internet. Your server is under attack from the moment it goes online.
The first line of defense? A firewall. And on Ubuntu, the simplest way to set one up is UFW — Uncomplicated Firewall.
What is UFW?
UFW is a user-friendly frontend for iptables, the powerful but notoriously complex Linux firewall. Ubuntu ships with UFW pre-installed (but disabled by default).
Think of it this way:
- iptables = the engine (powerful, low-level, hard to configure)
- UFW = the steering wheel (simple commands, sane defaults)
UFW doesn't replace iptables — it generates iptables rules for you, so you get the same security with a fraction of the complexity.
Getting Started
Installation
UFW comes pre-installed on Ubuntu. If it's missing for some reason:
sudo apt update
sudo apt install ufwCheck the status:
sudo ufw status
# Status: inactiveThe Golden Rule: Allow SSH Before Enabling
This is the most important step. If you enable UFW without allowing SSH, you'll lock yourself out of your own server.
# ALWAYS do this first
sudo ufw allow ssh
# or equivalently
sudo ufw allow 22/tcpEnable UFW
sudo ufw enableYou'll see a warning: "Command may disrupt existing ssh connections." Type y — your SSH rule is already in place.
Verify:
sudo ufw status verboseStatus: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip
To Action From
-- ------ ----
22/tcp ALLOW IN Anywhere
22/tcp (v6) ALLOW IN Anywhere (v6)Essential Commands
Allow and Deny Rules
# Allow a specific port
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Allow a port range
sudo ufw allow 8000:8100/tcp
# Allow from a specific IP
sudo ufw allow from 203.0.113.50
# Allow from a specific IP to a specific port
sudo ufw allow from 203.0.113.50 to any port 5432
# Allow a subnet
sudo ufw allow from 10.0.0.0/24
# Deny a specific IP (block an attacker)
sudo ufw deny from 23.94.168.0/24
# Deny a specific port
sudo ufw deny 3306/tcpUsing Application Profiles
UFW comes with pre-configured profiles for common applications:
# List available profiles
sudo ufw app list
# Allow by profile name
sudo ufw allow 'Nginx Full' # ports 80 + 443
sudo ufw allow 'Nginx HTTP' # port 80 only
sudo ufw allow 'Nginx HTTPS' # port 443 only
sudo ufw allow 'OpenSSH' # port 22
# View profile details
sudo ufw app info 'Nginx Full'Deleting Rules
# List rules with numbers
sudo ufw status numbered
# Delete by rule number
sudo ufw delete 3
# Delete by rule specification
sudo ufw delete allow 8080/tcpReset Everything
# Nuclear option — removes ALL rules and disables UFW
sudo ufw resetReal-World VPS Configuration
Here's a complete setup for a typical web server running Nginx + a backend app + PostgreSQL:
# 1. Set default policies
sudo ufw default deny incoming
sudo ufw default allow outgoing
# 2. Allow SSH (ALWAYS first)
sudo ufw allow ssh
# 3. Allow web traffic
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# 4. Allow your backend app (e.g., on port 3000) only from localhost
# This means Nginx reverse-proxies to it — no direct external access
sudo ufw allow from 127.0.0.1 to any port 3000
# 5. Allow PostgreSQL only from your app server's IP
sudo ufw allow from 10.0.0.5 to any port 5432
# 6. Enable UFW
sudo ufw enableThe key principle: deny everything by default, then allow only what's needed.
Docker and UFW — The Gotcha
If you run Docker on your VPS, there's a critical issue you need to know:
Docker bypasses UFW entirely. When Docker publishes a port (
-p 5432:5432), it writes its own iptables rules that UFW doesn't control. Your "deny all" policy won't block traffic to Docker-exposed ports.
Solutions:
Option 1: Bind Docker ports to localhost only
# docker-compose.yml
services:
postgres:
ports:
- "127.0.0.1:5432:5432" # Only accessible from the host# Or with docker run
docker run -p 127.0.0.1:5432:5432 postgresOption 2: Use Docker networks instead of published ports
# docker-compose.yml
services:
app:
networks:
- internal
postgres:
networks:
- internal
# No 'ports' section — only accessible within the Docker network
networks:
internal:
driver: bridgeOption 3: Configure Docker to respect UFW
Edit /etc/docker/daemon.json:
{
"iptables": false
}Then restart Docker:
sudo systemctl restart dockerWarning: Setting
"iptables": falsemeans Docker can't manage its own networking rules. You'll need to manually configure iptables rules for container communication. This approach requires more expertise.
Recommended approach: Use Option 1 (bind to localhost) for databases and internal services, and let Nginx handle external traffic. This is the simplest and most reliable method.
Best Practices for VPS Hardening
1. Change the Default SSH Port
Moving SSH off port 22 won't stop a determined attacker, but it eliminates 99% of automated brute-force bots:
# Edit SSH config
sudo nano /etc/ssh/sshd_config
# Change this line:
# Port 22
Port 2222
# Restart SSH
sudo systemctl restart sshdUpdate UFW:
sudo ufw allow 2222/tcp
sudo ufw delete allow 22/tcp
# or: sudo ufw delete allow sshImportant: Open a NEW SSH session on port 2222 before closing your current one. If something went wrong, you still have access through the existing connection.
2. Rate Limit SSH Connections
UFW can rate-limit connections — if an IP tries to connect more than 6 times in 30 seconds, it gets blocked:
sudo ufw limit ssh
# or if using a custom port:
sudo ufw limit 2222/tcpThis is a simple but effective defense against brute-force attacks.
3. Disable Password Authentication
Use SSH keys instead of passwords. This alone eliminates nearly all brute-force risks:
# On your local machine — generate a key pair if you haven't
ssh-keygen -t ed25519 -C "your-email@example.com"
# Copy the public key to your server
ssh-copy-id -p 2222 user@your-server-ipThen on the server, disable password login:
sudo nano /etc/ssh/sshd_configPasswordAuthentication no
PubkeyAuthentication yes
PermitRootLogin nosudo systemctl restart sshd4. Enable Logging
# Set logging level (low, medium, high, full)
sudo ufw logging mediumCheck logs:
# UFW logs go to syslog
sudo grep UFW /var/log/syslog | tail -20
# Or on newer Ubuntu versions
sudo journalctl | grep UFW | tail -205. Only Open Ports You Actually Need
Audit your open ports regularly:
# Check what UFW allows
sudo ufw status numbered
# Check what's actually listening
sudo ss -tlnpIf ss shows a service listening on a port that UFW doesn't allow, that's good — it means UFW is blocking external access. If UFW allows a port that nothing is listening on, remove the rule — no point having it open.
6. Use Fail2Ban Alongside UFW
UFW handles static rules. Fail2Ban watches logs and dynamically bans IPs that show malicious behavior:
sudo apt install fail2banCreate a local config:
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local[sshd]
enabled = true
port = 2222
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
findtime = 600
banaction = ufwsudo systemctl enable fail2ban
sudo systemctl start fail2banNow Fail2Ban will automatically add UFW deny rules for IPs that fail SSH login 3 times within 10 minutes.
Check banned IPs:
sudo fail2ban-client status sshdComplete VPS Security Checklist
Here's a quick reference for securing a fresh Ubuntu VPS:
Firewall Setup:
✅ Set default deny incoming, allow outgoing
✅ Allow SSH (on a custom port)
✅ Allow only ports 80 and 443 for web traffic
✅ Bind Docker ports to 127.0.0.1
✅ Enable UFW logging
SSH Hardening:
✅ Change SSH port from 22
✅ Disable password authentication
✅ Disable root login
✅ Use Ed25519 SSH keys
✅ Enable UFW rate limiting on SSH port
Additional Protection:
✅ Install and configure Fail2Ban
✅ Keep system updated (sudo apt update && sudo apt upgrade)
✅ Audit open ports regularly with ss -tlnp
✅ Remove unnecessary UFW rules
✅ Set up automatic security updates (unattended-upgrades)
Common Mistakes to Avoid
Enabling UFW before allowing SSH — You'll lock yourself out. Always sudo ufw allow ssh first.
Exposing database ports to the internet — PostgreSQL (5432), MySQL (3306), Redis (6379) should never be publicly accessible. Bind to localhost or use firewall rules to restrict access.
Trusting Docker + UFW together — Docker bypasses UFW by default. Always bind Docker ports to 127.0.0.1 for internal services.
Using ufw allow without specifying the protocol — sudo ufw allow 3000 allows both TCP and UDP. Be explicit: sudo ufw allow 3000/tcp.
Forgetting IPv6 — UFW handles IPv6 by default (check /etc/default/ufw for IPV6=yes), but make sure your rules work for both protocols.
Quick Reference
| Task | Command |
|---|---|
| Check status | sudo ufw status verbose |
| Enable UFW | sudo ufw enable |
| Disable UFW | sudo ufw disable |
| Allow port | sudo ufw allow 80/tcp |
| Deny port | sudo ufw deny 3306/tcp |
| Allow from IP | sudo ufw allow from 1.2.3.4 |
| Rate limit | sudo ufw limit ssh |
| Delete rule | sudo ufw delete allow 8080/tcp |
| List numbered | sudo ufw status numbered |
| Reset all | sudo ufw reset |
| View logs | sudo grep UFW /var/log/syslog |
Wrapping Up
UFW is one of those tools that takes 5 minutes to set up but saves you from countless headaches. The defaults are sane, the commands are readable, and combined with Fail2Ban and proper SSH hardening, you've got a solid security foundation for your VPS.
The key takeaway: default deny, explicit allow, and always verify. Your server is only as secure as the rules you define — so define them carefully, audit them regularly, and sleep a little better at night knowing your VPS isn't wide open to the internet.
📬 Subscribe to Newsletter
Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.
We respect your privacy. Unsubscribe at any time.
💬 Comments
Sign in to leave a comment
We'll never post without your permission.