nftables from Scratch
Build a complete nftables firewall from scratch: tables, chains, hooks, sets, maps, NAT, and atomic transactional updates explained with real rules.
Before you start
- ▸Root or sudo access on the target machine
- ▸Basic understanding of TCP/IP (addresses, ports, protocols)
- ▸Console or out-of-band access as a safety net before modifying firewall rules
- ▸nftables kernel module available (kernel 3.13+; all modern LTS releases qualify)
nftables replaced iptables as the Linux kernel's primary packet filtering framework, landing in the kernel at version 3.13 and now the default on every major distro. It collapses the old iptables/ip6tables/arptables/ebtables sprawl into a single coherent tool, adds proper data structures (sets and maps), and processes rules in a virtual machine inside the kernel — meaning fewer syscalls and faster evaluation. This guide builds a complete, production-ready ruleset from nothing, covering every conceptual layer you need to own the tool.
Core Concepts Before Writing a Single Rule
Tables
A table is a namespace. It has a name you choose and an address family: ip (IPv4), ip6 (IPv6), inet (both at once — prefer this), arp, bridge, or netdev. Tables do nothing alone; they hold chains. You can have multiple tables; they are evaluated independently.
Chains and Hooks
A chain inside a table either attaches to a kernel hook (a base chain) or is called from other chains (a regular chain). Base chains require three attributes: the hook name, the type, and a priority. The hook determines when in the packet path your rules run.
- prerouting — packet arrives, before routing decision
- input — packet destined for local processes
- forward — packet passing through (router/firewall)
- output — packet from local processes
- postrouting — after routing decision, before leaving
The type is one of filter, nat, or route. Priority is an integer; lower numbers run first. The standard values are -100 (conntrack), 0 (filter), 100 (nat source). The policy (default verdict when no rule matches) is accept or drop.
Install and Check the Tooling
Debian / Ubuntu
sudo apt update && sudo apt install -y nftables
sudo systemctl enable --now nftables
Fedora / RHEL 9+ / Rocky Linux
# nftables is already installed; firewalld uses it as a backend by default.
# If you want to manage nftables directly, stop firewalld first.
sudo systemctl disable --now firewalld
sudo systemctl enable --now nftables
Arch Linux
sudo pacman -S nftables
sudo systemctl enable --now nftables
Verify the kernel module and version:
nft --version
# Output will resemble: nftables v1.0.9 (Old Doc Yak #3)
Building a Ruleset Step by Step
Step 1 — Start with a clean slate
Flush everything currently loaded so you are starting from a known state. On a production box, confirm this will not lock you out of SSH first.
sudo nft flush ruleset
Step 2 — Create an inet table and essential chains
Write your ruleset to a file so it is reproducible. The canonical path is /etc/nftables.conf.
sudo tee /etc/nftables.conf <<'EOF'
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
# Accept loopback unconditionally
iif lo accept
# Drop invalid connection state early
ct state invalid drop
# Accept established/related traffic
ct state { established, related } accept
# Accept ICMP (rate-limited)
ip protocol icmp icmp type { echo-request } limit rate 10/second accept
ip6 nexthdr ipv6-icmp icmpv6 type { echo-request } limit rate 10/second accept
ip6 nexthdr ipv6-icmp icmpv6 type { nd-neighbor-solicit, nd-neighbor-advert,
nd-router-solicit, nd-router-advert,
mld-listener-query } accept
# SSH — restrict to your management subnet in production
tcp dport 22 ct state new accept
# Log and drop everything else
log prefix "[nftables drop] " flags all drop
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0; policy accept;
}
}
EOF
Step 3 — Load the ruleset atomically (transactional updates)
nftables applies the entire file as a single transaction. Either every rule loads or none do — no partial state that locks you out mid-apply.
sudo nft -f /etc/nftables.conf
The -c flag does a dry-run check without touching the running kernel state:
sudo nft -c -f /etc/nftables.conf
Step 4 — Sets: efficient multi-value matching
A set is a kernel-side hash or interval tree. Matching 500 IP addresses with a set costs one operation; chaining 500 rules costs 500. Named sets live inside a table and can be referenced by multiple chains. Add a set for blocked source addresses and one for allowed management hosts:
sudo tee -a /etc/nftables.conf <<'EOF'
table inet filter {
# Anonymous inline sets are written directly: { 22, 80, 443 }
# Named sets (below) are reusable and updatable without reloading rules.
set blocklist {
type ipv4_addr
flags dynamic, timeout
timeout 1h
size 65536
}
set mgmt_hosts {
type ipv4_addr
flags interval
elements = { 10.0.0.0/24, 192.168.1.10 }
}
}
EOF
Reference the sets inside chain rules:
# Inside the input chain, before the generic SSH rule:
# ip saddr @blocklist drop
# tcp dport 22 ip saddr @mgmt_hosts accept
# tcp dport 22 drop # deny SSH from everyone else
Add an address to the running blocklist dynamically — no reload required:
sudo nft add element inet filter blocklist { 198.51.100.77 }
Step 5 — Maps: verdict and value lookups
A map takes a key and returns a value. A verdict map returns a firewall decision. This lets you replace a chain of tcp dport X accept/drop rules with a single lookup — critical for high-port-count services.
sudo nft add table inet filter
sudo nft add map inet filter port_policy '{ type inet_service : verdict; }'
sudo nft add element inet filter port_policy '{ 22 : accept, 80 : accept, 443 : accept, 8080 : drop }'
Reference the map in a rule:
sudo nft add rule inet filter input tcp dport vmap @port_policy
Maps also return plain values. A common use is DNAT port forwarding by mapping destination ports to IP:port pairs.
Step 6 — NAT table for routing/masquerading
NAT requires its own chain type. Add it inside the same inet filter table or a dedicated table:
sudo tee -a /etc/nftables.conf <<'EOF'
table inet nat {
chain prerouting {
type nat hook prerouting priority -100;
# DNAT example: forward external port 8443 to an internal host
tcp dport 8443 dnat to 10.0.0.50:443
}
chain postrouting {
type nat hook postrouting priority 100;
# Masquerade all traffic leaving a specific interface
oif "eth0" masquerade
}
}
EOF
Enable IP forwarding in the kernel (required for routing/NAT to function):
sudo sysctl -w net.ipv4.ip_forward=1
# Make it persistent:
echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-forwarding.conf
sudo sysctl -p /etc/sysctl.d/99-forwarding.conf
Verification
# Show the complete running ruleset
sudo nft list ruleset
# List a single table
sudo nft list table inet filter
# Show rule handles (needed for deletion)
sudo nft -a list chain inet filter input
# Delete a rule by its handle number (e.g. handle 7)
sudo nft delete rule inet filter input handle 7
# Show named set contents
sudo nft list set inet filter blocklist
Confirm the systemd service will restore your ruleset at boot:
sudo systemctl status nftables
# The service reads /etc/nftables.conf on start
Troubleshooting
Locked out of SSH after applying rules
If you applied rules interactively and lost access, the safest recovery technique is scheduling an automatic rollback before you apply changes. Use at to flush and re-allow SSH in 2 minutes, then apply your experimental rules. If you lose access, the scheduled job recovers you.
echo 'nft flush ruleset; nft add rule inet filter input tcp dport 22 accept' | sudo at now + 2 minutes
sudo nft -f /etc/nftables.conf
# Cancel the job once you verify access: sudo atrm 1
Rules load but traffic is still blocked
Check whether conntrack is seeing the flows correctly. Also verify rule ordering — nftables evaluates rules in the order listed; a drop before an accept will win.
sudo conntrack -L
sudo nft monitor # real-time event trace (Ctrl-C to stop)
nft: Error: syntax error, unexpected …
Run nft -c -f /etc/nftables.conf and read the line number. Common causes: missing semicolons after type … hook … priority … declarations, or using iptables-style syntax (e.g., -s instead of saddr). The nft man page (man 8 nft) and the nftables wiki at wiki.nftables.org are the authoritative references.
Conflict with firewalld or ufw
Both firewalld and ufw manage nftables (or iptables) rules themselves. Running them alongside manual nftables rules produces unpredictable interactions. Disable one or the other — never run both managing the same hooks simultaneously.
Frequently asked questions
- What is the difference between a named set and an anonymous set in nftables?
- An anonymous set is defined inline inside a rule (e.g., '{ 80, 443 }') and cannot be updated without rewriting the rule. A named set is declared separately in the table, can be referenced by multiple rules, and its elements can be added or removed at runtime with 'nft add element' or 'nft delete element' without touching the rule itself.
- Can nftables and firewalld run on the same machine?
- Not safely at the same time managing the same hooks. Firewalld uses nftables as its backend on RHEL 9+ and Fedora; if you manage nftables directly, disable firewalld first with 'systemctl disable --now firewalld'. Running both produces unpredictable rule ordering and silent conflicts.
- How do I delete a single rule without flushing everything?
- First list the chain with 'nft -a list chain inet filter input' to see handle numbers, then delete by handle: 'nft delete rule inet filter input handle N'. Never guess a handle number.
- What does the 'inet' address family give me over separate 'ip' and 'ip6' tables?
- An inet table's rules match both IPv4 and IPv6 traffic in a single rule set, so you do not maintain two parallel rulesets. The trade-off is minor: a handful of protocol-specific constructs (like raw ICMP type matching) require explicit 'ip protocol' or 'ip6 nexthdr' qualifiers inside inet rules.
- Are nftables rules persistent after a reboot by default?
- Only if the nftables systemd service is enabled. The service reads /etc/nftables.conf on start. Rules added with 'nft add rule …' interactively are lost at reboot unless you write them to that file. Always edit the file and reload with 'nft -f /etc/nftables.conf' to keep the file and running state in sync.
Related guides
Build a Mesh VPN with Nebula
Build a fully self-hosted mesh VPN with Nebula: create a CA, sign node certs, configure lighthouses, enforce group-based firewall rules, and run as a systemd service.
Common Linux Network Ports Reference
Learn Linux port ranges, read /etc/services, find what's listening with ss and nmap, and apply solid firewall rules to expose or block the right ports.
How to Configure a Static IP on Linux
Configure a static IP on Linux using Netplan, NetworkManager (nmcli), or systemd-networkd across Ubuntu, Fedora, Debian, and Arch with verified steps.
Expose a Service with Cloudflare Tunnel
Expose local services to the internet without port-forwarding using Cloudflare Tunnel. Install cloudflared, create a named tunnel, configure ingress rules, and run as a systemd service.