$linuxjunkies
>

nftables from Scratch

Build a complete nftables firewall from scratch: tables, chains, hooks, sets, maps, NAT, and atomic transactional updates explained with real rules.

AdvancedUbuntuDebianFedoraArch12 min readUpdated June 7, 2026

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.

tested on:Ubuntu 24.04Debian 12Fedora 40Arch rolling

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