nftables is the modern successor to iptables and is the default firewall framework in RHEL 9. It replaces iptables, ip6tables, arptables, and ebtables with a single unified tool, offering better performance through an improved kernel path, atomic rule replacement, and a cleaner, more readable configuration syntax. This tutorial covers everything you need to know to configure a production-ready nftables ruleset on RHEL 9, from understanding the core concepts to enabling the service on boot.
Prerequisites
- RHEL 9 system with root or sudo access
- Basic knowledge of networking concepts (ports, protocols, IP addresses)
- An active SSH session to the server (keep it open while testing firewall rules)
- firewalld stopped if you prefer to manage nftables directly (optional — they can coexist but may conflict)
Step 1 — Check nftables Status and Install if Needed
The nftables package is installed by default on RHEL 9. Verify it is present and check the service status:
# Check if nftables is installed
rpm -q nftables
# If not installed, install it
dnf install -y nftables
# Check the service status
systemctl status nftables
# View any currently loaded rules
nft list ruleset
If you are currently using firewalld and want to manage nftables directly, stop firewalld first to avoid rule conflicts:
systemctl stop firewalld
systemctl disable firewalld
systemctl mask firewalld
Step 2 — Understand nftables Core Concepts
nftables uses a three-level hierarchy that differs significantly from iptables:
- Table — A container for chains; scoped to an address family (
ip,ip6,inet,arp,bridge). Useinetto handle both IPv4 and IPv6 simultaneously. - Chain — An ordered list of rules attached to a netfilter hook (
input,output,forward,prerouting,postrouting). Each chain has a type (filter,nat,route), a hook, a priority, and a default policy. - Rule — A match expression plus a verdict (
accept,drop,reject,log,counter).
Unlike iptables, nftables has no built-in chains — you create only what you need, which reduces overhead.
Step 3 — Create a Base Ruleset in /etc/nftables.conf
The main nftables configuration file is /etc/nftables.conf. Back up the default file and replace it with a complete, production-ready baseline ruleset:
cp /etc/nftables.conf /etc/nftables.conf.bak
cat > /etc/nftables.conf << 'EOF'
#!/usr/sbin/nft -f
# Flush all existing rules to start clean
flush ruleset
# Create an inet table (handles both IPv4 and IPv6)
table inet filter {
# INPUT chain: traffic destined for this host
chain input {
type filter hook input priority 0; policy drop;
# Accept loopback interface traffic
iifname "lo" accept
# Drop invalid connection states
ct state invalid drop
# Accept established and related connections
ct state { established, related } accept
# Accept ICMP (ping) for both IPv4 and IPv6
ip protocol icmp accept
ip6 nexthdr icmpv6 accept
# Accept SSH (port 22) - new connections only
tcp dport 22 ct state new counter accept
# Accept HTTP and HTTPS
tcp dport { 80, 443 } ct state new counter accept
# Log and drop everything else
counter log prefix "nftables-dropped: " drop
}
# FORWARD chain: traffic being routed through this host
chain forward {
type filter hook forward priority 0; policy drop;
}
# OUTPUT chain: traffic originating from this host
chain output {
type filter hook output priority 0; policy accept;
}
}
EOF
Step 4 — Apply the Ruleset and Verify
Load the configuration file with nft -f and verify that the rules are active. The -c flag performs a dry-run syntax check without loading the rules:
# Check the syntax without applying
nft -c -f /etc/nftables.conf && echo "Syntax OK"
# Apply the ruleset
nft -f /etc/nftables.conf
# List all active rules with counters
nft list ruleset
# List just the filter table
nft list table inet filter
# Monitor rule hit counts in real time (Ctrl+C to stop)
watch -n 2 "nft list table inet filter | grep counter"
Test that SSH still works by opening a new terminal and connecting before proceeding. Check that dropped traffic appears in the system log:
tail -f /var/log/messages | grep "nftables-dropped"
Step 5 — Manage Rules Dynamically
You can add and delete individual rules without reloading the entire ruleset. This is useful for temporary allowances or quick changes during troubleshooting:
# Add a rule to allow a custom port (e.g. 8080)
nft add rule inet filter input tcp dport 8080 ct state new accept
# List rules with handles (needed to delete specific rules)
nft list table inet filter -a
# Delete a rule by its handle number (replace 12 with actual handle)
nft delete rule inet filter input handle 12
# Add a source IP-based block (replace 203.0.113.5 with attacker IP)
nft add rule inet filter input ip saddr 203.0.113.5 drop
After making dynamic changes, save them back to the config file if you want them to persist across reboots:
nft list ruleset > /etc/nftables.conf
Step 6 — Enable nftables on Boot
Enable the nftables systemd service so your rules are automatically loaded from /etc/nftables.conf at every boot:
# Enable the service
systemctl enable nftables
# Start the service (loads /etc/nftables.conf)
systemctl start nftables
# Confirm it is active and enabled
systemctl is-active nftables
systemctl is-enabled nftables
# Test a reboot simulation: reload the service
systemctl restart nftables
nft list ruleset
Conclusion
You have configured a complete, production-ready nftables firewall on RHEL 9. The ruleset drops all inbound traffic by default, allows SSH, HTTP, HTTPS, and ICMP, logs dropped packets, and is loaded automatically at boot. nftables’ unified syntax, atomic reloads, and native support for both IPv4 and IPv6 make it the right choice for new RHEL 9 deployments over legacy iptables. As your infrastructure grows, explore nftables sets and maps for efficient multi-port and multi-IP rule management.
Next steps: How to Configure iptables Firewall Rules on RHEL 9, How to Configure Fail2Ban on RHEL 9, and How to Audit Linux Security with Lynis on RHEL 9.