Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

Configure a firewall with nftables

By updated

Improve this page

nftables is the firewall of the modern Linux kernel: it is what actually filters packets, under UFW as under firewalld. The nft tool replaces iptables with a unified syntax, native sets and better performance. This guide is for those who want to write their rules directly in nftables, without a frontend: minimalist servers, containers, appliances, or rulesets versioned as IaC. You will start from a complete server ruleset, then cover sets, NAT and persistence.

If you are first looking for a simple start, a frontend such as UFW or firewalld is faster. Direct nftables is for those who want full control.

What you will learn

  • The difference between nftables and iptables, and when to write direct nft.
  • The building blocks: tables, chains, hooks and default policy.
  • A complete server ruleset with stateful filtering.
  • Sets to manage address lists cleanly.
  • NAT, masquerade and port forwarding.
  • Making the ruleset persistent across reboots.

Prerequisites

  • A recent Linux server (RHEL 8+, Debian 10+, Ubuntu 20.04+) and root access.
  • The filtering concepts: default policy, inbound/outbound flows, stateful connections. If in doubt, see Introduction to firewalls.
  • A fallback console access before applying a drop policy on a remote server.

nftables or iptables?

iptables and nftables both drive Netfilter, the kernel's filtering subsystem. nftables is the official successor: on recent distributions, iptables is often just a wrapper (iptables-nft) that translates to nftables internally.

Criterioniptablesnftables
StatusLegacy, maintained for compatibilityStandard since Linux 3.13 (2014)
SyntaxOne command per table (iptables, ip6tables...)One nft command, IPv4 and IPv6 unified
IP listsRequires ipset on topNative sets
PerformanceRules evaluated linearlySets and maps in constant time
CountingCounters by defaultCounters on demand (faster)

In short: on a modern system, write nftables. iptables is now only useful to read old configurations or to run legacy scripts.

When should you write nftables directly?

The general rule is still to use your distribution's default frontend (UFW on Debian/Ubuntu, firewalld on RHEL). But direct nft is justified in several cases:

  • Minimalist server or container where you want no extra daemon.
  • Ruleset versioned in Git and deployed by Ansible or a script (infrastructure as code).
  • Advanced rules that frontends expose poorly: maps, hook priorities, range filtering.
  • Appliance or gateway where you control all the traffic.

Install nftables

nftables is present in every recent kernel; only the nft tool and the service can be missing. The version shown below comes from a container of each distribution.

Terminal window
sudo apt update && sudo apt install -y nftables
sudo systemctl enable --now nftables
nft --version
# nftables v1.1.3 (Commodore Bullmoose #4) <- Debian 13

Tables, chains and hooks

Three notions structure nftables. A table is a container of rules for a family of protocols (inet covers IPv4 and IPv6 at once). A chain groups rules; a so-called base chain hooks onto a kernel hook, a point the packet passes through.

The main hooks for filtering and NAT, each matching one moment in the packet's journey through the kernel:

HookMomentTypical use
inputPacket addressed to the machineFilter inbound flows
outputPacket sent by the machineFilter outbound flows
forwardPacket routed through the machineGateway, router
preroutingBefore the routing decisionDNAT, port forwarding
postroutingAfter routing, before sendingSNAT, masquerade

Each base chain carries a default policy (policy drop or policy accept) applied when no rule matched, which makes it the safety net of the whole chain.

A first server ruleset

Here is a complete server ruleset in the inet family (IPv4 + IPv6). It sets a drop policy by default, only allows the essentials, and logs the rest. This ruleset was validated (nft -c) then loaded on nftables 1.1.3 (Debian 13) and 1.1.5 (AlmaLinux 10).

/etc/nftables.conf
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
iif "lo" accept
ct state established,related accept
ct state invalid drop
ip protocol icmp accept
meta l4proto ipv6-icmp accept
tcp dport { 80, 443 } accept
tcp dport 22 accept
limit rate 5/minute log prefix "nft-drop: "
drop
}
chain forward { type filter hook forward priority filter; policy drop; }
chain output { type filter hook output priority filter; policy accept; }
}

Every line counts:

  • ct state established,related accept allows replies to connections already open: it is the heart of stateful filtering. Without it, your server would receive no reply.
  • ct state invalid drop drops inconsistent packets, often scans.
  • meta l4proto ipv6-icmp accept lets ICMPv6 through, essential to IPv6: without it, neighbour discovery fails and the server's IPv6 stops working. ip protocol icmp only covers IPv4.
  • tcp dport { 80, 443 } uses an anonymous list: more readable than one rule per port.
  • limit rate 5/minute log logs blocked packets without flooding the logs.

Always validate before applying, with the -c (check) option, which loads nothing:

Terminal window
sudo nft -c -f /etc/nftables.conf # validation only
sudo nft -f /etc/nftables.conf # apply
sudo nft list ruleset # check

Sets: manage address lists

A set is a named list, evaluated in constant time. It is the native equivalent of ipset, ideal for an administration allowlist.

table inet filter {
set ssh_allowed {
type ipv4_addr
elements = { 192.168.1.10, 10.0.0.5 }
}
chain input {
type filter hook input priority filter; policy drop;
# ... previous rules, without the "tcp dport 22 accept" line ...
tcp dport 22 ip saddr @ssh_allowed accept
}
}

