Intrepid Universe Logo

Nftables Modular Configuration

Published Nov 2 2025

IU Home > Projects > nftables-modular-configuration
A simple, powerful, modular approach to configuring an nftables firewall

Introduction

Nftables1 is a firewall for Linux. It simplifies netfilter2 firewall configuration compared with traditional iptables based firewalls.

Below we present a way to think about and configure nftables based firewalls to provide flexible configuration - allowing changes to be applied in a modular fashion. Each network interface can then be thought of as having its own firewall which can be dynamically managed - added or removed as interfaces are brought up and down. It allows dynamic configuration of features such as ip black lists.

The key novel concept underpinning this configuration framework is the module:

  • a module is a single configuration file containing nft commands
  • modules are combined via the nft include directive to create the final firewall
  • modules are idempotent and applied via the linux shebang3 feature
  • modules can be mandatory or optional

Framework

We will assume a linux system has been setup with nftables enabled such that the /etc/nftables.conf file is the initial root of the configuration system. This file is the root configuration module. It should contain a shebang line and be chmod a+x so it can easily be applied to the system from the command line. It will reset the firewall and include all mandatory modules for firewall configuration at system boot.

The root module will typically look as below:

#!/usr/sbin/nft -f

flush ruleset

include ...

It is suggested to create a /etc/nftables directory to contain all nftables firewall files other than the root file /etc/nftables.conf. These files can be version controlled with git.

Naming Configuration Files

It is useful to have a consistent way to name files. The nft tables introduce a namespace already and it suggests a prefix for all module files: <fmaily>-<table>. Further the object types that most easily suggest themselves for modularisation are:

  • table
  • chain

Finally if you adopt the convention to placing statements to remove a module in their own module a file prefix of -remove can be useful.

This leads to a general filename convention as below:

<family>-<table>[-objectname]-<object>[-remove].conf

So for example a typical configuration directory supporting several network interfaces with features such as nat and ip black lists might look as below

define-ports.conf
inet-filter-block_ipv4-set.conf
inet-filter-block_ipv6-set.conf
inet-filter-eth0-chain.conf
inet-filter-table.conf
inet-filter-tun0-chain.conf
inet-filter-tun0-chain-remove.conf
inet-filter-wg0-chain.conf
inet-filter-wg0-chain-remove.conf
ip6-nat-table.conf
ip-nat-table.conf

Helper Files

It can be useful to create additional files that are not modules themselves under the /etc/nftables directory. As an example consider a file that will define a library of port numbers as symbolic variables /etc/nftables/define-ports.conf:

redefine Dns = 53
redefine Http = 80
redefine Https = 443
redefine Wireguard = 51820
...

Notice the file does not include a shebang line and has no need to be marked executeable. This will prevent administrators accidentally using the file when it is not intended as a full module. In addition the use of the redefine statement is key to reusing this helper file acrross a library of modules. Without such a construction it is easy to cause symbol redefinition errors in even the simplest configurations.

Module Types

Below is a summary of modules and the key concept found to be useful when using them:

  • remove modules - to allow a module to be added or removed at runtime not just boot
  • table modules - verdict maps allow modularity e.g. by interface
  • chain modules - the target of verdict maps
  • data modules - dynamically updated to respond in real time

Remove Modules

A remove module can be used stand alone to remove a feature from a firewall. It may also be used in defining a module to ensure the modules application is idempotent. As such the pattern for using a remove module is then

#!/usr/sbin/nft -f

include "/etc/nftables/...-remove.conf"

...

The -remove.conf module may include submodules and typically will reverse the order in which objects were added to a firewall. For example to remove an interface from the firewall that has been added dynamically to a verdict map the remove module will first remove the interface from the verdict map so its chains are no longer considered refenced by the map. It then can remove the chains:

#!/usr/sbin/nft -f

destroy element inet filter ifname_forward_map { "wg0", }
destroy element inet filter ifname_input_map   { "wg0", }
destroy chain   inet filter wg0_forward
destroy chain   inet filter wg0_input

Note the use of destroy to prevent error messages when the module is run twice.

Table Modules

Table modules define tables and key data structures such as base chains and verdict maps essential for applying rules in a modular fashion.

An example table for the inet family that will allow modules for each interface of a machine to be created and attached via verdict maps is shown below. As it is intended to be used at boot it does not have a remove module.

#!/usr/sbin/nft -f

destroy table inet filter

