nftables is the modern Linux firewalling framework, replacing iptables. It’s highly flexible, but that flexibility means you can easily create a configuration that’s either too permissive or too restrictive, leaving your server vulnerable or inaccessible.

Let’s look at a production-ready nftables ruleset.

#!/usr/sbin/nft -f

flush ruleset

table ip 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 from specific IPs
        tcp dport 22 ip saddr 192.168.1.0/24 accept
        tcp dport 22 ip saddr 10.0.0.0/8 accept

        # Allow ICMP (ping)
        ip protocol icmp accept

        # Drop invalid packets
        ct state invalid drop

        # Log and drop all other incoming traffic
        log prefix "nftables-input-drop: "
        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 if needed for NAT
        # oifname "eth0" masquerade
    }
}

This ruleset is designed for a server that primarily needs to accept incoming SSH connections and block everything else by default. The policy drop on the input chain is crucial for security – it means any traffic not explicitly allowed is denied.

Here’s a breakdown of the key components and why they’re structured this way:

  • flush ruleset: This is essential. It ensures that any existing nftables rules are completely removed before applying the new ones. Without it, you could end up with conflicting rules, leading to unexpected behavior or security gaps.

  • table ip filter { ... }: This defines the main filtering table for IPv4 traffic. filter is a common name, but you could use anything.

  • chain input { ... }: This chain processes packets destined for the server itself.

    • type filter hook input priority 0; policy drop;: This is the heart of the input filtering.

      • type filter: Specifies that this is a filtering chain (as opposed to NAT or mangle).
      • hook input: Attaches this chain to the input hook, meaning it intercepts packets arriving at the server.
      • priority 0: Sets the priority. Lower numbers are processed earlier. 0 is standard for filter hooks.
      • policy drop;: This is the default action. If a packet reaches the end of the chain without matching an accept rule, it will be dropped. This is a secure default.
    • iifname "lo" accept: Allows all traffic on the loopback interface (lo). This is necessary for many local services to communicate with themselves. iifname specifies the incoming interface name.

    • ct state established,related accept: This is a performance and functionality optimization. ct state refers to connection tracking.

      • established: Allows packets that are part of an already established connection (e.g., your reply to an outgoing request).
      • related: Allows packets that are related to an established connection (e.g., FTP data connections that are initiated by an established control connection).
      • This rule means you don’t need explicit rules for every outgoing connection’s reply traffic.
    • tcp dport 22 ip saddr 192.168.1.0/24 accept: This is a specific allowance for SSH.

      • tcp dport 22: Matches TCP traffic destined for port 22 (SSH).
      • ip saddr 192.168.1.0/24: Further restricts this rule to only apply to source IP addresses within the 192.168.1.0/24 subnet.
      • You would add more ip saddr rules for any other trusted networks you want to allow SSH from.
    • ip protocol icmp accept: Allows ICMP packets. This is what enables ping and other basic network diagnostics. If you don’t need ping responses, you can remove this.

    • ct state invalid drop: Drops packets that are deemed invalid by the connection tracker. These are often malformed packets or packets that don’t fit any known connection state, and they can be a sign of malicious activity.

    • log prefix "nftables-input-drop: " : Logs any packets that reach this point. This is invaluable for debugging and security monitoring. The prefix helps identify the log messages.

    • drop: This is the final catch-all. Any packet that has made it this far without being accepted will be dropped.

  • chain forward { ... }: This chain is for packets that are being routed through the server to another destination. For a typical server, this chain should usually have a policy drop; and no accept rules unless it’s acting as a router or gateway.

  • chain output { ... }: This chain processes packets originating from the server.

    • type filter hook output priority 0; policy accept;: Here, the policy accept; means that by default, the server can send traffic out to anywhere. This is common for servers that need to make outbound connections (e.g., for updates, DNS lookups, API calls). If you need to restrict outbound traffic, you would change this to policy drop; and add specific accept rules.
  • table ip nat { ... }: This defines a table for Network Address Translation (NAT).

    • chain prerouting { ... }: Used for modifying packets as they arrive before routing decisions are made. Typically used for DNAT (Destination NAT).
    • chain postrouting { ... }: Used for modifying packets as they leave after routing decisions. The masquerade rule (commented out) is commonly used on gateways to allow internal machines to access the internet by changing their private source IP to the gateway’s public IP. For a standalone server, you usually don’t need NAT rules unless it’s acting as a router.

Applying this ruleset:

  1. Save the content above into a file, for example, /etc/nftables.conf.
  2. Ensure the nftables service is enabled: systemctl enable nftables.
  3. Load the rules: nft -f /etc/nftables.conf.

Troubleshooting and Common Pitfalls:

  • Locking yourself out: The most common mistake is misconfiguring the SSH rule and losing access. Always have a backup access method (like a physical console or out-of-band management) when making firewall changes.
  • Forgetting ct state established,related accept: This will break most bidirectional communication. Your server won’t be able to respond to anything, even if you have an explicit accept rule for the initial outgoing connection.
  • Not logging dropped packets: The log statement is your best friend for understanding what’s being blocked and why. Without it, troubleshooting is guesswork.
  • Misunderstanding policy drop vs. policy accept: A policy drop on input is secure; a policy accept is insecure by default. The reverse is true for output if you want to restrict outbound traffic.
  • Conflicting rules: If you apply a new ruleset without flush ruleset and have overlapping or contradictory rules, the behavior can be unpredictable.

The next thing you’ll likely encounter is needing to allow other services, like HTTP/HTTPS, or perhaps setting up more complex NAT scenarios.

Want structured learning?

Take the full Nftables course →