Firewall rules aren’t static; they’re living, breathing entities that need constant tending.

Let’s see nftables in action, managing a simple web server. Imagine you have a server that needs to accept HTTP (port 80) and HTTPS (port 443) traffic, while blocking everything else.

Here’s a basic nftables configuration file (/etc/nftables.conf) to achieve this:

#!/usr/sbin/nft -f

flush ruleset

table ip filter {
    chain input {
        type filter hook input priority 0; policy accept;

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

        # Allow loopback traffic
        iif lo 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
        reject with icmpx type admin-prohibited
    }
}

To apply this configuration, you’d run:

sudo nft -f /etc/nftables.conf

The flush ruleset command at the beginning is crucial. It nukes any existing nftables rules before applying the new ones. This ensures a clean slate and prevents rule duplication or conflicts.

The table ip filter defines a table named filter for IPv4 traffic. Inside this table, the chain input is where we define rules for packets arriving at the server. The type filter hook input priority 0 tells nftables to intercept incoming packets at the input hook (meaning packets destined for the server itself) with a priority of 0 (higher priority means it gets evaluated earlier). policy accept is the default action if no rule matches.

ct state established,related accept is a performance optimization. It allows packets that are part of an already established connection or are related to one (like ICMP error messages) to pass through without further inspection. This is a common and vital rule for any stateful firewall.

iif lo accept allows all traffic originating from and destined for the loopback interface (lo), which is essential for local processes to communicate.

The tcp dport <port> accept rules explicitly allow incoming TCP traffic destined for ports 22 (SSH), 80 (HTTP), and 443 (HTTPS). dport stands for "destination port".

Finally, reject with icmpx type admin-prohibited is the catch-all. Any packet that hasn’t been explicitly accepted by the preceding rules will be rejected with an ICMP "administratively prohibited" message. This is generally preferred over drop because it provides feedback to the sender, which can be helpful for debugging, without revealing too much information about the system.

The power here isn’t just in writing static rules, but in scripting them. You can use shell scripts to dynamically generate these nftables configurations. For example, you could have a script that:

  • Reads a list of allowed IP addresses from a file.
  • Generates nftables rules to only allow SSH from those IPs.
  • Reloads the nftables ruleset.

This allows for rapid response to security incidents or automated updates to your firewall policies.

The ct (connection tracking) module is fundamental to nftables’ stateful firewall capabilities. It tracks the state of network connections, allowing you to make decisions based not just on individual packets but on the entire flow of communication. This is what enables rules like ct state established,related accept to work efficiently and securely.

Most people think of firewalls as simply "allow" or "deny." But the real magic is in how nftables integrates with the kernel’s networking stack to understand the context of a packet – is it part of an existing conversation, is it a new connection attempt, is it coming from a trusted internal network? This context is what allows for granular control and efficient performance.

The next step is understanding how to integrate nftables with system services like systemd for automatic loading on boot.

Want structured learning?

Take the full Nftables course →