Sunday, October 5, 2025

Debian 13: moving from iptables to nftables

Nftables has been introduced from debian 10. iptables still available until debian 12. since debian 13, nftables is the default firewall. there is iptables-nft layer if user still want to interact with nftables with iptables command with some limitation due to they have different way to manage firewall.

The nftables flow to create firewall rules:

  1. Create rule
  2. Create a table
  3. Create a chain
  4. Create a set (for IPv4 or IPv6)

Add rule(s) that match against that set, we’re using the inet family, the same output chain can handle both IPv4 and IPv6. These is /etc/nftables.conf for pc 

  1. any connection from localhost  to outside
  2. accept incoming connection for port http, https and ssh from outside
  3. block incoming access from ip 1.2.3.4 
  4. block incoming access from ip 1.2.3.5 for port ssh

#!/usr/sbin/nft -f

flush ruleset

table inet filter {
    # Define a set for allowed TCP ports
    set allowed_tcp_ports {
        type inet_service;
        elements = { 22, 80, 443 }
    }

    # Define a set for blocked IPv4 addresses (all traffic)
    set blocked_ips {
        type ipv4_addr;
        elements = { 1.2.3.4 }
    }

    # Define a set for IPs blocked only on SSH
    set ssh_blocked_ips {
        type ipv4_addr;
        elements = { 1.2.3.5 }
    }

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

        # Drop packets from blocked IPs (all traffic)
        ip saddr @blocked_ips drop

        # Drop SSH packets from ssh_blocked_ips
        tcp dport 22 ip saddr @ssh_blocked_ips drop

        # Allow loopback interface
        iif lo accept

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

        # Allow ICMP (ping)
        ip protocol icmp accept
        ip6 nexthdr icmpv6 accept

        # Allow TCP packets to ports in allowed_tcp_ports set
        tcp dport @allowed_tcp_ports accept
    }

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

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

To add element in blocked_ips (temporary, need to save)

# nft add element inet filter blocked_ips { 5.6.7.8 }

To replace the whole element in blocked_ips (temporary, need to save)

# nft replace set inet filter blocked_ips { elements = { 1.2.3.4, 2.3.4.5 } }

To delete an element in blocked_ips (temporary, need to save)

# nft delete element inet filter blocked_ips { 203.0.113.5 }

To store/save nftables permanently  

# nft list ruleset > /etc/nftables.conf

This command will clean/remove all rules until nftables.conf reload

# nft flush ruleset

Back to iptables, in iptables the rules store and restore with following command:

# nft list ruleset > my_rules.nft
# nft -f my_rules.nft 

This is vulnerable to the system, there is a delay when system ready but not protected by firewall until system fully loading the the firewall rules. This weakness has been eliminate in nftables, nftables uses systemd to load the firewall. Firewall was loaded before system ready.

To enable nftables and start

# systemctl enable nftables
# systemctl start nftables

To export set blocked_ips in nftables

# nft list set inet filter blocked_ips > blocked_ips.conf
# nft -f blocked_ips.conf

These are more cleaner nftables.conf, set will be put on different file

/etc/nftables.conf:

#!/usr/sbin/nft -f

flush ruleset

include "/etc/nftables.d/sets-common.nft"
include "/etc/nftables.d/sets-ipv4.nft"
include "/etc/nftables.d/sets-ipv6.nft"

table inet filter {

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

        ip saddr @blocked_ips_v4 drop
        ip6 saddr @blocked_ips_v6 drop

        tcp dport 22 ip saddr @ssh_blocked_ips_v4 drop
        tcp dport 22 ip6 saddr @ssh_blocked_ips_v6 drop

        iif lo accept

        ct state established,related accept

        ip protocol icmp accept
        ip6 nexthdr icmpv6 accept

        tcp dport @allowed_tcp_ports accept
        tcp dport @allowed_tcp_ports accept
    }

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

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

/etc/nftables.d/sets-common.nft: 

table inet filter {
    set allowed_tcp_ports {
        type inet_service;
        elements = { 22, 80, 443 }
    }
}

/etc/nftables.d/sets-ipv4.nft: 

table inet filter {
    set blocked_ips_v4 {
        type ipv4_addr;
        elements = { 1.2.3.4 }
    }

    set ssh_blocked_ips_v4 {
        type ipv4_addr;
        elements = { 1.2.3.5 }
    }
}

/etc/nftables.d/sets-ipv6.nft:

table inet filter {
    set blocked_ips_v6 {
        type ipv6_addr;
        elements = { 2001:db8::dead:beef }
    }

    set ssh_blocked_ips_v6 {
        type ipv6_addr;
        elements = { 2001:db8::cafe:babe }
    }
}