nftables is the modern replacement for iptables, offering a more structured and flexible way to manage network filtering. But just dropping in a default ruleset isn’t enough for production.
Let’s see nftables in action with a simple, yet effective, production-ready ruleset. Imagine we have a web server that should only accept incoming HTTP (port 80) and HTTPS (port 443) traffic, and allow all outgoing traffic.
#!/usr/sbin/nft -f
flush ruleset
table ip filter {
chain input {
type filter hook input priority 0; policy accept;
# Allow loopback traffic
iif lo accept
# Allow established and related connections
ct state established,related accept
# Allow SSH (port 22) for remote administration
tcp dport 22 accept
# Allow HTTP (port 80)
tcp dport 80 accept
# Allow HTTPS (port 443)
tcp dport 443 accept
# Drop all other incoming traffic
drop
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0; policy accept;
}
}
table ip nat {
chain prerouting {
type nat hook prerouting priority -100; policy accept;
}
chain postrouting {
type nat hook postrouting priority 100; policy accept;
# Masquerade outgoing traffic on eth0
oifname "eth0" masquerade
}
}
This ruleset establishes a baseline: it permits essential traffic like loopback and established connections, allows specific incoming services (SSH, HTTP, HTTPS), and then explicitly drops everything else. The output chain is set to accept by default, meaning the server can initiate connections outward freely, which is common for web servers needing to fetch updates or external resources. The forward chain is set to drop because this server isn’t acting as a router. The nat table with postrouting masquerades outgoing traffic on eth0, a common setup for servers behind a NAT gateway.
The core problem this solves is turning a generally open network interface into a tightly controlled gateway. Instead of relying on the default accept policy for the input chain (which would let in all incoming traffic), we explicitly permit only what’s necessary and then drop the rest. This is the principle of "deny by default." The ct state established,related accept rule is crucial; without it, even if you allow incoming connections on port 80, the server wouldn’t be able to send back any responses because the return packets wouldn’t be recognized as part of an established conversation.
The nftables command ct state established,related accept leverages the connection tracking module. When a new connection is initiated (e.g., a client requests a webpage), the first packet hitting the input chain is marked as new. If it matches an accept rule (like tcp dport 80 accept), it’s allowed. Subsequent packets belonging to that same connection, or related ones (like ICMP error messages), are marked as established or related by the kernel’s connection tracking. This rule then allows those packets through without needing to re-evaluate the explicit service rules, dramatically improving performance and ensuring proper two-way communication.
The policy drop on the forward chain is a critical security measure. It ensures that the server cannot be used as a pivot point to access other machines on the network. If this server were compromised, an attacker couldn’t easily use it to scan or attack internal systems.
The masquerade rule in the postrouting chain is a form of Network Address Translation (NAT). When traffic leaves the server and goes out through eth0, its source IP address is replaced with the IP address of eth0. This is essential for machines on a private network to access the internet, as only the router’s public IP is known to the outside world.
The most surprising thing about nftables’s ct state is how it interacts with the policy accept on the input chain. If you were to remove the ct state established,related accept rule and keep the policy accept for the input chain, your server would indeed accept incoming connections, but it wouldn’t be able to respond to them properly. This is because the packets from the server back to the client wouldn’t be explicitly allowed by any rule, and the default policy accept only applies to packets arriving at the hook, not packets leaving the hook on their way out of the system. The connection tracking state is what allows the return traffic to flow seamlessly without needing a specific output rule for every possible response.
The next logical step after hardening your server’s ingress and egress is to consider rate limiting, especially for services exposed to the internet. You might want to limit the number of new SSH connection attempts per second to prevent brute-force attacks, or limit HTTP requests to prevent denial-of-service.