How to Set Up a Linux VPS Firewall with UFW and nftables
Configure a default-deny firewall on your Linux VPS using either UFW or nftables. Two paths, one goal: only the ports you choose stay open.
A fresh VPS has no firewall rules. Every port is open. Automated scanners will find your server within minutes of it going live and start probing SSH, testing default credentials, and looking for exposed services. A default-deny firewall is the first layer of defense.
This tutorial covers two tools:
- UFW -- a simplified frontend that generates firewall rules from short commands. Best for getting a secure baseline fast.
- nftables -- the native Linux firewall framework that replaces iptables. Best for operators who need granular control, Docker compatibility, or per-IP rate limiting.
Pick the path that matches your needs. Both end with the same result: a verified default-deny firewall where only the ports you explicitly allow are reachable.
Should you use UFW or nftables on your VPS?
UFW is a simplified frontend that generates iptables/nftables rules from short commands like ufw allow 22. nftables is the native Linux firewall framework that replaces iptables, using tables, chains, and rules with a cleaner syntax. UFW suits beginners who want fast defaults. nftables suits operators who need rate limiting with meters, named sets, or Docker-compatible rules.
| Feature | UFW | nftables |
|---|---|---|
| Learning curve | Low. One-line commands. | Moderate. Table/chain/rule structure. |
| Default on | Ubuntu (installed, inactive) | Debian 12 (installed, minimal config) |
| IPv6 support | Automatic (dual-stack) | Manual rules required (inet family) |
| Rate limiting | ufw limit (per-rule) |
Meters with per-IP tracking |
| Docker compatible | No. Docker bypasses UFW rules. | Yes. nftables works with Docker's DOCKER-USER chain. |
| Config persistence | Automatic on ufw enable |
/etc/nftables.conf + systemd |
| Best for | Single-app VPS, first server | Multi-service, Docker hosts, production |
If you run Docker, skip UFW or read Fix Docker Bypassing UFW: 4 Tested Solutions for Your VPS for the workaround. Docker manipulates iptables directly and bypasses UFW entirely, exposing container ports to the internet even when UFW says they are blocked.
Prerequisites
Before changing firewall rules, confirm two things:
- You have SSH access and it works. You are connected to your VPS right now over SSH.
- You have console access as a fallback. Your hosting provider's control panel offers a VNC or serial console. If you lock yourself out of SSH, you can still reach the server through the console.
This tutorial targets Ubuntu 24.04 and Debian 12. Commands work on both unless noted otherwise.
Check your OS version:
cat /etc/os-release | grep PRETTY_NAME
Expected output:
PRETTY_NAME="Ubuntu 24.04.4 LTS"
or
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
How do you set up UFW on Ubuntu 24.04?
UFW is installed by default on Ubuntu 24.04 but inactive. On Debian 12, install it first:
apt update && apt install -y ufw
Check the current status:
ufw status
Expected output:
Status: inactive
How do you enable default-deny with UFW?
Set default policies to drop all incoming traffic, then explicitly allow only the ports your services need. With UFW: set the defaults, allow SSH, then enable. The order matters. If you enable UFW before allowing SSH, you lock yourself out.
Set the default policies:
ufw default deny incoming
ufw default allow outgoing
This tells UFW to drop all inbound connections by default and allow all outbound. Your server can still reach the internet (for updates, DNS, etc.) but nothing from outside gets in unless you create a rule.
How do you allow SSH without getting locked out?
Allow SSH before enabling UFW. This is the single most important step:
ufw allow 22/tcp comment 'SSH'
If you changed your SSH port (e.g., to 2222), use that port number instead.
Now enable UFW:
ufw enable
You will see:
Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
Firewall is active and enabled on system startup
Your current SSH session stays alive. UFW adds the allow rule before activating, so existing connections are not dropped.
Verify the rules:
ufw status verbose
Expected output:
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip
To Action From
-- ------ ----
22/tcp ALLOW IN Anywhere # SSH
22/tcp (v6) ALLOW IN Anywhere (v6) # SSH
Both IPv4 and IPv6 rules appear. UFW creates dual-stack rules automatically when IPV6=yes is set in /etc/default/ufw (the default on Ubuntu 24.04).
How do you allow web traffic with UFW?
If your VPS runs a web server, allow HTTP and HTTPS:
ufw allow 80/tcp comment 'HTTP'
ufw allow 443/tcp comment 'HTTPS'
ufw status numbered
Status: active
To Action From
-- ------ ----
[ 1] 22/tcp ALLOW IN Anywhere # SSH
[ 2] 80/tcp ALLOW IN Anywhere # HTTP
[ 3] 443/tcp ALLOW IN Anywhere # HTTPS
[ 4] 22/tcp (v6) ALLOW IN Anywhere (v6) # SSH
[ 5] 80/tcp (v6) ALLOW IN Anywhere (v6) # HTTP
[ 6] 443/tcp (v6) ALLOW IN Anywhere (v6) # HTTPS
The numbered view helps when you need to delete specific rules later with ufw delete <number>.
How do you rate-limit SSH connections with UFW?
UFW has a built-in rate limiter. It denies connections from an IP that attempts more than 6 connections within 30 seconds. Replace the plain allow rule with a limit rule:
ufw delete allow 22/tcp
ufw limit 22/tcp comment 'SSH rate-limited'
ufw status verbose
The SSH rule now shows LIMIT IN instead of ALLOW IN. This slows brute-force attempts without blocking legitimate access. For stronger protection, combine with Fail2Ban Install and Configure Fail2Ban on a Linux VPS.
How do you allow specific IPs or subnets with UFW?
To restrict access to a service by source IP:
ufw allow from 198.51.100.0/24 to any port 22 proto tcp comment 'SSH from office'
To allow a single IP to reach all ports:
ufw allow from 198.51.100.42 comment 'Trusted admin'
This is useful for management services that should never face the public internet. Combine restricted access with the default deny policy.
How do you delete UFW rules?
Delete by specifying the exact rule. This is the safest method because it works regardless of rule ordering:
ufw delete allow 80/tcp
You can also delete by number. List rules with numbers first:
ufw status numbered
Then delete by number:
ufw delete 2
Be careful with numbered deletion. Rule numbers change whenever you add or remove rules. Always run ufw status numbered immediately before deleting to confirm which rule each number refers to. Deleting the wrong number can lock you out of SSH.
How do you configure UFW logging?
Enable logging to see blocked connections:
ufw logging on
UFW supports three log levels: low, medium, high. The default low logs blocked packets. For troubleshooting, increase temporarily:
ufw logging medium
View logs:
journalctl -k -f | grep UFW
Or check the log file directly:
tail -20 /var/log/ufw.log
Set it back to low after debugging. Higher levels generate significant disk I/O on busy servers.
Does UFW work with IPv6?
Yes. UFW handles IPv4 and IPv6 simultaneously when IPV6=yes is set in /etc/default/ufw. This is enabled by default on Ubuntu 24.04. Verify:
grep IPV6 /etc/default/ufw
Expected:
IPV6=yes
Every ufw allow rule automatically creates matching IPv4 and IPv6 entries. No extra steps needed.
UFW verification checklist
Run these checks to confirm your firewall works:
- List all rules:
ufw status verbose - Check UFW is enabled at boot:
systemctl is-enabled ufw - Test SSH from a second terminal (do not close your current session)
- Test from outside the server -- from your local machine:
nc -zv YOUR_SERVER_IP 22
Expected: Connection to YOUR_SERVER_IP 22 port [tcp/ssh] succeeded!
nc -zv YOUR_SERVER_IP 25
Expected: connection refused or timeout (port 25 is not allowed).
How do you configure nftables on Debian 12?
nftables is the default firewall framework on Debian 12. It replaces iptables with a cleaner syntax and better performance. On Ubuntu 24.04, install it if you want to use it instead of UFW:
apt update && apt install -y nftables
If UFW is active, disable it first. Running both creates conflicting rules:
ufw disable
systemctl disable --now ufw
What are nftables tables, chains, and rules?
nftables organizes firewall rules in a hierarchy:
- Table -- a container that holds chains. The
inetfamily handles both IPv4 and IPv6 in one table. - Chain -- a list of rules that hook into a specific point in packet processing. An
inputchain filters packets destined for the server. Aforwardchain filters packets passing through. - Rule -- a match-action pair. "If the packet matches TCP port 22, accept it."
The core concept: set the chain's policy to drop, then add rules that accept specific traffic. Everything not explicitly allowed is silently dropped.
How do you write a default-deny nftables ruleset?
Set default policies to drop all incoming traffic, then add rules for SSH, HTTP, and HTTPS. With nftables, you write this as a configuration file.
Create the configuration:
cp /etc/nftables.conf /etc/nftables.conf.bak
cat > /etc/nftables.conf << 'NFTEOF'
#!/usr/sbin/nft -f
flush ruleset
table inet firewall {
set ssh_ratelimit {
type ipv4_addr
flags dynamic
timeout 60s
}
set ssh_ratelimit6 {
type ipv6_addr
flags dynamic
timeout 60s
}
chain inbound_ipv4 {
icmp type echo-request limit rate 5/second accept
}
chain inbound_ipv6 {
icmpv6 type { nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } accept
icmpv6 type echo-request limit rate 5/second accept
}
chain inbound {
type filter hook input priority filter; policy drop;
ct state established,related accept
ct state invalid drop
iifname "lo" accept
meta protocol vmap { ip : jump inbound_ipv4, ip6 : jump inbound_ipv6 }
tcp dport 22 ct state new add @ssh_ratelimit { ip saddr limit rate 3/minute burst 5 packets } accept
tcp dport 22 ct state new add @ssh_ratelimit6 { ip6 saddr limit rate 3/minute burst 5 packets } accept
tcp dport { 80, 443 } accept
log prefix "[nftables] Dropped: " counter drop
}
chain forward {
type filter hook forward priority filter; policy drop;
}
chain outbound {
type filter hook output priority filter; policy accept;
}
}
NFTEOF
What each section does:
flush ruleset-- clears any existing rules so you start clean.table inet firewall-- creates a table in theinetfamily (dual-stack IPv4+IPv6).set ssh_ratelimit-- a dynamic set that tracks source IPs for SSH rate limiting. Entries expire after 60 seconds.chain inbound-- the main input chain withpolicy drop. Everything not matched by a rule is dropped.ct state established,related accept-- allows return traffic for connections your server initiated. Without this, outbound connections (apt updates, DNS queries) would break.ct state invalid drop-- drops malformed packets that do not belong to any known connection.iifname "lo" accept-- allows loopback traffic. Many services communicate internally over localhost.meta protocol vmap-- jumps to IPv4 or IPv6-specific chains based on the protocol. This handles ICMP and ICMPv6 separately.tcp dport 22 ... limit rate 3/minute-- allows SSH with per-IP rate limiting. Each source IP gets 3 new connections per minute with a burst of 5.tcp dport { 80, 443 } accept-- allows HTTP and HTTPS.log prefix "[nftables] Dropped: "-- logs all dropped packets to the kernel log for troubleshooting.
How do you handle IPv6 with nftables?
The inet family in the table definition handles both IPv4 and IPv6 in one ruleset. But IPv6 requires specific ICMPv6 types for basic networking to function.
The inbound_ipv6 chain accepts three ICMPv6 types:
nd-neighbor-solicit-- like ARP for IPv6. Without it, your server cannot discover neighbors on the local network.nd-router-advert-- lets routers announce themselves. Required for SLAAC (automatic IPv6 address configuration).nd-neighbor-advert-- responses to neighbor solicitation. Required for IPv6 connectivity.
Blocking any of these breaks IPv6 networking. The ruleset above handles this correctly. If you only have IPv4, these rules are harmless -- they simply never match.
How do you rate-limit connections with nftables meters?
The ruleset above uses dynamic sets (formerly called meters) for per-IP rate limiting on SSH. When a connection arrives, this is what happens:
- A new TCP connection to port 22 arrives from
198.51.100.42. - nftables checks
@ssh_ratelimitfor an entry matching that IP. - If the IP has fewer than 3 connections in the last minute, the connection is accepted and the counter updates.
- If the IP exceeds the limit, the rule does not match, and the packet falls through to the
droppolicy.
The burst 5 packets parameter allows a brief spike, then enforces the steady rate. The timeout 60s on the set means entries are cleaned up automatically, preventing the set from growing indefinitely.
To inspect the current state of the rate limiter:
nft list set inet firewall ssh_ratelimit
This shows which IPs are currently being tracked and their counters.
How do you add rules for additional services?
The ruleset above allows SSH, HTTP, and HTTPS. To add more services, insert rules in the inbound chain before the final log ... drop line.
For example, to allow a Node.js app on port 3000 only from a specific IP:
tcp dport 3000 ip saddr 198.51.100.42 accept
To allow a UDP service like WireGuard on port 51820:
udp dport 51820 accept
To allow a range of ports (e.g., for passive FTP):
tcp dport 40000-50000 accept
After editing /etc/nftables.conf, validate the syntax without applying:
nft -c -f /etc/nftables.conf
The -c flag checks for errors and exits. No output means the syntax is valid.
How do you manage nftables rules at runtime?
You can add and remove rules without editing the config file. These changes are temporary and lost on reboot unless you save them.
Add a rule at runtime:
nft add rule inet firewall inbound tcp dport 8080 accept
List the current ruleset:
nft list ruleset
Delete a rule by its handle number. First, list rules with handles:
nft -a list chain inet firewall inbound
Each rule shows a # handle N suffix. Delete by handle:
nft delete rule inet firewall inbound handle 15
To save runtime changes to the config file:
nft list ruleset > /etc/nftables.conf
Add the shebang line back to the top of the file:
sed -i '1i #!/usr/sbin/nft -f' /etc/nftables.conf
How do you apply and persist nftables rules?
Apply the configuration:
nft -f /etc/nftables.conf
Verify the rules loaded:
nft list ruleset
The complete table with all chains and rules appears. If any syntax errors exist, nft -f prints the line number and error.
Enable nftables at boot:
systemctl enable --now nftables
enable makes it survive reboots. --now starts it immediately. Verify the service is running:
systemctl status nftables
Expected output includes Active: active (exited). The exited status is normal. nftables loads rules into the kernel and exits. The rules remain active in kernel space.
Confirm it is enabled for next boot:
systemctl is-enabled nftables
Expected: enabled.
How do you read nftables logs?
The ruleset includes a log statement before the final drop. Dropped packets appear in the kernel log with the prefix [nftables] Dropped:.
View dropped packets in real time:
journalctl -k -f | grep nftables
A typical log entry:
Mar 19 14:23:01 vps kernel: [nftables] Dropped: IN=eth0 OUT= SRC=203.0.113.55 DST=198.51.100.1 PROTO=TCP SPT=44521 DPT=3389
This tells you: an external IP (203.0.113.55) tried to reach port 3389 (RDP) on your server and was dropped. This is normal scan traffic. If you see your own IP in the SRC field, you have a rule problem.
To avoid log spam on heavily scanned servers, the troubleshooting section below explains how to add a rate limit to the log rule.
nftables verification checklist
- List the full ruleset:
nft list ruleset - Check the service:
systemctl is-enabled nftables - Test SSH from a second terminal (keep your current session open)
- Test from your local machine:
nc -zv YOUR_SERVER_IP 22
Expected: connection succeeds.
nc -zv YOUR_SERVER_IP 25
Expected: connection refused or timeout.
- Check dropped packets in logs:
journalctl -k | grep "nftables"
How do you prevent SSH lockout when changing firewall rules?
Locking yourself out of SSH is the biggest risk when configuring a firewall. Two safeguards prevent this.
Safeguard 1: always test in a second terminal
Before closing your SSH session after changing firewall rules, open a new terminal and SSH into your server. If the new connection works, your rules are correct. If it fails, go back to your original session and fix the rules.
Never close your working SSH session until you have confirmed a second connection works.
Safeguard 2: timed failsafe with at
When testing nftables changes, schedule a job that automatically flushes all rules after 5 minutes. If your new rules lock you out, wait 5 minutes and try again.
at now + 5 minutes <<< 'nft flush ruleset'
If at is not installed:
apt install -y at
systemctl enable --now atd
at now + 5 minutes <<< 'nft flush ruleset'
After confirming your rules work, cancel the failsafe:
atrm $(atq | awk '{print $1}')
For UFW, the equivalent failsafe:
at now + 5 minutes <<< 'ufw disable'
What about Docker and the firewall?
Docker manipulates iptables directly to set up container networking. When you publish a port with -p 8080:80, Docker creates NAT rules that bypass UFW entirely. Your ufw deny rules have no effect on Docker-published ports.
This is the most common firewall surprise on Linux VPS. A port you thought was blocked is wide open because Docker routed traffic around your firewall.
Two solutions:
- Use nftables instead of UFW. nftables does not have the same bypass problem because you control the full ruleset.
- Apply the DOCKER-USER chain fix. See Fix Docker Bypassing UFW: 4 Tested Solutions for Your VPS for the step-by-step workaround that forces Docker traffic through UFW.
If you are running Docker in production, nftables is the safer choice.
Common ports reference
| Service | Port | Protocol | UFW command | nftables rule |
|---|---|---|---|---|
| SSH | 22 | TCP | ufw allow 22/tcp |
tcp dport 22 accept |
| HTTP | 80 | TCP | ufw allow 80/tcp |
tcp dport 80 accept |
| HTTPS | 443 | TCP | ufw allow 443/tcp |
tcp dport 443 accept |
| DNS | 53 | TCP/UDP | ufw allow 53 |
tcp dport 53 accept; udp dport 53 accept |
| PostgreSQL | 5432 | TCP | ufw allow 5432/tcp |
tcp dport 5432 accept |
| MySQL | 3306 | TCP | ufw allow 3306/tcp |
tcp dport 3306 accept |
Do not expose database ports to the internet. If you need remote database access, use an SSH tunnel or restrict to specific IPs:
UFW:
ufw allow from 198.51.100.0/24 to any port 5432 proto tcp comment 'PostgreSQL from trusted network'
nftables (add before the final drop in the inbound chain):
tcp dport 5432 ip saddr 198.51.100.0/24 accept
Troubleshooting
Something went wrong?
Locked out of SSH: Use your provider's VNC/serial console to connect. Then either ufw disable or nft flush ruleset to clear all rules. Re-add your SSH allow rule before re-enabling the firewall.
Rules not persisting after reboot (nftables): Check that the service is enabled: systemctl is-enabled nftables. If it shows disabled, run systemctl enable nftables. Also verify that /etc/nftables.conf is syntactically valid: nft -c -f /etc/nftables.conf (the -c flag checks syntax without applying).
UFW status shows rules but traffic is still blocked: Check for conflicting nftables or iptables rules: nft list ruleset and iptables -L -n. Two firewall tools running simultaneously produce unpredictable results.
Logs filling up disk: If you enabled logging and the server receives heavy scan traffic, logs can grow fast. Lower the log level (ufw logging low) or add a rate limit to the nftables log rule:
log prefix "[nftables] Dropped: " limit rate 10/minute counter drop
Cannot connect after enabling rate limiting: The rate limiter may be too strict. For nftables, inspect the dynamic set: nft list set inet firewall ssh_ratelimit. For UFW, ufw limit uses a fixed 6 connections/30 seconds threshold that is not configurable. If that is too strict, use a plain ufw allow and rely on Fail2Ban instead Install and Configure Fail2Ban on a Linux VPS.
Next steps
A firewall blocks unwanted ports. It does not detect repeated failed login attempts or application-layer attacks. To complete your VPS security baseline:
- Set up Fail2Ban to ban IPs after repeated SSH failures Install and Configure Fail2Ban on a Linux VPS
- Harden your SSH configuration SSH Hardening on a Linux VPS: Complete sshd_config Security Guide
- If you run Nginx, add application-layer protections
- Read the full VPS security guide for the complete hardening checklist Linux VPS Security: Threats, Layers, and Hardening Guide
Copyright 2026 Virtua.Cloud. All rights reserved. This content is original work by the Virtua.Cloud team. Reproduction, republication, or redistribution without written permission is prohibited.
Ready to try it yourself?
Deploy your own server in seconds. Linux, Windows, or FreeBSD.
See VPS Plans