
nftables Firewall Rules for Production Linux Servers
- AuthorLiam K.
- Date
- Time26 minutes
nftables is the modern Linux firewall framework that replaces iptables, ip6tables, and arptables with a single unified syntax. Debian 12 and Ubuntu 22.04+ use nftables as the default backend even when you run UFW. Understanding nftables directly gives you finer control over rate limiting, set-based blocking, and logging — capabilities that higher-level tools expose only partially.
This guide builds a production baseline: default-deny inbound, allow established connections, permit SSH/HTTP/HTTPS, rate-limit SSH against brute force, log dropped packets for analysis, and persist rules across reboots. The rules work on bare metal, VPS, and cloud instances alike.
nftables vs UFW vs iptables
- UFW — user-friendly wrapper, good for simple allow/deny rules. Uses nftables under the hood on modern Debian/Ubuntu.
- iptables — legacy syntax, still functional but deprecated for new deployments.
- nftables — native framework, atomic rule updates, sets/maps, built-in rate limiting.
Use UFW for quick setups on single-service servers. Use nftables directly when you need rate limiting, complex sets, or cross-table logic that UFW cannot express cleanly.
Step 1: Install and Verify nftables
sudo apt update
sudo apt install -y nftables
sudo systemctl enable nftables
# Flush legacy iptables rules if migrating:
sudo iptables -F 2>/dev/null || true
sudo ip6tables -F 2>/dev/null || true
nft --versionStep 2: Production Ruleset
Write the complete ruleset to /etc/nftables.conf. The policy is default drop on input — only explicitly allowed traffic passes.
sudo tee /etc/nftables.conf >/dev/null <<'EOF'
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
sets {
allowed_tcp_ports {
type inet_service
elements = { 22, 80, 443 }
}
ssh_rate_limit {
type ipv4_addr
flags dynamic
timeout 1h
}
}
chain input {
type filter hook input priority 0; policy drop;
# Allow loopback
iif lo accept
# Allow established and related connections
ct state established,related accept
# Drop invalid packets early
ct state invalid drop
# Rate limit SSH: max 4 new connections per IP per minute
tcp dport 22 ct state new add @ssh_rate_limit { ip saddr limit rate over 4/minute } drop
# Allow configured TCP ports
tcp dport @allowed_tcp_ports accept
# Allow ICMP ping (optional)
ip protocol icmp icmp type echo-request limit rate 5/second accept
ip6 nexthdr icmpv6 icmpv6 type echo-request limit rate 5/second accept
# Log dropped packets (rate limited to avoid log flood)
limit rate 5/minute log prefix "nftables-drop: " flags all
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0; policy accept;
}
}
EOFStep 3: Apply and Verify
# IMPORTANT: keep an active SSH session open while testing!
sudo nft -c -f /etc/nftables.conf
sudo systemctl restart nftables
sudo nft list ruleset
# Verify from another terminal:
ssh user@your-server
curl -I http://your-serverStep 4: Restrict SSH to Management Network
If you use Tailscale or a VPN for admin access, restrict SSH to that interface only and remove port 22 from the public allowed set.
# Add before the SSH rate limit rule:
# iifname "tailscale0" tcp dport 22 accept
# Remove 22 from allowed_tcp_ports set for public interfaceStep 5: Block Known Bad IPs with Sets
# Add a blocklist set:
# set blocklist {
# type ipv4_addr
# flags interval
# elements = { 192.0.2.0/24, 198.51.100.50 }
# }
# ip saddr @blocklist drop
# Populate dynamically from CrowdSec bouncer or threat feedsStep 6: Monitor Dropped Packets
sudo journalctl -k | grep "nftables-drop"
sudo dmesg | grep "nftables-drop" | tail -20
# Count drops per hour for dashboard:
sudo journalctl -k --since "1 hour ago" | grep -c "nftables-drop"Step 7: Runtime Rule Changes (Without Reboot)
# Add a port temporarily:
sudo nft add element inet filter allowed_tcp_ports { 8080 }
# Remove it:
sudo nft delete element inet filter allowed_tcp_ports { 8080 }
# Runtime changes are lost on restart — always update /etc/nftables.conf tooCommon Mistakes
- Applying rules without nft -c check — syntax errors can lock you out.
- Forgetting established/related rule — breaks active SSH sessions on reload.
- Not testing from a second session before closing your current SSH connection.
- Running UFW and nftables simultaneously with conflicting rules.
- Logging every dropped packet without rate limiting — fills disk with kernel logs.
Production Checklist
- Default deny inbound — allow only required ports explicitly.
- Rate-limit SSH and any admin-facing port.
- Version-control /etc/nftables.conf in your infrastructure repo.
- Test rule changes with nft -c before applying.
- Integrate with CrowdSec or similar for dynamic IP blocking.
"A firewall rule you have not tested is a lockout waiting to happen — always validate from a second session before you close the first."



