$linuxjunkies
>

Build an nftables Firewall Script

Build a complete nftables firewall from scratch: tables, chains, sets, default-deny input policy, service allowlisting, and persistent systemd configuration.

IntermediateUbuntuDebianFedoraArch10 min readUpdated June 7, 2026

Before you start

  • Root or sudo access on the target server
  • A second terminal or console fallback in case SSH is accidentally blocked
  • Basic familiarity with IP addressing and TCP/UDP ports
  • nftables kernel module available (standard on kernels 3.13+, default on all listed distros)

nftables replaced iptables as the Linux kernel's primary packet-filtering framework and is now the default on Debian 10+, Fedora 32+, RHEL 8+, and Arch. It unifies IPv4, IPv6, ARP, and bridge filtering under one coherent ruleset, introduces native sets and maps, and ships with a much cleaner syntax. This guide builds a complete, production-ready firewall script from scratch—covering the core concepts and every command you need to lock down a server.

Core Concepts Before You Write a Single Rule

Tables

A table is the top-level container. It has a name you choose and a family that determines what traffic it processes:

  • ipIPv4 only
  • ip6 — IPv6 only
  • inet — both IPv4 and IPv6 (recommended for servers)
  • arp, bridge, netdev — specialised use cases

Use the inet family for almost every server firewall; one ruleset handles both protocols.

Chains

Chains live inside tables and contain the actual rules. A base chain hooks into the netfilter processing pipeline with three attributes: type (filter, nat, route), hook (prerouting, input, forward, output, postrouting), and priority (an integer; lower runs first, 0 is the standard filter priority). A chain without a hook is a regular chain used only when jumped to explicitly.

Sets

Sets are named collections of IP addresses, ports, or other match criteria. Referencing a set (@setname) keeps rules readable and lets you update membership without rewriting rules.

Step 1 — Install and Enable nftables

Most distributions ship nftables already. Install and enable the service if needed.

# Debian / Ubuntu
sudo apt install nftables
sudo systemctl enable --now nftables
# Fedora / RHEL 8+ / Rocky
sudo dnf install nftables
sudo systemctl enable --now nftables
# Arch
sudo pacman -S nftables
sudo systemctl enable --now nftables

If you have legacy iptables rules loaded, flush them first to avoid conflicts. On RHEL/Rocky, also disable firewalld if you intend to manage nftables directly.

sudo systemctl disable --now firewalld   # RHEL/Rocky only if bypassing firewalld
sudo iptables -F && sudo ip6tables -F    # flush any legacy rules

Step 2 — Create the Firewall Script

Write your ruleset as a plain text file that nft -f loads atomically. Create /etc/nftables.conf (this is the default path the systemd service loads on most distros).

sudo nano /etc/nftables.conf

Paste the following complete ruleset, then work through the explanation below:

#!/usr/sbin/nft -f

# Flush all existing rules before loading
flush ruleset

table inet filter {

    # --- Sets ---
    set trusted_hosts {
        type ipv4_addr
        flags interval
        elements = { 192.168.1.0/24, 10.0.0.5 }
    }

    set blocked_ips {
        type ipv4_addr
        flags dynamic, timeout
        timeout 1h
    }

    # --- Chains ---
    chain input {
        type filter hook input priority filter; policy drop;

        # Allow established and related connections
        ct state established,related accept

        # Drop invalid packets
        ct state invalid drop

        # Allow loopback
        iifname lo accept

        # Drop traffic from explicitly blocked IPs
        ip saddr @blocked_ips drop

        # ICMP — rate-limit ping to prevent floods
        ip protocol icmp icmp type echo-request limit rate 5/second accept
        ip protocol icmp accept
        ip6 nexthdr icmpv6 accept

        # Allow SSH from trusted hosts only
        tcp dport 22 ip saddr @trusted_hosts accept

        # Allow HTTP and HTTPS from anywhere
        tcp dport { 80, 443 } accept

        # Allow DNS (if this host is a resolver)
        # tcp dport 53 accept
        # udp dport 53 accept

        # Log and drop everything else
        log prefix "nft-drop: " flags all limit rate 10/minute
        drop
    }

    chain forward {
        type filter hook forward priority filter; policy drop;
    }

    chain output {
        type filter hook output priority filter; policy accept;
    }
}

Key Design Decisions Explained

  • flush ruleset at the top ensures a clean load every time; no leftover rules accumulate.
  • policy drop on the input chain is the default-deny stance. Anything not explicitly accepted is silently discarded.
  • ct state established,related accept must come early — it passes return traffic for connections you initiated outbound.
  • iifname lo accept is essential; many programs communicate over loopback and will break without it.
  • The trusted_hosts set uses flags interval so it can hold CIDR prefixes, not just individual IPs.
  • The blocked_ips set uses flags dynamic, timeout, enabling runtime additions that auto-expire (useful with fail2ban or manual bans).
  • The output chain uses policy accept. Restricting outbound traffic is valuable in hardened environments but adds significant operational complexity; address it once inbound is stable.

