Published Nov 9 2025
Nftables1 is a Linux firewall. 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 assume a Linux system 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 shown 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.
We recommend against including wildcard paths because of the security implications this could have - if a bad actor manages to create a file matched by a wildcard they could for example arrange for the entire firewall to be dropped at the next system boot.
It is useful to have a consistent way to name files. The nft tables
introduce a namespace and this suggests a prefix for all module files: <fmaily>-<table>
Further the object types that most easily suggest themselves for modularisation are:
If you adopt the convention of placing statements to remove a module in separate
files a suffix of -remove can be useful.
This leads to the general filename convention below:
<family>-<table>[-objectname]-<object>[-remove].conf
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 themselves modules 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 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 athe module itself to ensure the modules pplication 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 the 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 referenced 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 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 applied 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 service providers who typically cause errors on SMTP ports.
add element inet filter block_ipv4 {
3.138.185.30,
...
}
...
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