22 October 2026

nftables firewall on a VPS: a default-deny ruleset you can read and test

One short file instead of a pile of iptables commands: drop everything, allow web and SSH, check the syntax before loading, avoid a lockout.

Port 80, yes. Port 8080, no. Anything else, no. That's all a firewall is: a list of who gets in. The list used to live in iptables, a command per rule, and good luck reading it back a month later. nftables replaced that with one file. You check it, you load it, done. It's already on Ubuntu 24.04. Below I write a ruleset, then knock on the server to see if it really holds.

In short. Set policy drop on input, then let through only replies, loopback, ping, web ports and SSH. Run nft -c -f to check the file before nft -f loads it. And don't close your working SSH window until a second one has logged in.

What nftables is and how it differs from iptables

Underneath, both drive netfilter, the packet filter inside the Linux kernel. nftables is the newer front end, with a single command called nft. Even the old iptables is now just a wrapper around it; on our box iptables -V prints iptables v1.8.10 (nf_tables). By default there aren't two engines to pick from. What you choose is how to write the rules.

Four words cover almost everything:

  • table - a container for rules. inet means "both IPv4 and IPv6 at once", which saves you from writing everything twice.
  • chain - an ordered list of rules attached to a point in a packet's path. input is traffic addressed to this server, output is traffic leaving it, forward is traffic passing through.
  • policy - the fate of a packet that none of the rules in the chain caught.
  • set - a named list of addresses or ports that rules can refer to.

A default-deny ruleset

Shut everything, then open what you can name. On a real server the text below goes into /etc/nftables.conf; for a trial, any file name will do. Look at line two, flush ruleset. It erases every rule that's loaded right now. Keep that in mind, it matters near the end.