The ip saddr @ssh_allowed rule only accepts SSH from the addresses in the set. It replaces the tcp dport 22 accept line of the first ruleset: left in place, that line would accept SSH from anywhere before the set is ever consulted, because an accept ends the evaluation of the chain. Adding an IP does not require reloading the whole ruleset:

Terminal window
sudo nft add element inet filter ssh_allowed { 203.0.113.7 }

This addition only lives in memory: also add the address to the ruleset file, otherwise it disappears at the next reboot or service reload.

Maps go further by associating a key with a value (for example a port with a decision), useful for compact rule routing.

NAT and masquerade

For a gateway to share its connection, add a nat table with a postrouting chain. Masquerade rewrites the source address of outgoing packets with the interface's IP, the form of SNAT suited to dynamic IPs.

table ip nat {
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
oifname "eth0" masquerade
}
}

Do not forget to enable kernel routing, otherwise packets do not cross the machine:

Terminal window
echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-forward.conf
sudo sysctl --system

Port forwarding with DNAT

To forward a public port to an internal service, use a prerouting chain with dnat. Loaded without error on nftables 1.1.3:

table ip nat {
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
tcp dport 8080 dnat to 192.168.1.20:80
}
}

Here, all traffic arriving on port 8080 is rewritten to 192.168.1.20:80. Combine prerouting (DNAT) and postrouting (masquerade) for a complete gateway.

Make the ruleset persistent

A ruleset loaded with nft disappears at reboot. Two reflexes make it permanent.

  1. Write the ruleset into the file actually read by the service, starting it with flush ruleset to start clean. This path is not the same on every distribution, and it is the first pitfall of persistence.

    The same systemctl cat reveals a second behaviour of the unit, on the service stop side this time, which concerns any machine where another firewall manager is already running.

  2. Enable the service that reloads this file at boot:

    Terminal window
    sudo systemctl enable --now nftables
  3. Check after a reboot that the rules are there:

    Terminal window
    sudo nft list ruleset

Since the file is a text script, version it in Git and deploy it with Ansible: that is the whole point of direct nftables as infrastructure as code.

nftables and Docker

Docker writes its own rules into Netfilter to publish container ports. A manual flush ruleset erases these rules and breaks container networking until the next restart of the Docker daemon.

The good practice: let Docker manage its chains, and put your rules in a separate table rather than emptying the global ruleset. On a gateway that also hosts containers, isolate carefully and test after each change.

nftables cheat sheet

The everyday commands to inspect and modify an active ruleset, with the handle you need for a targeted deletion and the flush to use with care.

Terminal window
sudo nft list ruleset # the whole ruleset
sudo nft -a list ruleset # with handles (to delete)
sudo nft list table inet filter # one specific table
sudo nft add rule inet filter input tcp dport 8443 accept
sudo nft delete rule inet filter input handle 12 # via the handle
sudo nft flush ruleset # erase everything (careful!)

Troubleshooting

The first row of the table is the one that will cost you the most: a drop policy applied without an SSH rule cuts you off from the machine, and only the console gets you back. Get into the habit of testing a ruleset with a session open in parallel. The other symptoms follow a common logic: a blocked legitimate packet almost always points to a missing ct state rule, because without connection tracking the replies to outbound traffic are treated as inbound traffic.

SymptomProbable causeSolution
Locked out of the serverpolicy drop without an SSH ruleConsole access, add the port 22 rule
No reply to requestsNo ct state established ruleAllow established,related
IPv6 unreachable, IPv4 workingICMPv6 blocked by the policy dropAdd meta l4proto ipv6-icmp accept
Rules lost at rebootRuleset written to a file the service does not readsystemctl cat nftables, write the right file, enable the service
Docker containers unreachableflush ruleset erased the Docker chainsRestart docker, isolate your rules
Blocked packets invisibleNo loggingAdd a log prefix rule before drop

Key points

  • nftables is the filtering engine of every modern Linux; iptables is now just a wrapper.
  • Structure: tables (families), chains hooked onto hooks, default policy.
  • A good server ruleset is stateful (ct state established,related), drop by default, and lets ICMPv6 through.
  • Sets replace ipset for address lists; maps go further.
  • NAT in postrouting (masquerade), port forwarding in prerouting (dnat).
  • Make the ruleset persistent in the file read by the nftables service: /etc/nftables.conf on Debian/Ubuntu, /etc/sysconfig/nftables.conf on RHEL.

Knowledge check

Check that the essentials of this guide are acquired. The questions only cover what has just been explained here.

Knowledge check

Check what you have learned with this interactive quiz

6 questions
6 min
70% to pass

How it works

  • The timer starts when you click Start
  • Multiple choice, true/false and short answer questions
  • You can move between questions
  • Detailed results are shown at the end

Starts the quiz and the timer

FAQ: frequently asked questions

These questions come up regularly when moving to nftables: what sets it apart from iptables, its coexistence with UFW and firewalld, rule persistence, NAT, and the effect on Docker container networking.

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn