Set up nftables firewall on a VPS to replace iptables

The first thing you do on a fresh VPS is lock down the firewall. For years that meant iptables, but its successor, nftables, is now the default on all current distributions: Ubuntu 24.04 LTS, Debian 12, AlmaLinux 9, and Rocky Linux 9 all ship with nftables under the hood. The old iptables command still works via a compatibility layer, but you lose the cleaner syntax, atomic rule updates, and better performance that nftables gives you natively. This guide walks you through moving from iptables to nftables on a Ubuntu 24.04 or Debian 12 VPS. You will write a real ruleset, apply it, and make it survive a reboot.
- Key takeaways:
- nftables uses a single
nftcommand instead of the fragmented iptables/ip6tables/ebtables stack. - Rules are atomic: a
nft -feither applies fully or not at all, no half-applied rules. - Your IPv4 and IPv6 rules live in the same configuration file, not separate ones.
- A simple stateful ruleset (allow established traffic, drop everything else) is about 10 lines.
Prerequisites
- A VPS running Ubuntu 24.04 or Debian 12.
- Root or a sudo user with
sudoaccess. - SSH access (you will apply firewall rules remotely, so you need to allow port 22 safely).
- Basic understanding of TCP/UDP ports, nothing deeper.
Why move from iptables to nftables now
The iptables command has been officially deprecated for years. The kernel's netfilter subsystem still runs nftables internally, and compatibility tools like iptables-legacy wrap the old API. If you run iptables -V on a modern Ubuntu, you will see it's actually calling nftables underneath. That works, but it carries overhead: you must manage separate rules for IPv4 and IPv6, the syntax is inconsistent between the tools, and there is no atomic commit, if a rule in a script fails, the earlier rules stay applied. nftables solves all of that with a single command-line tool, a single syntax, and a single atomic file.
For a production VPS, nftables also reduces CPU overhead on high-traffic interfaces because the rule-set lives in a single B-tree structure in kernel memory, not a linear chain of matches. On a machine handling tens of thousands of connections, that matters.
Step 1, Verify what your VPS is currently running
Before you remove anything, check which firewall backend is active:
iptables -V
ls -la /etc/alternatives/iptables
If the output shows nf_tables, your system already uses nftables underneath. If it shows legacy, the old stack is in use. Either way, you will switch to managing the rules natively with nft.
systemctl status nftables 2>/dev/null || echo "nftables service not running"
On a minimal install, nftables may be installed but not enabled. That is fine, you will enable it after writing your ruleset.
Verify: run nft list ruleset. If you get table ip filter with some rules, someone already set a ruleset. If the output is empty, the VPS has no firewall active, which is dangerous.
Step 2, Disable the legacy iptables tools and purge them
On Ubuntu and Debian, the iptables package depends on iptables-legacy and iptables-nft. Instead of purging iptables entirely (which may break scripts that call iptables directly), switch the default mode to nftables:
sudo update-alternatives --set iptables /usr/sbin/iptables-nft
sudo update-alternatives --set ip6tables /usr/sbin/ip6tables-nft
sudo update-alternatives --set arptables /usr/sbin/arptables-nft
sudo update-alternatives --set ebtables /usr/sbin/ebtables-nft
Now iptables -V will show nf_tables. Any script that still calls iptables will work, but the kernel sees nftables rules.
If you want a clean break and no iptables-compat binaries, purge the legacy packages:
sudo apt remove iptables-legacy arptables-legacy ebtables-legacy -y
This is optional and safe if you have no legacy scripts. I prefer the clean approach, fewer moving parts.
Step 3, Write your first nftables ruleset
nftables organizes rules into tables (address families: ip, ip6, inet, arp, bridge). Inside each table are chains (base chains that hook into the netfilter pipeline: prerouting, input, forward, output, postrouting). The most common base chain is input (traffic arriving at the VPS itself) and forward (traffic passing through, needed only if you NAT or route).
The inet family combines IPv4 and IPv6 rules into a single table. That's what you should use for a VPS.
Create a file at /etc/nftables.conf with this content:
#!/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
# Accept loopback
iifname "lo" accept
# Allow established/related connections
ct state established,related accept
# Allow SSH (adjust port if you changed it)
tcp dport 22 accept
# Allow HTTP and HTTPS
tcp dport { 80, 443 } accept
# Allow ping (useful for diagnostics)
icmp type { echo-request } accept
ip6 icmpv6 type { echo-request } accept
# Log rejected packets (rate-limited)
limit rate 5/minute burst 10 packets log prefix "nftables_INPUT_DROP: "
# Everything else is dropped by policy
}
chain forward {
type filter hook forward priority filter; policy drop;
}
chain output {
type filter hook output priority filter; policy accept;
}
}
Explanation of each line:
flush ruleset, clear all existing nftables rules so you start clean.table inet filter, a table that covers both IPv4 and IPv6, namedfilter.chain input … hook input … policy drop, base chain hooked into the INPUT netfilter point; the default policy drops all traffic not explicitly allowed.iifname "lo" accept, allow everything on the loopback interface (localhost).ct state established,related accept, allow return traffic for outbound connections you initiated (e.g., a DNS response). This is what makes the firewall stateful.tcp dport 22 accept, allow SSH. Critical when applying remotely, if you forget this, you lock yourself out.tcp dport { 80, 443 }, braced set syntax, equivalent to two separate rules but more compact. Allows HTTP and HTTPS.icmp type { echo-request }, allow ping. Theip6variant is needed for IPv6 ping.limit rate 5/minute … log prefix, log dropped packets at most 5 per minute so syslog is not flooded.chain forward … policy drop, you are not using this VPS as a router, so drop all forwarded traffic.chain output … policy accept, allow all outbound traffic (you do not need to whitelist outgoing ports on a VPS).
Step 4, Apply the ruleset and enable persistence
First, check the configuration for syntax errors:
sudo nft -c -f /etc/nftables.conf
If there are no errors, the command exits silently. If there is a syntax problem, nftables tells you the exact line number. Fix it before applying.
Now apply the ruleset:
sudo nft -f /etc/nftables.conf
Verify:
sudo nft list ruleset
You should see the entire table inet filter printed, including your chains and rules. Also test that SSH still works from another terminal session before you close the current one.
To make it persistent after reboot, enable the nftables systemd service:
sudo systemctl enable nftables
sudo systemctl start nftables
The service loads /etc/nftables.conf on boot. On Ubuntu 24.04 and Debian 12, the default install already includes this service unit, you just need to enable it.
Double-check: Reboot the VPS once (sudo reboot) and verify after restart that sudo nft list ruleset shows your rules. If the output is empty, the service is not reading your file. Run systemctl status nftables to see errors.
Step 5, Add common services (port list reference)
Most VPS workloads need more than just SSH and HTTP. Here is a table of common service ports and the corresponding nftables rule:
| Service | Port(s) | nftables rule (add to input chain) |
|---|---|---|
| MySQL / MariaDB | 3306 | tcp dport 3306 accept |
| PostgreSQL | 5432 | tcp dport 5432 accept |
| DNS (unbound/bind) | 53/udp, 53/tcp | udp dport 53 accepttcp dport 53 accept |
| Redis | 6379 | tcp dport 6379 accept |
| SnapMirror (rsync) | 873 | tcp dport 873 accept |
| SMTP (Postfix) | 25, 465, 587 | tcp dport { 25, 465, 587 } accept |
| IMAP (Dovecot) | 143, 993 | tcp dport { 143, 993 } accept |
Add these in the same braced-set syntax where possible. For example, if your VPS runs a web server with MySQL locally, you only need the two lines in the example ruleset. No need to add MySQL if it binds only to 127.0.0.1, the loopback rule already covers it.
Step 6, Block brute-force attempts with rate limiting
You can add a simple rate limit on SSH port 22 to slow down brute-force scanners without needing fail2ban:
tcp dport 22 ct state new limit rate 10/minute accept
Replace the plain tcp dport 22 accept line with this. It allows 10 new SSH connections per minute; any faster than that gets dropped. This is a coarse but effective mechanism. For finer control, you still want fail2ban or CrowdSec on top, but the rate limit alone stops most automated attacks.
Apply: edit /etc/nftables.conf, run sudo nft -c -f to check, then sudo nft -f to apply. Keep your current SSH session open while you test a new one.
Troubleshooting
I applied the ruleset and lost SSH access.
This happens when you forget the tcp dport 22 accept line, or when you set policy drop without allowing established connections. The only fix for a remote VPS is to reboot it from your provider's control panel (IPv4 KVM or remote console). Most VPS panels, including thueVPS's dedicated control panel, offer an emergency console. Boot the VPS, log in via the console, and correct the ruleset.
To prevent this: never apply a new ruleset without first testing it with nft -c -f and keeping your current SSH session alive. Always open a second SSH window and confirm you can connect before closing the first.
The systemd service says "failed to start" after boot.
Check journalctl -u nftables -n 20. Common causes: a syntax error in /etc/nftables.conf, or the file is not executable (the shebang line needs it). Make sure the file begins with #!/sbin/nft -f and that you have rx permissions on /sbin/nft (they are fine by default). Also ensure the file ends with a blank line, nftables is strict about that.
nftables disappears after "apt upgrade" on Debian.
Debian updates sometimes remove the nftables package if you purged the legacy iptables. Always pin nftables with apt-mark hold nftables after installing, or simply reinstall it silently with apt install --reinstall nftables.
FAQ
Is nftables faster than iptables?
In most cases, yes, especially on rule-sets with many entries (100+). The old iptables used a linear chain match; nftables uses a B-tree (set-based matching) that scales better. On a typical VPS with fewer than 20 rules, the difference is negligible, but the single-syntax and atomic-apply advantages alone make it worth switching.
Can I still use ufw if I switch to nftables?
Yes. On Ubuntu 24.04, UFW is actually a frontend that generates nftables rules. The same applies to firewalld on RHEL-based distros. You can use UFW commands (ufw allow 443) and they will produce nftables rules. However, if you are writing your own ruleset, you should either use UFW or raw nftables, do not mix both, or they will overwrite each other.
How do I see current nftables rules in human-readable form?
Use sudo nft list ruleset. This prints all tables, chains, and rules in the nftables syntax. You can also see a specific table with sudo nft list table inet filter.
What happens to my old iptables rules when I enable nftables?
They are independent. The iptables compat layer (iptables-legacy or iptables-nft) stores rules separately from the native nftables ruleset. If you run sudo nft list ruleset and it is empty, but sudo iptables -L shows rules, then you have a legacy ruleset still loaded. You must flush the legacy rules (sudo iptables -F) and then apply your nftables rules. Or, as shown in Step 2, switch the compat mode to nftables-backed iptables.
Is the inet family safe for IPv6-only VPS?
Yes. The inet family works for both IPv4 and IPv6. If you only have IPv6, the IPv4 rules are simply ignored. The single table approach is cleaner than writing two separate tables. No reason to avoid it.
Related articles
- Hardening SSH on a new VPS, keys, ports, fail2ban, and CrowdSec
- Security audit and hardening with Lynis on a VPS
- Set up a WireGuard VPN on your VPS in 10 minutes