Step 3 — Load and Test the Ruleset

Before making it permanent, test the file for syntax errors and do a dry run:

sudo nft -c -f /etc/nftables.conf   # -c = check only, no changes applied

If the check passes with no output, load it live:

sudo nft -f /etc/nftables.conf

Immediately verify your SSH session still works (you should still be connected). Then inspect the loaded ruleset:

sudo nft list ruleset

Output will mirror your script with minor formatting differences. Check the counters are zeroed and the policy reads drop on input.

Step 4 — Make the Ruleset Persistent

The systemd nftables.service unit reads /etc/nftables.conf on start by default on all three distro families. Confirm the path in the unit if you store your file elsewhere:

sudo systemctl cat nftables | grep ExecStart

Enable persistence:

sudo systemctl enable nftables
sudo systemctl restart nftables
sudo systemctl status nftables

After a reboot, run sudo nft list ruleset again to confirm rules survived.

Step 5 — Managing Sets at Runtime

One of nftables' biggest practical advantages is live set management without a full reload.

Block an IP immediately

sudo nft add element inet filter blocked_ips { 203.0.113.99 }

Remove a block

sudo nft delete element inet filter blocked_ips { 203.0.113.99 }

Add a new allowed service temporarily

# Allow port 8080 without reloading the whole ruleset
sudo nft add rule inet filter input tcp dport 8080 accept

For permanent changes, edit /etc/nftables.conf and reload with sudo systemctl reload nftables (or sudo nft -f /etc/nftables.conf directly).

Step 6 — Verify the Firewall Is Working

Test from a second machine or use nmap locally:

# From another host — replace IP with your server's address
nmap -sS -p 22,80,443,8080 192.0.2.10

Expected: ports 80 and 443 show open; port 8080 shows filtered (dropped, not rejected); port 22 shows filtered from untrusted IPs. Check the drop log in real time:

sudo journalctl -f | grep nft-drop

Troubleshooting

  • Locked out via SSH: Use console access (cloud provider serial console, KVM, or physical). Load a permissive ruleset: sudo nft flush ruleset to clear everything, then re-add your rules carefully with your management IP in the set.
  • Rules don't survive reboot: Verify the service is enabled (systemctl is-enabled nftables) and the ExecStart path points to your file. On some minimal RHEL installs the service unit exists but firewalld takes precedence — disable firewalld first.
  • "Error: Could not process rule": Run sudo nft -c -f /etc/nftables.conf to pinpoint the line. Common causes: missing semicolons, wrong family name, or an interval flag missing on a set that holds CIDRs.
  • IPv6 traffic blocked unexpectedly: The inet family covers both protocols. Ensure your trusted set has a corresponding ipv6_addr set if you also restrict by source on IPv6, or open SSH for both protocols explicitly with an ip6 saddr match.
  • Docker or libvirt breaks after enabling the firewall: Both create their own nftables tables. The forward chain policy drop blocks inter-container and VM traffic. Add ct state established,related accept to your forward chain, or set forward policy to accept if you trust the hypervisor to manage its own rules.
tested on:Ubuntu 24.04Debian 12Fedora 40Rocky 9

Frequently asked questions

What is the difference between nftables and iptables?
nftables is the modern replacement built into the kernel since 3.13. It unifies IPv4, IPv6, ARP, and bridge filtering in one tool, supports native sets and maps for efficient multi-value matching, and uses an atomic ruleset load. iptables has separate commands for each protocol and no native set support without ipset.
Can I use nftables alongside firewalld or ufw?
Not safely on the same interface. firewalld and ufw both write their own nftables (or iptables) rules. Managing rules directly while one of those services is active will cause conflicts and unpredictable behavior. Choose one management layer and disable the others.
How do I allow a new port permanently?
Add the appropriate 'tcp dport X accept' or 'udp dport X accept' rule inside the input chain in /etc/nftables.conf, then run 'sudo systemctl reload nftables'. Avoid adding rules only at runtime — they won't survive a reboot unless also in the file.
Why does Docker networking break when I set the forward chain to policy drop?
Docker routes traffic between containers and to the internet through the forward chain. A default-deny forward policy blocks that traffic. At minimum, add 'ct state established,related accept' to your forward chain, or add explicit rules accepting traffic on Docker's bridge interfaces (docker0, br-*).
How do I log dropped packets to diagnose a blocked connection?
Add a log rule immediately before your final drop in the input chain: 'log prefix "nft-drop: " flags all limit rate 10/minute'. Messages appear in the kernel log and are visible with 'sudo journalctl -k | grep nft-drop'. The rate limit prevents log flooding.

Related guides