A small Linux firewall is easy to understand.
You might start with a few rules:
tcp dport 22 accept
tcp dport 80 accept
tcp dport 443 accept Then your network grows.
Suddenly you have:
The firewall can quickly turn into hundreds of repetitive rules.
That is where nftables sets and maps become extremely valuable.
Instead of representing every host, subnet, service, and policy as an individual rule, nftables can group data into reusable structures and perform efficient lookups against them.
The official Netfilter nftables project documentation specifically highlights sets, maps, and concatenations as mechanisms that can reduce linear ruleset inspection and build more scalable packet-classification policies.
In this guide, we will take the stateful VLAN firewall built in CorgiNetwork’s Linux nftables Inter-VLAN Firewall and redesign parts of it using:
Named sets
Anonymous sets
IPv4 address sets
Network sets
Port sets
Maps
Concatenations
Dynamic elements
Counters
Stateful filtering The objective is not simply to make the firewall shorter.
It is to make it easier to scale, audit, troubleshoot, and maintain.
Imagine a management VLAN containing four trusted administration systems:
192.168.30.10
192.168.30.11
192.168.30.12
192.168.30.13 Each system needs SSH access to:
192.168.20.10 A basic firewall design might contain:
192.168.30.10 → 192.168.20.10 TCP/22 ACCEPT
192.168.30.11 → 192.168.20.10 TCP/22 ACCEPT
192.168.30.12 → 192.168.20.10 TCP/22 ACCEPT
192.168.30.13 → 192.168.20.10 TCP/22 ACCEPT That works.
But imagine having:
50 administrators
30 servers
12 VLANs
20 services Repeating rules becomes increasingly difficult to maintain.
The problem is not only the number of lines.
You also have to answer:
Which hosts are trusted?
Which servers belong to this security group?
Which ports belong to the web-services policy?
Where do I modify a host when its IP changes? A more scalable architecture separates:
DATA from:
POLICY That is one of the major advantages of sets.
A set is a collection of values that nftables can match against.
For example:
trusted_admins = {
192.168.30.10,
192.168.30.11,
192.168.30.12
} Instead of writing three firewall rules, we can conceptually write:
IF source IP belongs to trusted_admins
AND destination is server
AND destination port is SSH
THEN ACCEPT The official Netfilter material describes nftables as providing a generic set infrastructure capable of storing supported data types, including IP addresses and service-related values.
This produces an important architectural change:
Traditional design:
Rule
Rule
Rule
Rule
Rule
Set-based design:
DATA SET
↓
POLICY RULE You have already seen a simple nftables set if you followed the previous CorgiNetwork firewall guide.
Consider:
tcp dport { 80, 443 } accept The values:
{ 80, 443 } form an anonymous set.
This means:
Destination TCP port
↓
Is it 80 OR 443?
↓
YES
↓
ACCEPT Netfilter’s own nftables examples use this style for matching multiple ports in a single rule.
Anonymous sets are excellent for small, fixed groups.
For example:
tcp dport { 22, 80, 443 } accept But when data becomes important enough to manage independently, a named set is usually better.
Suppose our Linux router uses:
table inet vlan_firewall Create a named set:
sudo nft add set inet vlan_firewall trusted_admins \
'{ type ipv4_addr; }' Now add elements:
sudo nft add element inet vlan_firewall trusted_admins \
'{ 192.168.30.10, 192.168.30.11, 192.168.30.12 }' Inspect:
sudo nft list set inet vlan_firewall trusted_admins Conceptually:
trusted_admins
|
+-- 192.168.30.10
+-- 192.168.30.11
+-- 192.168.30.12 The set contains data.
It does not by itself permit traffic.
We still need a firewall rule.
Suppose:
192.168.20.10 is an SSH server.
Create:
sudo nft add rule inet vlan_firewall forward \
ip saddr @trusted_admins \
ip daddr 192.168.20.10 \
tcp dport 22 \
ct state new \
accept Now the rule means:
SOURCE
↓
Is source inside @trusted_admins?
↓
YES
↓
Destination = 192.168.20.10?
↓
YES
↓
TCP/22?
↓
YES
↓
ACCEPT This builds directly on the stateful firewall architecture from Linux nftables Inter-VLAN Firewall, where ct state established,related handles return traffic for permitted connections.
Suppose a new administrator receives:
192.168.30.14 With repetitive rules, you might need another firewall rule.
With a set:
sudo nft add element inet vlan_firewall trusted_admins \
'{ 192.168.30.14 }' The policy rule does not change.
This separation is powerful:
Firewall Policy
|
| unchanged
|
v
@trusted_admins
|
+ new host The rule describes what trusted administrators may do.
The set describes who the trusted administrators are.
Suppose:
192.168.30.11 should no longer have management access.
Remove it:
sudo nft delete element inet vlan_firewall trusted_admins \
'{ 192.168.30.11 }' The SSH policy remains intact.
You modified the underlying security group rather than rebuilding the firewall logic.
We can apply the same approach to destinations.
Create:
sudo nft add set inet vlan_firewall managed_servers \
'{ type ipv4_addr; }' Populate:
sudo nft add element inet vlan_firewall managed_servers \
'{ 192.168.20.10, 192.168.20.20, 192.168.20.30 }' Now write:
sudo nft add rule inet vlan_firewall forward \
ip saddr @trusted_admins \
ip daddr @managed_servers \
tcp dport 22 \
ct state new \
accept Our policy becomes:
Trusted Administrators
|
| SSH
v
Managed Servers instead of maintaining every source/destination combination separately.
Suppose only these systems host web applications:
192.168.20.20
192.168.20.21
192.168.20.22 Create:
sudo nft add set inet vlan_firewall web_servers \
'{ type ipv4_addr; }' Then:
sudo nft add element inet vlan_firewall web_servers \
'{ 192.168.20.20, 192.168.20.21, 192.168.20.22 }' Users in:
192.168.10.0/24 may access web services:
sudo nft add rule inet vlan_firewall forward \
ip saddr 192.168.10.0/24 \
ip daddr @web_servers \
tcp dport { 80, 443 } \
ct state new \
accept This produces:
USER VLAN
192.168.10.0/24
|
| HTTP / HTTPS
v
@web_servers Other systems inside the server VLAN do not automatically inherit the same access.
Compare these policies.
Broad policy:
Users
↓
TCP 80/443
↓
Entire Server VLAN More precise policy:
Users
↓
TCP 80/443
↓
@web_servers only The second policy reduces unnecessary reachability.
That is especially useful when the server VLAN also contains:
databases
backup servers
monitoring systems
management services
internal APIs Network segmentation becomes more meaningful when rules describe actual service groups instead of simply trusting entire subnets.
Sets are not limited to addresses.
Suppose our public web applications use:
80
443
8443 Create a service set:
sudo nft add set inet vlan_firewall web_ports \
'{ type inet_service; }' Add:
sudo nft add element inet vlan_firewall web_ports \
'{ 80, 443, 8443 }' Now:
sudo nft add rule inet vlan_firewall forward \
ip saddr 192.168.10.0/24 \
ip daddr @web_servers \
tcp dport @web_ports \
ct state new \
accept The policy becomes:
Users
↓
@web_ports
↓
@web_servers This is significantly easier to maintain when the service list changes.
For a simple rule:
tcp dport { 80, 443 } accept an anonymous set is perfectly reasonable.
But suppose the same web-service definition appears in:
User → Web
Management → Web
Monitoring → Web
VPN → Web Instead of repeating:
{ 80, 443, 8443 } everywhere, define:
@web_ports once.
This reduces configuration drift.
Sets can also represent network prefixes when configured appropriately.
Imagine trusted infrastructure networks such as:
192.168.30.0/24
10.20.0.0/16
10.30.50.0/24 You may want a logical set representing trusted management networks rather than individual hosts.
This can be useful for large environments, but remember:
Whole subnet trusted is a broader security decision than:
Specific administration hosts trusted Do not use broad prefixes simply because they make configuration shorter.
Our firewall sits behind the VLAN architecture developed in the previous CorgiNetwork guides:
Linux Bridge VLAN Filtering Explained establishes the Layer 2 boundaries.
Linux Inter-VLAN Routing with Network Namespaces creates controlled Layer 3 reachability.
Linux nftables Inter-VLAN Firewall introduces stateful security between those networks.
Sets now make those policies scalable.
The architecture becomes:
VLAN
↓
Router
↓
nftables
↓
Sets
↓
Policy
↓
Destination This is one of the strongest operational advantages.
Imagine a new web server:
192.168.20.23 Instead of editing the firewall rule:
sudo nft add element inet vlan_firewall web_servers \
'{ 192.168.20.23 }' The existing policy immediately uses the updated set.
Likewise, retiring:
192.168.20.20 can be handled by:
sudo nft delete element inet vlan_firewall web_servers \
'{ 192.168.20.20 }' The conceptual firewall remains:
Users
↓
Web Services
↓
Web Servers even though the individual machines change over time.
A set answers a question such as:
Does this value belong to this collection? A map goes further:
For this key, what value or action should be used? Conceptually:
KEY
↓
MAP
↓
VALUE For example:
Network A → Address A
Network B → Address B or, depending on the policy:
Input condition → verdict The official Netfilter project overview describes maps as extending the set concept and highlights them as one of nftables’ major capabilities.
Consider a traditional firewall with many conditions:
If source A → ACCEPT
If source B → DROP
If source C → ACCEPT
If source D → DROP A map can represent a relationship between keys and outcomes rather than requiring a long linear chain of repetitive rules.
This becomes increasingly valuable as policy complexity grows.
Netfilter explicitly notes that maps and concatenations can reduce the number of rule inspections required to determine packet handling.
A useful mental model is:
SET
Value
↓
Member?
↓
YES / NO while:
MAP
Key
↓
Lookup
↓
Associated value Use sets when you need membership.
Use maps when you need a key-to-value relationship.
Now imagine that security depends on a combination of values.
Not merely:
Source IP but:
Source IP + Destination IP or:
Source Network + Destination Port or:
Source IP + Destination IP + Protocol nftables supports concatenations, allowing multiple selectors to form one lookup key.
Conceptually:
SOURCE
+
DESTINATION
+
PORT
↓
ONE LOOKUP This can replace large collections of repetitive rules.
Imagine:
Admin A → Server A → SSH
Admin B → Server B → SSH
App A → Database A → PostgreSQL
App B → Database B → PostgreSQL A conventional ruleset might require one or more rules for every relationship.
With an appropriate concatenated set, relationships can instead become data entries.
Conceptually:
{
AdminA . ServerA . SSH,
AdminB . ServerB . SSH,
AppA . DB-A . PostgreSQL,
AppB . DB-B . PostgreSQL
} and one policy mechanism performs the lookup.
This is one reason nftables can scale more elegantly than a purely linear firewall design.
Let’s build the conceptual policy:
Users
|
| web_ports
v
Web Servers
Trusted Admins
|
| SSH
v
Managed Servers
App Servers
|
| Database Ports
v
Database Servers
IoT
|
X
Internal Infrastructure Instead of dozens of independent rules, we can define:
@trusted_admins
@managed_servers
@web_servers
@web_ports
@app_servers
@database_servers
@database_ports The firewall policy then references these objects.
Create:
sudo nft add set inet vlan_firewall app_servers \
'{ type ipv4_addr; }' Add:
sudo nft add element inet vlan_firewall app_servers \
'{ 192.168.20.30, 192.168.20.31 }' Create database servers:
sudo nft add set inet vlan_firewall database_servers \
'{ type ipv4_addr; }' Populate:
sudo nft add element inet vlan_firewall database_servers \
'{ 192.168.50.10, 192.168.50.11 }' Then create database ports:
sudo nft add set inet vlan_firewall database_ports \
'{ type inet_service; }' For PostgreSQL:
sudo nft add element inet vlan_firewall database_ports \
'{ 5432 }' Now policy can become:
sudo nft add rule inet vlan_firewall forward \
ip saddr @app_servers \
ip daddr @database_servers \
tcp dport @database_ports \
ct state new \
accept It does not say:
Server VLAN → Database VLAN = TRUSTED It says:
Approved Application Servers
↓
Approved Database Servers
↓
Approved Database Service
↓
ACCEPT That is a much more precise security relationship.
Sets and maps do not replace conntrack.
We still want a stateful foundation such as:
ct state invalid drop
ct state established,related accept followed by explicit rules for new connections.
Conceptually:
INVALID
↓
DROP
ESTABLISHED / RELATED
↓
ACCEPT
NEW
↓
Evaluate security sets/policy The sets optimize and organize policy data.
Connection tracking continues to provide stateful behavior.
A scalable ruleset might conceptually resemble:
table inet vlan_firewall {
SETS
----
trusted_admins
managed_servers
web_servers
web_ports
app_servers
database_servers
database_ports
FORWARD POLICY
--------------
invalid → drop
established/related → accept
trusted_admins
→ managed_servers
→ SSH
→ accept
users
→ web_servers
→ web_ports
→ accept
app_servers
→ database_servers
→ database_ports
→ accept
everything else
→ drop
} An administrator can understand the architecture much faster than hundreds of nearly identical lines.
Counters remain valuable.
For example:
sudo nft add rule inet vlan_firewall forward \
ip saddr @app_servers \
ip daddr @database_servers \
tcp dport @database_ports \
ct state new \
counter \
accept Inspect:
sudo nft list chain inet vlan_firewall forward You can now determine whether the application-to-database policy is actually being used.
List the entire table:
sudo nft list table inet vlan_firewall Or inspect one set:
sudo nft list set inet vlan_firewall trusted_admins Another:
sudo nft list set inet vlan_firewall web_servers This separation is extremely useful during incident response.
Instead of reading hundreds of rules to answer:
Which hosts currently have administrative access?
you can inspect:
@trusted_admins directly.
Suppose:
192.168.30.14 cannot SSH to a managed server.
First verify membership:
sudo nft list set inet vlan_firewall trusted_admins Is:
192.168.30.14 present?
Then inspect:
sudo nft list set inet vlan_firewall managed_servers Is the destination present?
Then check the rule:
sudo nft -a list chain inet vlan_firewall forward Then inspect routing:
ip route get DESTINATION Then capture:
tcpdump -eni any host 192.168.30.14 This gives us:
Routing
↓
Set membership
↓
Rule match
↓
Conntrack
↓
Return path If the set contains the correct address but the rule counter remains zero, investigate:
wrong chain
wrong hook
wrong protocol
wrong destination port
wrong destination address
earlier rule
routing failure
traffic never reached firewall Do not keep adding elements to the set.
The zero counter tells you that the expected rule is not seeing the traffic.
nft monitor traceFor difficult cases, nftables tracing can show the packet’s journey through the ruleset.
Use:
sudo nft monitor trace with appropriately targeted tracing configuration.
This can reveal:
packet enters chain
↓
rule evaluated
↓
set lookup
↓
verdict For busy production systems, trace narrowly rather than generating massive diagnostic output.
Suppose the firewall says:
@trusted_admins → @managed_servers → SSH → ACCEPT but Linux routes the packet through the wrong interface.
The set is not the problem.
Use:
ip route get DESTINATION If a multi-homed Linux server chooses an unexpected interface, CorgiNetwork’s Linux Traffic Uses the Wrong Interface provides a dedicated diagnostic workflow.
For source-based routing, use Linux Policy Routing with ip rule and Multiple Routing Tables.
Likewise:
Firewall policy = correct does not imply:
VLAN configuration = correct Check:
bridge vlan show and:
bridge fdb show If VLAN tagging or bridge membership is wrong, the packet may never reach the Layer 3 firewall.
Use Linux Bridge VLAN Filtering Explained for that layer.
Suppose you create:
@trusted_networks containing:
10.0.0.0/8
192.168.0.0/16
172.16.0.0/12 and then broadly accept traffic from the set.
Technically, the ruleset is concise.
Security-wise, it may be terrible.
The objective is not:
fewest possible firewall lines The objective is:
clear
precise
maintainable
auditable
security policy Never trade policy precision for configuration elegance.
Compare:
@set1
@set2
@ports1 with:
@trusted_admins
@web_servers
@database_servers
@web_ports
@database_ports The second design documents itself.
This becomes increasingly important when multiple administrators maintain the firewall.
Do not create one giant:
@trusted_hosts containing:
administrators
monitoring servers
backup systems
application servers
automation nodes unless all those systems genuinely share the same privileges.
Prefer:
@trusted_admins
@monitoring_servers
@backup_servers
@app_servers Different roles should normally map to different security capabilities.
Sets are also useful when infrastructure changes dynamically.
Imagine an automation system that knows the current web-server addresses.
Instead of rebuilding the entire firewall, it can update:
@web_servers This model can integrate more naturally with configuration-management and infrastructure-automation systems.
But automation introduces another requirement:
Validate inputs before modifying security policy.
A bad automation pipeline updating a firewall set can create an immediate security incident.
Production nftables deployments should generally be managed as coherent configurations rather than uncontrolled collections of shell commands.
nftables supports transactional ruleset updates, one of the architectural advantages highlighted by the official Netfilter project.
That matters because you want to avoid states such as:
old rule deleted
↓
new rule not yet installed
↓
temporary security gap Treat the firewall as configuration, not merely a command history.
Before deploying a set-based ruleset, verify:
✓ Default policy is intentional
✓ established/related handling is correct
✓ invalid traffic policy is defined
✓ Sets have descriptive names
✓ Source groups are narrowly scoped
✓ Destination groups are narrowly scoped
✓ Service sets contain only required ports
✓ IPv6 policy is considered
✓ Counters exist where useful
✓ Logging is rate-limited
✓ Ruleset persistence is configured
✓ Backup configuration exists
✓ Remote management access is protected
✓ Rollback access exists
✓ Routing is tested independently
✓ VLAN configuration is tested independently Consider a network with:
20 administrators
50 servers A naïve pairwise model can become difficult very quickly.
The set-based model instead expresses:
@trusted_admins
↓
SSH
↓
@managed_servers The number of actual members can change while the security relationship remains stable.
This is the key architectural benefit.
Use an anonymous set for small fixed values:
tcp dport { 80, 443 } Good candidates include a few closely related service ports that are unlikely to require independent management.
Use a named set when:
values change
values are reused
membership matters operationally
the group has security meaning
administrators need to inspect it Examples:
@trusted_admins
@web_servers
@dns_servers
@monitoring_nodes
@blocked_hosts Use a map when you need:
KEY → VALUE rather than simple membership.
Maps become particularly powerful when different inputs need different outputs or verdicts.
The official Netfilter project specifically identifies maps as a major nftables capability beyond ordinary sets.
Use concatenations when the security decision depends on combinations such as:
source + destination or:
source + destination + port instead of one field alone.
This can dramatically simplify policies containing many specific relationships.
An nftables set is a collection of typed values that can be referenced by firewall rules.
Typical values include IP addresses and service ports.
An anonymous set is defined directly inside a rule:
tcp dport { 80, 443 } A named set exists as a separate object:
@web_ports and can be referenced by rules and managed independently.
Yes.
For a named set you can add an element, for example:
nft add element inet vlan_firewall trusted_admins \
'{ 192.168.30.14 }' The rule referencing @trusted_admins remains unchanged.
Yes.
For example, an inet_service set can represent service ports such as:
80
443
8443 and be referenced by a transport-layer port match.
A map associates keys with values.
This extends the membership concept of a set and can be used to build more sophisticated policies.
The exact performance depends on the ruleset and workload, but Netfilter specifically highlights maps and concatenations as tools for reducing linear ruleset inspection.
The maintainability benefit can be equally important.
No.
Sets organize and match data.
Conntrack maintains connection state.
A stateful firewall commonly uses both:
conntrack
+
sets/maps
+
routing
+
firewall policy The biggest nftables improvement is not necessarily learning another command.
It is learning to design the firewall as structured policy.
A simple ruleset starts as:
Rule
Rule
Rule
Rule
Rule A scalable ruleset evolves toward:
Security Groups
↓
Sets
↓
Maps / Relationships
↓
Stateful Policy
↓
Default Deny Instead of hard-coding every host into every rule, define meaningful objects such as:
@trusted_admins
@managed_servers
@web_servers
@web_ports
@app_servers
@database_servers
@database_ports Then express the security relationships between them.
This makes the firewall easier to:
read
audit
modify
troubleshoot
automate
scale The official Netfilter nftables project emphasizes sets, maps, and concatenations as important nftables mechanisms for efficient and scalable rule evaluation.
For the foundation beneath this configuration, start with CorgiNetwork’s Linux nftables Inter-VLAN Firewall, which explains default-deny forwarding, connection tracking, and stateful VLAN policies.
If the VLAN architecture itself is unclear, Linux Bridge VLAN Filtering Explained covers PVIDs, tagged/untagged traffic, access ports, and trunks.
Then Linux Inter-VLAN Routing with Network Namespaces explains the Layer 3 routing path between those isolated VLANs.
For more advanced path selection, Linux Policy Routing with ip rule and Multiple Routing Tables explains how Linux can select different routing tables according to policy.
And when the operating system unexpectedly sends traffic through the wrong NIC, Linux Traffic Uses the Wrong Interface provides a packet-path troubleshooting workflow.
Together, the CorgiNetwork Linux networking cluster now progresses from:
Namespaces
↓
veth
↓
Linux Bridge
↓
VLAN Filtering
↓
Inter-VLAN Routing
↓
Stateful nftables
↓
Sets and Maps
↓
Scalable Firewall Architecture The next stage should not simply add more firewall syntax. A stronger progression is to build a real Linux DMZ architecture where Internet-facing services are isolated from internal servers and management networks using VLANs, routing, nftables, NAT, and strict traffic flows.
AI Agents as Code: Managing AI Agent Configurations, Prompts and Tools with Git AI agents…
Data Center Power Distribution Architecture: UPS, PDU, Redundancy and Rack Power Explained Data center power…
DNSSEC Troubleshooting: Fix SERVFAIL & DNSKEY Errors DNSSEC Troubleshooting: How to Fix SERVFAIL, DS, DNSKEY…
Terraform State Management Explained: Remote State, Locking, Backends and Best Practices Terraform makes infrastructure reproducible…
RTO vs RPO Explained: Disaster Recovery Objectives, Examples and Planning A disaster recovery plan is…
Network Intrusion Detection Explained: NIDS, Sensors, Signatures and Traffic Analysis A firewall can decide whether…