nftables on Debian: a working configuration

2025-09-14 — firewall, networking

Finally moved my servers over from iptables to nftables. Debian 12 made nftables the default, so iptables is now a compatibility shim anyway. The syntax is different enough that there was a learning curve, but the underlying model is cleaner once it clicks.

These are the concepts I needed to understand and a config that actually works.

How it differs from iptables

The big conceptual shift: iptables had separate commands for IPv4 (iptables) and IPv6 (ip6tables), and rules were spread across built-in tables with fixed chains. nftables lets you define your own tables and chains, and the inet address family covers both IPv4 and IPv6 in a single ruleset.

Also: nft rule changes are atomic. No more partial updates if something fails halfway.

Key concepts

Tables

A table is a container for chains. It has an address family: ip (IPv4 only), ip6 (IPv6 only), inet (both), arp, bridge, or netdev. For a server firewall, inet is almost always what you want.

Chains

Chains are where rules live. There are two kinds:

  • Base chains — attached to a netfilter hook (input, output, forward, prerouting, postrouting). Packets flow through these automatically.
  • Regular chains — not attached to any hook. Only reached via a jump or goto from another chain.

Each base chain has a type, a hook, a priority, and a default policy. Priority determines order when multiple chains attach to the same hook — lower number runs first.

Rules

Rules are evaluated top to bottom within a chain. A rule that matches and has a terminal verdict (accept, drop, reject) ends processing for that packet. If no rule matches, the chain's default policy applies.

A working server configuration

This is what I run on servers that need to accept SSH, HTTP, and HTTPS, with everything else dropped inbound. Using the inet family so one ruleset handles both IPv4 and IPv6.

#!/usr/sbin/nft -f

flush ruleset

table inet filter {

    chain input {
        type filter hook input priority filter; policy drop;

        # loopback
        iif lo accept

        # established and related connections
        ct state established,related accept

        # drop invalid packets early
        ct state invalid drop

        # ICMP
        ip protocol icmp accept
        ip6 nexthdr icmpv6 accept

        # services
        tcp dport 22 accept    # SSH
        tcp dport 80 accept    # HTTP
        tcp dport 443 accept   # HTTPS
    }

    chain forward {
        type filter hook forward priority filter; policy drop;
    }

    chain output {
        type filter hook output priority filter; policy accept;
    }
}

The flush ruleset at the top wipes all existing rules before loading the new ones. Important to know if you have other services (like fail2ban) that manage their own nftables rules — they'll need to re-add them after a reload.

Safety note: always test a new firewall config from a second SSH session before closing your current one. If you lock yourself out, you'll need console access to fix it. Tools like at can schedule a rule flush as a safety net.

Common nft commands

CommandWhat it does
nft list rulesetPrint all tables, chains, and rules
nft list chainsList chains only (no rules)
nft flush rulesetRemove everything — blank slate
nft -f /etc/nftables.confLoad rules from file
nft -c -f /etc/nftables.confCheck file for errors (dry run)
nft list table inet filterPrint one specific table
nft add rule inet filter input tcp dport 8080 acceptAdd a rule live

Connection tracking states

The ct state matcher is one of the most useful things in nftables. States I actually use:

  • new — first packet of a new connection
  • established — part of a connection already seen in both directions
  • related — related to an existing connection (e.g. FTP data, ICMP errors)
  • invalid — doesn't match any known connection; usually safe to drop

Enabling on boot

Debian's nftables service reads /etc/nftables.conf on start:

systemctl enable nftables
systemctl start nftables

After editing the config file, reload with systemctl reload nftables or nft -f /etc/nftables.conf directly.