nftables Sets and Maps: Build a Scalable Linux Firewall 2026

nftables Sets and Maps: Build a Scalable Linux Firewall Without Hundreds of Rules

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:

  • multiple VLANs;
  • management hosts;
  • web servers;
  • DNS resolvers;
  • database servers;
  • monitoring systems;
  • trusted administration networks;
  • blocked networks;
  • dozens of allowed services.

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.

Why Large Firewall Rulesets Become Difficult

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.


What Is an nftables Set?

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

Anonymous Sets

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.


Named Sets

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.


Use the Named Set in 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.


Why This Is Better

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.


Remove an Element

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.


Create a Server Set

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.


Create a Web Server Set

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.


Sets Improve Segmentation

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.


Create a Named Port Set

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.


Why Named Port Sets Can Be Better Than Inline Sets

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.


Network Sets

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.


VLANs, Sets, and Routing

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

Updating Sets Without Rewriting Policy

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.


What Is an nftables Map?

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.


Why Maps Matter

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.


Sets vs Maps

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.


What Are Concatenations?

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.


Why Concatenations Are Powerful

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.


A Scalable Security Model

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.


Add Application Servers

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

What This Policy Says

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.


Keep Stateful Rules

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 Cleaner Firewall Structure

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.


Add Counters to Important Policies

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.


Inspect the Sets

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.


Troubleshooting: A Host Should Be Allowed but Is Blocked

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

Troubleshooting: Set Is Correct but Counter Is Zero

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.


Troubleshooting with nft monitor trace

For 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.


Sets Do Not Fix Routing Problems

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.


Sets Do Not Fix VLAN Problems

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.


Avoid Overly Broad Sets

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.


Use Meaningful Names

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.


Separate Security Roles

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 and Automation

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.


Atomic Ruleset Management

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.


Production Firewall Design Checklist

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

Sets vs Hundreds of Rules

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.


When Should You Use an Anonymous Set?

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.


When Should You Use a Named Set?

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

When Should You Use a Map?

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.


When Should You Use Concatenations?

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.


Frequently Asked Questions

What is an nftables set?

An nftables set is a collection of typed values that can be referenced by firewall rules.

Typical values include IP addresses and service ports.


What is the difference between named and anonymous nftables sets?

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.


Can I add an IP without recreating the firewall rule?

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.


Can nftables sets contain ports?

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.


What is an nftables map?

A map associates keys with values.

This extends the membership concept of a set and can be used to build more sophisticated policies.


Are nftables sets faster than writing many rules?

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.


Do sets replace conntrack?

No.

Sets organize and match data.

Conntrack maintains connection state.

A stateful firewall commonly uses both:

conntrack
+
sets/maps
+
routing
+
firewall policy

Conclusion

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.

Leave a Reply

Your email address will not be published. Required fields are marked *