Published Nov 2 2025
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:
include directive to create the final firewallWe 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.
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:
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
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.
Below is a summary of modules and the key concept found to be useful when using them:
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 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
}
}
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, }
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
...
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
linux
nftables