table inet filter {

    comment "Modular filter with dynamically plugable chains by interface name";

    counter block_ipv4_drop { comment "Blocked IPV4 packets"; }
    counter block_ipv6_drop { comment "Blocked IPV6 packets"; }
    counter ct_invalid_drop { comment "Invalid connection state dropped packets"; }
    counter lo_drop         { comment "Loopback device dropped packets"; }

    set block_ipv4 {
        comment "Set of ipv4 addresses that will be blocked";
        type ipv4_addr
        flags interval
    }

    set block_ipv6 {
        comment "Set of ipv6 addresses that will be blocked";
        type ipv6_addr
        flags interval
    }

    map ifname_forward_map {
        comment "Dynamic verdict map by input interface name for forwarded packets";
        type ifname : verdict
        elements = {
             "lo" : drop,
        }
    }

    map ifname_input_map {
        comment "Dynamic verdict map by input interface name for incoming packets";
        type ifname : verdict
        elements = {
             "lo" : accept,
        }
    }

    chain forward {
        comment "Dynamic forwarding based on input interface name";
        type filter hook forward priority filter
        policy drop

        iifname vmap @ifname_forward_map
    }

    chain input {
        comment "Basic networking and dynamic rules based on input interface name";
        type filter hook input priority filter
        policy drop

        ######################################################################
        # Basic Networking
        ######################################################################
        iif != "lo" ip daddr 127.0.0.1/8 counter name lo_drop drop
        iif != "lo" ip6 daddr ::1/128 counter name lo_drop drop

        ip saddr @block_ipv4 counter name block_ipv4_drop drop;
        ip6 saddr @block_ipv6 counter name block_ipv6_drop drop;

        ct state vmap { established : accept, related : accept, invalid : goto ct_invalid_drop }

        icmp type echo-request limit rate 5/second accept
        icmpv6 type echo-request limit rate 5/second accept

        # Accept neighbour discovery otherwise connectivity breaks
        icmpv6 type { nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } accept

        iifname vmap @ifname_input_map
    }

    chain ct_invalid_drop {
        comment "Count and drop invalid connections";
        counter name ct_invalid_drop drop;
    }

    chain output {
        comment "Output all packets";
        type filter hook output priority filter
        policy accept
    }
}

Chain Modules

Defining chains as modules is useful as chains can be referenced in a verdict map using the goto and jump statements. These references can be dropped in a modular way allowing the rules to be enabled dynamically and managed as individual modules.

VPNs are a good example where interfaces may be brought up and down dynamically along with their firewall rules. Containers are another example where these moudles can be useful both inside and outside the container image.

/etc/nftables/inet-filter-wg0-chain-remove.conf:

#!/usr/sbin/nft -f

destroy element inet filter ifname_forward_map { "wg0", }
destroy element inet filter ifname_input_map { "wg0", }
destroy chain inet filter wg0_forward
destroy chain inet filter wg0_input

/etc/nftables/inet-filter-wg0-chain.conf:

include "/etc/nftables/inet-filter-wg0-chain-remove.conf"

include "/etc/nftables/define-ports.conf"

define wg0_ports = { $Http, $Https, $IMAP, $IMAPs, $Ssh, $SMTP, $sSMTP }

table inet filter {

    counter wg0_forward_drop { comment "wg0 forward dropped packets"; }
    counter wg0_input_drop { comment "wg0 input dropped packets"; }

    chain wg0_forward {
        comment "wg0 forward rules";

        # Modify below as requried
        accept

        counter name wg0_forward_drop drop;
    }

    chain wg0_input {
        comment "wg0 input rules";

        # Modify below as requried
        accept

        counter name wg0_input_drop drop;
    }
}

add element inet filter ifname_forward_map { "wg0": goto wg0_forward, }
add element inet filter ifname_input_map { "wg0": goto wg0_input, }

Data Modules

Features such as ip black lists can be provided dynamically through the use of sets. A module defining a set can ensure all items are added at start up and if later more items are added the module simply updated and rerun to keep new items in a persistent way.

#!/usr/sbin/nft -f

add set inet filter block_ipv4 {
    type ipv4_addr
    flags interval
}

flush set inet filter block_ipv4

# Block so called 'service providers' who typically cause more errors
# on SMTP ports across the entire internet than do the bots actively
# exploiting vulnerabilites

# s-c-a-n.c-y-p-e-x.a-i
add element inet filter block_ipv4 {
    3.138.185.30,
    ...
}

# c-e-n-s-y-s-s-c-a--n-n-e-r.c-o-m
add element inet filter block_ipv4 {
    ...
}

# c-e-n-s-u-s.s-h-o-d-a-n.i-o
...

# c-r-i-m-i-n-a-l-i-p.c-o-m
...

# r-n-d.g-r-o-u-p-i-b.c-o-m
...

# s-h-a-d-o-w-s-e-r-v-e-r.o-r-g
...

A note on Counters

As an aside it is worth adopting a policy of using named counters. This then allows for a simple realtime firewall monitor to be created using the command:

watch -d -p -n 2.0 nft list counters

Additioanl Resources

Nftables man page