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 droponinput, then let through only replies, loopback, ping, web ports and SSH. Runnft -c -fto check the file beforenft -floads it. And don't close your working SSH window until a second one has logged in.
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:
inet means "both IPv4 and IPv6 at once", which saves you from writing everything twice.input is traffic addressed to this server, output is traffic leaving it, forward is traffic passing through.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.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.confNo 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.
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 rulesetWhat 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.
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 counterAfter 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.)
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 nftablesBuilt 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.
established rule keeps the first alive even if your new port 22 rule is wrong, so it's your way back in.sudo nft delete table inet filter there and your rules go, the drop policy with them, while UFW and Docker keep theirs.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 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.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 ACCEPTIts 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.
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.
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.
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.
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.
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.
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.
nft, and one file you can read. On Ubuntu 24.04 the old iptables already runs on top of it.policy drop on input, plus a short list of exceptions: replies, loopback, ping, web, SSH.nft -c -f before every load. Then a typo costs you nothing.flush ruleset wipes UFW and Docker rules too. Let one tool own the firewall.