What each part does:

  • policy drop on input - whatever the rules below don't allow is thrown away without a word. Honestly, this is the firewall; the rest is exceptions.
  • ct state established,related accept - replies to connections that you started, and the SSH session you are typing in, keep working. Without this line the server cuts off its own answers.
  • ct state invalid drop - packets that conntrack (the kernel's connection tracker) considers malformed or impossible for their connection state get dropped early. A fresh first packet is not invalid, it is new, and falls through to the rules below.
  • iif "lo" accept - the server talking to itself over 127.0.0.1, for example nginx passing requests to an app on a local port. Close it and half of your services stop.
  • limit rate 5/second - ping works. A flood of pings doesn't.
  • tcp dport { 80, 443 } accept - the web, from anywhere.
  • tcp dport 22 ip saddr @admin_ips accept - SSH only from the addresses in the set. Replace the example address with yours. The set holds IPv4 addresses, so SSH over IPv6 is dropped; the icmpv6 line above is not decoration either, IPv6 needs those messages to find neighbours. I didn't run IPv6 in the lab. If your home address changes often, this rule can lock you out, so for a start use plain tcp dport 22 accept and tighten it later.
  • forward with policy drop - an ordinary VPS has no business routing strangers' traffic. Docker is the catch: it hooks the same point with rules of its own, and a drop in any chain at that point kills the packet. On a Docker host this policy will break container ports and container internet access unless you add forward rules or keep the policy at accept. I haven't tested Docker here.

Check the file before you load it

A typo here costs more than on a web page. The worst case is a server you can't reach. So use -c: it parses the file and loads nothing.

sudo nft -c -f /etc/nftables.conf

No output means the syntax is fine. A typo, here polcy instead of policy, gives an error that points at the exact spot:

Two errors, because I made the typo in two chains. Nothing was loaded, the exit code was 1, and the rules already in memory stayed untouched.

Load it and test it

I didn't run this on the live server. Experimenting with a firewall on the machine you're connected to is a fine way to lose it. So I built a small lab instead. Two network namespaces, srv and cli, which are isolated copies of the network stack, joined by a virtual cable. Inside srv a test web server listens on ports 80 and 8080. The client sits at 192.0.2.20, the address from admin_ips. With no rules, both ports answered. Then nft -f and:

Port 80 answers with HTTP 200. Port 8080 gives 000 and curl quits with code 28: a timeout. The packet was dropped and nobody replied, which is exactly how drop looks from outside. A port scanner has to wait out every such port, so it crawls compared to a server that answers "connection refused". Ping still gets through, because the rules say it may.

To see what is loaded, ask the kernel:

sudo nft list ruleset

What comes back is your file, tidied. A few lines look different from what you typed, and that's normal: priority 0 shows as priority filter, the ping limit gets a burst 5 packets, and icmpv6 is printed ipv6-icmp. Same rules.

Change a live ruleset

You don't have to reload the whole file for a small change. Rules can be added on the fly. A rule with only a counter and no verdict doesn't allow anything, it just counts packets that pass it, which makes it a cheap way to see who knocks on a port:

sudo nft add rule inet filter input tcp dport 8080 counter

After two requests to port 8080 the rule showed:

Two packets, 120 bytes, and both were still dropped by the policy. Rules added this way live only in memory. To delete one you need its handle, a number that sudo nft -a list chain inet filter input prints at the end of the line (like # handle 13; yours will differ), then sudo nft delete rule inet filter input handle 13.

Sets change the same way, without touching the rules that use them:

sudo nft add element inet filter admin_ips '{ 198.51.100.7 }'

Afterwards admin_ips held both 192.0.2.20 and 198.51.100.7, and the SSH rule now covers both addresses, with no reload. (I checked the set contents here, not a login from the second address.)

Keep it across reboots

Rules loaded by hand vanish at reboot. Ubuntu 24.04 has a service for this: the nftables package loads /etc/nftables.conf at boot. I looked at our box and found the service installed but switched off (systemctl is-enabled nftables says disabled), with a stock config of three empty chains. Switch it on once the check has passed, not before:

sudo systemctl enable --now nftables

Built the rules live and want them in the file? sudo nft list table inet filter | sudo tee /etc/nftables.conf does it for our table. Don't dump the whole ruleset on a box with UFW or Docker, the dump would also carry their rules. Add the shebang line and flush ruleset at the top yourself, the dump has neither.

Don't lock yourself out

  • Leave the SSH window you're in alone. Open a second one and log in. The established rule keeps the first alive even if your new port 22 rule is wrong, so it's your way back in.
  • If you do get locked out, the Console tab in your hosting panel doesn't need SSH. Type sudo nft delete table inet filter there and your rules go, the drop policy with them, while UFW and Docker keep theirs.
  • For a safety net, which I didn't test here, schedule the same cleanup before loading a new file: sudo systemd-run --on-active=5m nft delete table inet filter. If you're still in after five minutes, cancel it with sudo systemctl stop run-*.timer; if you got locked out, the firewall opens itself. A plain sleep in the background would die with a dropped SSH session, which is exactly when you need it.

UFW, Docker and what flush ruleset breaks

UFW and Docker don't write nftables files, they add rules through the iptables command, which on Ubuntu 24.04 ends up in the same kernel tables. Our box shows it: nft list ruleset opens with the warning table ip filter is managed by iptables-nft, do not touch! and lists the UFW chains. Two consequences:

  • flush ruleset deletes everything, including the UFW and Docker rules. I haven't tested it with Docker, but expect published container ports to stop working until Docker is restarted, and UFW needs sudo ufw reload to bring its rules back.
  • Running UFW and a hand-written nftables file as two bosses leads to confusion about which one blocks a port. Pick one. If you want the easier option, our guide on nginx and UFW in front of your app covers UFW. Use nftables when you want one readable file or sets and finer rate limits than UFW gives.

Converting old iptables rules

If you have iptables commands from an older guide, iptables-translate prints the nft equivalent without applying anything:

iptables-translate -A INPUT -p tcp --dport 443 -j ACCEPT

Its answer: nft 'add rule ip filter INPUT tcp dport 443 counter accept'. For a source-restricted rule, -s 203.0.113.5 --dport 22 became ip saddr 203.0.113.5 tcp dport 22. Treat it as a draft. The chain name is uppercase and the rule lands in a table called filter of the ip family, and moving it into your own inet table is up to you.

Check what is actually listening

A firewall hides ports; it doesn't close them. Before you decide what to open, list what listens at all with ss -tlnp; our article on ss instead of netstat explains the output. The basics of locking down a fresh server are in the secure a fresh VPS guide.

FAQ

Is nftables better than iptables?

For rules you write today, I'd say yes. One syntax covers IPv4 and IPv6, you get sets, and the whole thing is a file you can read. But iptables still works and lands in the same kernel code, so nobody has to rush to rewrite an old setup.

How do I see my nftables rules?

Run sudo nft list ruleset to see everything, or sudo nft list table inet filter for one table. Add -a and each rule shows its handle.

How do I test a config without applying it?

Use sudo nft -c -f file.nft. It parses the file and reports errors but doesn't load anything. For behaviour, as opposed to syntax, test in a network namespace or a spare server, as above.

Do I need nftables if I already use UFW?

Not really. UFW drives the same kernel filter and covers a typical server fine. Reach for nftables if you want a single declarative file, or finer control than UFW gives.

Why does the dropped port time out instead of saying "refused"?

Because drop says nothing at all. reject answers with an error and the client gives up at once. Drop tells a stranger less; reject is kinder to your own tools. On a public server most people pick drop.

Summary

  • nftables means one command, nft, and one file you can read. On Ubuntu 24.04 the old iptables already runs on top of it.
  • The firewall itself is policy drop on input, plus a short list of exceptions: replies, loopback, ping, web, SSH.
  • Run nft -c -f before every load. Then a typo costs you nothing.
  • Don't close your SSH window until a second one has logged in.
  • flush ruleset wipes UFW and Docker rules too. Let one tool own the firewall.
PUBLISHED
22 October 2026
AUTHOR
HIP-HOSTING
LANGUAGES
EN · RU