nftables is a packet filtering framework that replaces older tools like iptables, ipchains, and ipfwadm. It provides a more unified and efficient way to manage network traffic.

Here’s how you can set up nftables to secure a web server, allowing HTTP, HTTPS, and SSH traffic while blocking everything else.

Let’s imagine we have a web server with the IP address 192.168.1.100 and we want to allow access to SSH on port 22, HTTP on port 80, and HTTPS on port 443.

First, we need to create an nftables configuration file. Let’s call it webserver.nft.

#!/usr/sbin/nft -f

flush ruleset

table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;

        # Allow loopback traffic
        iifname "lo" accept

        # Allow established and related connections
        ct state established,related accept

        # Allow SSH
        tcp dport 22 accept

        # Allow HTTP
        tcp dport 80 accept

        # Allow HTTPS
        tcp dport 443 accept

        # Drop invalid packets
        ct state invalid drop
    }
}

Let’s break down what’s happening here:

  • flush ruleset: This command ensures that any existing nftables rules are removed before applying the new configuration. It’s good practice to start with a clean slate.
  • table inet filter: We’re creating a table named filter that handles both IPv4 (ip) and IPv6 (inet). inet is a shorthand for ip ip6.
  • chain input: This defines a chain named input. Chains are where packet processing rules are defined. The input chain processes packets destined for the local machine.
  • type filter hook input priority 0;: This specifies that the input chain is of type filter (meaning it inspects packets) and hooks into the input packet path. The priority 0 is the default priority for filtering.
  • policy drop;: This is the crucial part for security. The default policy for the input chain is drop. This means any packet that doesn’t explicitly match an accept rule will be silently discarded. This is a "default deny" approach, which is much more secure than a "default allow."

Now for the specific rules:

  • iifname "lo" accept: This rule allows all traffic coming in from the loopback interface (lo). This is necessary for local services to communicate with each other.
  • ct state established,related accept: This rule uses the connection tracking (ct) module. It allows packets that are part of an already established connection or are related to an established connection (like ICMP error messages). This is essential for any network service to function properly, as it allows responses to outgoing requests and ongoing communication.
  • tcp dport 22 accept: This rule explicitly allows incoming TCP traffic destined for port 22 (the default SSH port).
  • tcp dport 80 accept: This rule allows incoming TCP traffic destined for port 80 (the default HTTP port).
  • tcp dport 443 accept: This rule allows incoming TCP traffic destined for port 443 (the default HTTPS port).
  • ct state invalid drop: This rule drops packets that are considered "invalid" by the connection tracking module. These are often malformed packets or packets that don’t seem to belong to any legitimate connection, and dropping them helps protect against certain types of network attacks.

To apply these rules, save the content above into /etc/nftables.conf (or a file you include in your main nftables configuration) and then run:

sudo nft -f /etc/nftables.conf

If you want to make these rules persistent across reboots, you’ll need to ensure the nftables service is enabled and started:

sudo systemctl enable nftables
sudo systemctl start nftables

Now, your web server will only accept SSH, HTTP, and HTTPS traffic from the outside world. All other incoming connections will be dropped by default.

The most surprising thing about how nftables handles state is that it doesn’t just track TCP and UDP connections; it can track the state of many other protocols, including ICMP, and can even be extended to track application-level states with custom modules.

Let’s see this in action. Suppose we have an active SSH session to 192.168.1.100. When you run sudo nft list ruleset, you’ll see the rules we just applied. If you then try to ping 192.168.1.100 from another machine on the network, the ct state established,related accept rule will allow the ICMP echo reply because it’s related to the ongoing network activity (even if the initial ping was blocked by the default policy). If you had another service running on port 8080 and tried to connect, it would fail because there’s no explicit rule to allow it.

The mental model here is that nftables processes packets sequentially through chains. Each chain has a policy (accept, drop, reject) and a set of rules. If a packet matches a rule, the action specified by that rule (accept, drop, reject, jump to another chain, etc.) is taken. If it doesn’t match any rule, the chain’s default policy is applied. The inet family handles both IPv4 and IPv6 traffic, and the filter table is a common place to put rules that allow or deny traffic. The input hook is for traffic destined for the server itself.

A subtle but powerful aspect of nftables is its ability to create sets and maps. For instance, instead of listing multiple IP addresses in a rule, you could define a set of "trusted IPs" and then use that set in your rules. This makes managing large numbers of IPs or ports much cleaner and more efficient. For example, to allow access from a specific subnet 192.168.2.0/24 for SSH:

table inet filter {
    set trusted_ssh_sources {
        type ipv4_addr
        elements = { 192.168.2.0/24 }
    }
    chain input {
        # ... other rules ...
        tcp dport 22 ip saddr @trusted_ssh_sources accept
        # ... other rules ...
    }
}

This makes it easy to add or remove IPs from the trusted list without modifying the main rule structure.

The next logical step after securing basic ports is to implement rate limiting to prevent abuse and consider more advanced filtering techniques.

Want structured learning?

Take the full Nftables course →