Check what you have
# See if nftables is in use
sudo nft list ruleset
# On Debian / Ubuntu, the default iptables command is a thin wrapper
# over nftables (via iptables-nft); your existing iptables rules already
# live in the nftables kernel layer.
which iptables
update-alternatives --display iptables # typically iptables-nft, not iptables-legacy
The minimum-viable host firewall
# /etc/nftables.conf
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
# Sets for re-use
set blocklist_v4 {
type ipv4_addr
flags interval
}
chain input {
type filter hook input priority filter; policy drop;
# Always allow loopback + established connections
iif lo accept
ct state {established, related} accept
# Drop invalid + blocklisted
ct state invalid drop
ip saddr @blocklist_v4 drop
# Allow ICMP / ICMPv6 (rate-limited)
ip protocol icmp icmp type {echo-request, destination-unreachable, time-exceeded} limit rate 5/second accept
ip6 nexthdr ipv6-icmp accept
# SSH only from the LAN
ip saddr 192.168.1.0/24 tcp dport 22 accept
# HTTPS from anywhere
tcp dport {80, 443} accept
# Log + drop everything else
log prefix "nft-input-drop: " limit rate 5/minute
counter
}
chain forward {
type filter hook forward priority filter; policy drop;
# Add rules here if this host routes
}
chain output {
type filter hook output priority filter; policy accept;
# Optionally restrict egress
}
}
# Load + enable
sudo nft -f /etc/nftables.conf
sudo systemctl enable --now nftables
# Inspect
sudo nft list ruleset
The key concepts
- table — a container; one per address family (or
inetfor both v4 and v6). - chain — an ordered rule list, attached to a hook (input, output, forward, prerouting, postrouting).
- rule — a match + action.
- set — named list of values (IPs, ports, MACs) with O(1) lookup. Way faster than iptables' multi-rule equivalent.
- map — a set with values (key → value); enables verdict maps for compact L4 dispatch.
Sets: the killer feature
# Define
set blocklist {
type ipv4_addr
flags interval, timeout
}
# Add elements (with optional TTL)
add element inet filter blocklist { 198.51.100.42 timeout 1h }
add element inet filter blocklist { 203.0.113.0/24 }
# Reference in a rule
ip saddr @blocklist drop
Sets support intervals (CIDR ranges) and per-element timeouts. Useful for ephemeral block lists, fail2ban-style temporary bans without rewriting the whole ruleset.
Atomic replacement
# Compose a new ruleset in a file, swap in atomically
sudo nft -c -f /etc/nftables.conf # -c = check only, validates syntax
sudo nft -f /etc/nftables.conf # replaces in one transaction
Either the entire new ruleset is loaded or none of it is. No "half-applied" state where the SSH rule is loaded but the firewall-drop default isn't — the kind of bug that locks you out of a remote host. iptables didn't have this safety; nftables makes it the default.
NAT (for a router-shaped host)
table ip nat {
chain prerouting {
type nat hook prerouting priority dstnat;
# Port forward 80/443 to an internal host
iif "eth0" tcp dport {80, 443} dnat to 192.168.1.20
}
chain postrouting {
type nat hook postrouting priority srcnat;
# Masquerade LAN traffic going out the WAN
oif "eth0" masquerade
}
}
Verdict maps for L4 dispatch
# A compact way to express "different verdicts per dest port"
chain input {
type filter hook input priority filter; policy drop;
iif lo accept
ct state {established, related} accept
# Map: port -> action
tcp dport vmap {
22: jump ssh_chain,
80: accept,
443: accept,
9100: jump prometheus_chain
}
}
chain ssh_chain {
ip saddr {192.168.1.0/24, 10.0.5.0/24} accept
log prefix "nft-ssh-deny: " counter drop
}
chain prometheus_chain {
ip saddr 10.0.6.50 accept # only the prometheus server
counter drop
}
Vmaps are O(1) per packet vs the O(n) of a chain of "if dport == X jump Y" rules. Critical for high-throughput firewalls with many port-specific policies.
Connection tracking and rate limiting
# Per-IP connection limit (DDoS basic mitigation)
chain input {
tcp flags syn tcp dport 80 ct count over 100 reject
# Rate-limit SYN floods
tcp flags & (fin|syn|rst|ack) == syn limit rate over 50/second drop
}
Logging
# Log to dmesg
log prefix "nft-drop: " counter drop
# Log to nflog (more efficient; consumed by ulogd2)
log prefix "nft-drop: " group 0 counter drop
# View
journalctl -k -f | grep nft-drop
Monitor what's actually hitting your rules
# Add 'counter' to any rule to count packets + bytes
tcp dport 22 accept counter
# View counts
sudo nft list ruleset | grep -A 1 counter
sudo nft reset counter inet filter ssh # reset specific counter
Useful for "is this rule actually catching anything?" debugging without sniffing the wire.
Convert old iptables rules
# Convert
sudo iptables-save > /tmp/old.rules
sudo iptables-restore-translate -f /tmp/old.rules > /tmp/new.nft
# Review (it's mostly mechanical, with quirks)
cat /tmp/new.nft
# Test in a non-flushed mode
sudo nft -c -f /tmp/new.nft
The translation is decent for simple rule sets, less clean for elaborate ones. Treat output as a starting point; rewrite for clarity if needed.
UFW / firewalld: the friendly wrappers
If you don't want to write nftables directly:
- UFW (Ubuntu's default) —
ufw allow ssh, etc. Backed by nftables in modern Debian/Ubuntu. Smaller feature surface. - firewalld (RHEL / Fedora) — zone-based; well-integrated with NetworkManager. Backed by nftables too.
Both are fine if their abstraction fits. For "I have a specific policy and want the full nftables expressiveness," drop down to nftables directly.
For Kubernetes / containers
Don't manage host nftables to expose container ports; the container runtime (Docker / containerd) and Kubernetes (kube-proxy, Cilium, Calico) manage their own iptables/nftables layers. Conflicting host rules cause confusing behavior.
For host-level egress restriction plus Kubernetes — use NetworkPolicies + Cilium's policy engine (see that tutorial) inside the cluster, leave host nftables for SSH / management ports only.
The big mental shift from iptables
- iptables had separate
filter/nat/mangle/rawtables — each with their own chain layout. nftables has one table system; you create the chains. - iptables matched parameters with flags (
-p tcp -m multiport --dports 80,443); nftables uses a cleaner predicate-style syntax (tcp dport {80, 443}). - iptables' policy was per-chain; nftables makes it explicit on the chain definition.
- iptables ruleset updates were sequential (one rule at a time); nftables is atomic.
The result: significantly less code for the same policy, faster lookups at scale, no "lock yourself out by typo" footgun.