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.
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:
ip— IPv4 onlyip6— IPv6 onlyinet— 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_hostsset usesflags intervalso it can hold CIDR prefixes, not just individual IPs. - The
blocked_ipsset usesflags 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 rulesetto 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.confto pinpoint the line. Common causes: missing semicolons, wrong family name, or anintervalflag missing on a set that holds CIDRs. - IPv6 traffic blocked unexpectedly: The
inetfamily covers both protocols. Ensure your trusted set has a correspondingipv6_addrset if you also restrict by source on IPv6, or open SSH for both protocols explicitly with anip6 saddrmatch. - Docker or libvirt breaks after enabling the firewall: Both create their own nftables tables. The forward chain policy
dropblocks inter-container and VM traffic. Addct state established,related acceptto your forward chain, or set forward policy toacceptif you trust the hypervisor to manage its own rules.
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
Configure Prometheus Alertmanager
Configure Prometheus Alertmanager with routing trees, receivers, inhibition rules, grouping, Go templates, and PagerDuty/Slack on-call integrations.
Build an Intranet Server on Linux
Set up a complete small-office intranet on one Linux box: Nginx web server, dnsmasq local DNS, Samba file sharing, and a Wiki.js team wiki.
Caddy as a Reverse Proxy
Set up Caddy as a reverse proxy with automatic HTTPS, load balancing, WebSocket passthrough, reusable snippets, and header control — no certbot required.
How to Change the Webmin Port
Move Webmin off its default port 10000 by editing miniserv.conf, updating your firewall (ufw, firewalld, or nftables), and restarting the service.