A Linux firewall often starts with a few simple rules: allow SSH, permit web traffic, accept established connections, and drop everything else. That approach works for a small server, but it becomes difficult to maintain when the system grows to include multiple interfaces, VLANs, a DMZ, NAT, IPv4 and IPv6, policy routing, monitoring, and different security zones.
The challenge is no longer learning another nftables command. It is designing the ruleset as an architecture.
A production firewall should make it easy to answer five questions:
This guide shows how to build an advanced nftables firewall architecture using tables, base chains, custom chains, connection tracking, sets, maps, counters, controlled logging, NAT, and clearly separated security zones.
It builds on the CorgiNetwork guides covering nftables Sets and Maps, nftables Dynamic Sets and Meters, nftables Inter-VLAN Firewall, and nftables Conntrack States and Troubleshooting.
For authoritative reference material, the official nftables documentation maintained by the Netfilter project should be used when validating syntax and behavior for a particular nftables and Linux kernel version.
Consider a Linux router connecting several networks:
Internet
|
WAN
|
+-------------+
| Linux |
| nftables |
| Firewall |
+-------------+
/ | \
/ | \
Users Servers DMZ
VLAN VLAN VLAN
\
Management
VLAN A flat ruleset might contain hundreds of statements such as:
ip saddr 192.168.10.0/24 ip daddr 192.168.20.10 tcp dport 443 accept
ip saddr 192.168.30.0/24 ip daddr 192.168.20.10 tcp dport 22 accept
ip saddr 192.168.40.0/24 ip daddr 192.168.20.53 udp dport 53 accept These rules may be technically correct, but as their number grows it becomes increasingly difficult to understand the security policy.
A better design starts with zones and relationships:
Users → Servers HTTPS
Management → Servers SSH
Users → DNS Servers DNS
Internet → DMZ HTTP/HTTPS
DMZ → LAN Restricted The firewall configuration should express those relationships clearly.
That is the central principle of scalable nftables design:
Organize rules around security policy rather than individual packets, addresses, or ports.
Before writing nftables rules, identify the trust boundaries in the network.
A typical environment might contain:
| Zone | Example Network | Purpose |
|---|---|---|
| WAN | Public Internet | Untrusted external traffic |
| USERS | 192.168.10.0/24 | Workstations |
| SERVERS | 192.168.20.0/24 | Internal services |
| MANAGEMENT | 192.168.30.0/24 | Administrative access |
| IOT | 192.168.40.0/24 | Restricted devices |
| DMZ | 192.168.50.0/24 | Public-facing servers |
Now policy becomes easier to define.
For example:
USERS → SERVERS
Allow HTTPS and DNS where required.
MANAGEMENT → SERVERS
Allow administrative services.
IOT → SERVERS
Deny by default; allow explicitly required services.
WAN → DMZ
Allow only published services.
DMZ → LAN
Deny by default. If VLAN routing is part of the topology, the packet-flow foundation is explained in Linux Inter-VLAN Routing with Network Namespaces, while actual filtering between those networks is covered in nftables Inter-VLAN Firewall.
For many dual-stack firewalls, the inet family provides a convenient place for filtering both IPv4 and IPv6 traffic.
A basic structure can begin with:
table inet firewall {
} Inside that table, you can create filtering chains for:
INPUT
FORWARD
OUTPUT while using protocol-specific expressions such as:
ip saddr for IPv4 and:
ip6 saddr for IPv6 when necessary.
Using inet does not mean IPv4 and IPv6 policies become automatically identical. Both still need to be considered explicitly.
The nftables Families documentation explains the available families and their purposes.
A scalable firewall begins by putting policy in the correct packet path.
Traffic destined for the firewall itself.
Examples:
SSH to the firewall
DNS service running on the firewall
Monitoring of the firewall Traffic passing through the Linux system.
Examples:
Users → Servers
Internet → DMZ
VLAN 10 → VLAN 20 Traffic generated locally by the firewall.
Examples:
DNS queries generated by the firewall
package updates
monitoring traffic A common troubleshooting mistake is putting a routing rule in input when the packet is actually traversing forward.
Before debugging a rule, determine whether the Linux host is the destination or the router.
A clean base structure might look like:
table inet firewall {
chain input {
type filter hook input priority filter;
policy drop;
}
chain forward {
type filter hook forward priority filter;
policy drop;
}
chain output {
type filter hook output priority filter;
policy accept;
}
} This example uses default-drop policies for inbound and forwarded traffic.
That creates a simple security model:
Not explicitly permitted
↓
DROP A default-drop architecture can make policy easier to reason about, but it must be deployed carefully on remote systems because missing management rules can lock out administrators.
One of the most useful structural improvements is keeping base chains relatively small.
Instead of placing every policy directly inside forward, use it primarily for classification and dispatch.
Conceptually:
FORWARD
|
+-- invalid handling
|
+-- established traffic
|
+-- WAN policy
|
+-- VLAN policy
|
+-- DMZ policy
|
+-- final handling This makes the packet path much easier to inspect.
You can create custom chains such as:
chain wan_to_dmz {
}
chain users_to_servers {
}
chain management_to_servers {
}
chain iot_policy {
} Then dispatch traffic from the base chain.
Conceptually:
FORWARD
|
+-- USERS → SERVERS
| |
| └── users_to_servers
|
+-- MANAGEMENT → SERVERS
| |
| └── management_to_servers
|
+-- WAN → DMZ
|
└── wan_to_dmz This provides two major benefits.
First, the ruleset becomes easier to read.
Second, troubleshooting becomes easier because each chain has a clearly defined responsibility.
Most production firewalls are stateful.
A common pattern is:
ct state invalid counter drop
ct state established,related counter accept before evaluating rules for new connections.
The architecture becomes:
Packet
|
+-- INVALID? → DROP
|
+-- ESTABLISHED/RELATED? → ACCEPT
|
└-- NEW → evaluate security policy This prevents every packet belonging to an already approved connection from being repeatedly evaluated against the full new-connection policy.
Connection tracking is sufficiently important that it should be understood independently. See nftables Conntrack States and Troubleshooting for the detailed packet-state model.
Do not repeatedly hard-code the same networks.
Instead of:
ip saddr 192.168.10.0/24 ...
ip saddr 192.168.20.0/24 ...
ip saddr 192.168.30.0/24 ... define meaningful objects.
For example:
set management_networks {
type ipv4_addr
flags interval
elements = {
192.168.30.0/24
}
} and:
set server_networks {
type ipv4_addr
flags interval
elements = {
192.168.20.0/24
}
} Now rules can express intent:
ip saddr @management_networks \
ip daddr @server_networks \
tcp dport 22 \
accept This reads almost like a policy statement.
For deeper coverage of named sets, interval sets, maps, concatenations, and verdict maps, see nftables Sets and Maps.
The same principle applies to ports.
Instead of:
tcp dport 80 accept
tcp dport 443 accept define:
set web_ports {
type inet_service
elements = { 80, 443 }
} Then:
tcp dport @web_ports accept Other useful service groups might include:
@web_ports
@dns_ports
@monitoring_ports
@management_ports Do not create a set for every single value simply because nftables allows it. Use sets when they improve reuse or readability.
Suppose:
Management:
192.168.30.0/24
Servers:
192.168.20.0/24 Administrators need SSH access to servers.
A dedicated chain might contain:
chain management_to_servers {
ip saddr @management_networks \
ip daddr @server_networks \
tcp dport 22 \
ct state new \
counter \
accept
} The policy is clear:
Management
↓
Servers
↓
SSH
↓
ACCEPT No unrelated DMZ or IoT rules belong in this chain.
Suppose normal users should reach internal web applications but not SSH.
A dedicated chain can implement:
chain users_to_servers {
ip saddr @user_networks \
ip daddr @web_servers \
tcp dport @web_ports \
ct state new \
counter \
accept
} This keeps user access separate from management access.
If SSH is later added to the management policy, it does not accidentally become available to user networks.
IoT networks deserve explicit restrictions because many devices require only a narrow set of services.
A reasonable architecture is:
IoT
|
+→ DNS
|
+→ NTP
|
+→ selected Internet services
|
└→ Internal servers → DENY unless required Instead of trusting the entire IoT network because it is “internal,” treat it as its own security zone.
This is exactly the kind of policy that becomes easier to maintain with dedicated chains and named sets.
A DMZ should not simply be another internal subnet.
Conceptually:
Internet
↓
Firewall
↓
DMZ
X
Internal LAN Public services may need inbound access from the Internet, but that does not imply unrestricted access from the DMZ to internal systems.
A stronger policy is:
WAN → DMZ
Allow published services.
DMZ → LAN
Drop by default.
MANAGEMENT → DMZ
Allow required administrative services.
DMZ → Internet
Allow only what the servers actually require. For a complete implementation, see Linux DMZ with nftables, VLAN, NAT and Firewall.
Filtering answers:
Should this traffic be permitted?
NAT answers:
Should an address or port be translated?
These are different responsibilities.
A clean architecture therefore treats NAT and filtering as related but separate components.
For example:
Internet
↓
DNAT
↓
Internal/DMZ Server
↓
FORWARD filtering
↓
ACCEPT or DROP A DNAT rule does not automatically mean that forwarding policy is correct.
Likewise, an accept rule does not automatically configure destination translation.
This distinction is critical when troubleshooting published services.
If port forwarding is involved, use nftables DNAT Port Forwarding Not Working to trace the full path.
Internal clients sometimes access an internal service using its public address:
LAN Client
↓
Public IP
↓
Firewall
↓
Internal Server This is a special NAT path and should not be mixed conceptually with ordinary Internet-to-server DNAT.
Depending on the topology, both destination and source translation may be necessary for the return path to remain symmetrical.
For that scenario, use nftables Hairpin NAT and NAT Loopback.
Keeping specialized NAT behavior in dedicated logic makes the overall firewall easier to maintain.
Sets answer questions such as:
Is this address a member of this group? Maps answer:
Given this key, what value corresponds to it? When a firewall grows, maps can replace repeated relationships.
Verdict maps can also direct traffic to specific verdicts or chains.
For example, a service-dispatch architecture can conceptually behave like:
TCP/22
↓
SSH policy
TCP/80
↓
Web policy
TCP/443
↓
Web policy rather than using a long sequence of repetitive dispatch rules.
The official nftables Maps documentation provides the underlying syntax and behavior.
After the static architecture is correct, dynamic sets and per-source rate controls can be added for selected services.
Do not begin with complex dynamic logic.
The correct progression is:
Routing
↓
Security zones
↓
Stateful firewall
↓
Static sets/maps
↓
Logging/counters
↓
Dynamic controls For example, an Internet-facing SSH service might use dynamic state to track new connections per source.
But if SSH should only be reachable through the management VLAN or VPN, restricting access at the zone level is usually the more fundamental control.
For dynamic tracking, see nftables Dynamic Sets and Meters.
A production firewall should be observable.
Adding:
counter to important policy rules allows you to answer:
Is this rule actually being used? For example:
ip saddr @management_networks \
ip daddr @server_networks \
tcp dport 22 \
counter \
accept If the counter remains zero while testing SSH, the packet is probably:
That immediately narrows the troubleshooting scope.
Logging every dropped packet can create enormous log volumes.
Instead, log meaningful events with appropriate rate controls.
A typical design is:
Packet
↓
Counter
↓
Rate-limited log
↓
DROP The rate limit should control logging volume, not accidentally determine whether prohibited traffic is dropped.
For example, conceptually:
limit rate 5/second \
log prefix "NFT_FORWARD_DROP: "
counter drop The first rule controls how many messages are generated.
The second enforces the policy.
This separation is much safer than combining logging and enforcement without understanding the control flow.
A firewall with random log prefixes is difficult to search.
Use predictable names such as:
NFT_INPUT_DROP:
NFT_FORWARD_DROP:
NFT_WAN_DMZ_DROP:
NFT_IOT_DROP:
NFT_INVALID: Then tools such as journalctl, log collectors, or SIEM platforms can filter events by policy area.
Logging becomes part of the firewall architecture rather than an emergency debugging feature.
The following example demonstrates the overall structure rather than serving as a drop-in configuration for every network:
table inet firewall {
set user_networks {
type ipv4_addr
flags interval
elements = { 192.168.10.0/24 }
}
set server_networks {
type ipv4_addr
flags interval
elements = { 192.168.20.0/24 }
}
set management_networks {
type ipv4_addr
flags interval
elements = { 192.168.30.0/24 }
}
set web_servers {
type ipv4_addr
elements = {
192.168.20.10,
192.168.20.11
}
}
set web_ports {
type inet_service
elements = { 80, 443 }
}
chain input {
type filter hook input priority filter;
policy drop;
iifname "lo" accept
ct state invalid counter drop
ct state established,related counter accept
ip saddr @management_networks \
tcp dport 22 \
ct state new \
counter \
accept
limit rate 5/second \
log prefix "NFT_INPUT_DROP: "
counter drop
}
chain forward {
type filter hook forward priority filter;
policy drop;
ct state invalid counter drop
ct state established,related counter accept
ip saddr @management_networks \
ip daddr @server_networks \
jump management_to_servers
ip saddr @user_networks \
ip daddr @server_networks \
jump users_to_servers
limit rate 5/second \
log prefix "NFT_FORWARD_DROP: "
counter drop
}
chain management_to_servers {
tcp dport 22 \
ct state new \
counter \
accept
return
}
chain users_to_servers {
ip daddr @web_servers \
tcp dport @web_ports \
ct state new \
counter \
accept
return
}
} Notice what this architecture does not do.
It does not put hundreds of unrelated rules into one chain.
Instead:
BASE CHAIN
↓
classify traffic
↓
CUSTOM CHAIN
↓
apply zone-specific policy
↓
VERDICT That is the structure we want.
Inside a busy filtering chain, a logical order is often:
1. Essential special handling
2. INVALID handling
3. ESTABLISHED / RELATED
4. Explicitly trusted infrastructure
5. New-connection policies
6. Zone-specific dispatch
7. Controlled logging
8. Default drop The exact order depends on the firewall.
The important principle is that rules should follow a deliberate hierarchy rather than being appended wherever convenient.
A rule such as:
ip saddr 192.168.0.0/16 accept may undermine an otherwise carefully segmented architecture.
It effectively says:
Any host in a huge internal address range
↓
trusted This can bypass distinctions between:
users
servers
IoT
management
DMZ Internal does not automatically mean trusted.
Security zones exist precisely because different internal systems have different risk profiles and access requirements.
Interfaces are useful classification signals:
iifname "wan0" or:
iifname "vlan30" but production policy often benefits from checking additional context such as:
source network
destination network
service
connection state A packet arriving through the expected interface is not automatically authorized.
Suppose:
nftables counter increases and the rule accepts the packet, but the application still fails.
At that point, investigate routing.
On multi-homed Linux systems, a packet may enter through one interface while the reply leaves through another.
If the kernel selects an unexpected route, see Linux Traffic Uses the Wrong Interface and Linux Policy Routing with ip rule and Multiple Routing Tables.
The firewall can approve a connection and still be blamed for a routing failure it did not cause.
Reverse-path filtering can interact with asymmetric routing.
A topology such as:
Request → eth0
Reply → eth1 deserves special attention.
Do not disable security controls blindly because a multi-homed service fails.
First understand the routing path and the host’s rp_filter configuration.
Our Linux rp_filter and Asymmetric Routing guide covers this scenario in detail.
If multiple interfaces are connected to the same Layer 2 network, unexpected ARP behavior can make a firewall problem appear mysterious.
Before rewriting nftables rules, inspect:
neighbor table
ARP replies
interface addresses
routing
packet capture For multi-NIC same-subnet systems, see Linux ARP Flux with Multiple NICs on the Same Subnet.
The important troubleshooting principle is:
Do not fix a Layer 2 problem with a Layer 3/4 firewall rule.
When traffic fails, use the same workflow every time.
Check:
ip route get 192.168.20.10 For policy-routing environments, inspect:
ip rule show
ip route show table all Use:
tcpdump -ni any host 192.168.20.10 Add protocol and port filters where possible.
Use:
nft list ruleset Determine which counters increase.
For stateful problems:
conntrack -L Narrow the output whenever possible.
If DNAT or SNAT is involved, determine what the packet looks like before and after translation.
For difficult cases, the official nftables tracing documentation describes how nftrace can be combined with:
nft monitor trace to follow selected packets through the ruleset.
The workflow is therefore:
ROUTE
↓
PACKET CAPTURE
↓
COUNTERS
↓
CONNTRACK
↓
NAT
↓
TRACE This is far more effective than randomly adding accept rules.
A syntax error in a production firewall configuration can cause serious problems.
When using a persistent nftables configuration, validate it before applying it.
For example, depending on the deployment:
sudo nft -c -f /etc/nftables.conf The -c option performs a check without applying the ruleset.
Then, after validation, the configuration can be loaded using the appropriate deployment procedure.
This is particularly important when editing a firewall remotely.
One of nftables’ important design advantages is support for atomic ruleset replacement through its transactional model.
That matters because firewall updates should not temporarily leave the machine in an inconsistent state.
The official nftables project documents nftables as the modern Netfilter packet-classification framework replacing the older iptables-family tools.
For complex production configurations, maintaining the ruleset in a file and loading a validated configuration is generally easier to review than executing hundreds of independent commands manually.
A production firewall is infrastructure code.
The configuration should ideally have:
version history
change descriptions
peer review
rollback capability
documentation A Git repository can make it possible to answer:
Who changed the firewall?
What changed?
When?
Why?
What was the previous working configuration? Do not store passwords, private keys, or other secrets in an ordinary ruleset repository.
Bad comment:
# Allow port 443 The rule itself already tells you that.
Better:
# Allow user VLANs to reach the internal customer portal over HTTPS A useful comment explains why the rule exists.
Months later, that context is much more valuable than restating the command.
A consistent naming convention might use:
@user_networks
@server_networks
@management_networks
@web_servers
@dns_servers
@web_ports
@management_ports and chains such as:
users_to_servers
management_to_servers
wan_to_dmz
dmz_to_internal
iot_policy This makes large configurations self-documenting.
A firewall architecture is incomplete if IPv4 is carefully filtered while IPv6 is effectively ignored.
For every service, ask:
Is it reachable through IPv4?
Is it reachable through IPv6?
Should the policy be equivalent?
Are both paths logged and monitored?
Does routing differ? The inet family can help organize dual-stack filtering, but it does not automatically design IPv6 security policy for you.
A firewall test plan should verify both:
what should work and:
what must fail For example:
Management → Server SSH
EXPECTED: ACCEPT
Users → Server HTTPS
EXPECTED: ACCEPT
Users → Server SSH
EXPECTED: DROP
IoT → Management
EXPECTED: DROP
Internet → Internal LAN
EXPECTED: DROP
Internet → Published DMZ HTTPS
EXPECTED: ACCEPT A firewall is not validated simply because the administrator can still connect.
The most damaging nftables architecture problems are usually structural rather than syntactic.
One giant chain: Hundreds of unrelated rules become difficult to audit and troubleshoot.
Duplicated IP addresses: Use named sets when values are reused.
Duplicated ports: Group meaningful services where appropriate.
Mixing NAT and filtering mentally: Translation and authorization solve different problems.
Allowing entire internal address ranges: Internal networks should still be segmented by role.
Ignoring conntrack: Stateful policy becomes unnecessarily complex.
Logging everything: Excessive logs can hide the events that matter.
No counters: Troubleshooting becomes guesswork.
No IPv6 policy: Services may remain reachable through a path that was never reviewed.
No rollback strategy: A small configuration error can become an outage.
Random rule ordering: Policy becomes dependent on accidental placement rather than deliberate design.
Before considering a firewall production-ready, verify:
□ Security zones are documented.
□ INPUT, FORWARD and OUTPUT responsibilities are understood.
□ Default policies are intentional.
□ INVALID traffic has an explicit policy.
□ ESTABLISHED and RELATED traffic is handled deliberately.
□ New connections follow explicit zone policies.
□ Repeated networks are represented with meaningful sets.
□ Repeated services are grouped where useful.
□ Custom chains have clear responsibilities.
□ NAT and filtering are designed separately.
□ DMZ-to-internal access is restricted.
□ Management traffic has its own policy.
□ IoT traffic is not implicitly trusted.
□ IPv6 has been reviewed.
□ Important rules have counters.
□ Logging is selective and rate-controlled.
□ Log prefixes are consistent.
□ Routing has been tested separately from filtering.
□ Multi-homed systems have been checked for asymmetric routing.
□ Configuration can be validated before deployment.
□ A rollback or console recovery path exists.
□ Allowed and denied flows have both been tested. If these questions have clear answers, the firewall is much easier to maintain.
There is no universal ruleset for every network, but a scalable architecture usually separates base chains, connection-state handling, security zones, reusable sets, zone-specific custom chains, NAT, counters, and logging.
The objective is to make policy understandable rather than minimize the number of lines at any cost.
Small servers can work with a few base-chain rules, but larger routers and firewalls benefit from custom chains that separate policies such as WAN-to-DMZ, management-to-server, and user-to-server traffic.
Default-drop policies are common when administrators want only explicitly permitted traffic to pass. They require careful planning because missing management or service rules can cause outages.
In many stateful firewall architectures, established,related traffic is handled early while new connections proceed through the full access policy. The exact ordering must still match the network’s security requirements.
Use sets when collections of addresses, networks, ports, or other values are reused or when they make the policy easier to understand and maintain.
A set primarily tests membership. A map associates a key with a value. Verdict maps can associate keys with firewall verdicts or control flow.
The detailed explanation is available in nftables Sets and Maps.
NAT and filtering are related parts of the packet path, but they perform different functions. Keep their purposes separate so that translation problems are not confused with authorization problems.
Use a repeatable sequence:
route
→ packet capture
→ counters
→ conntrack
→ NAT inspection
→ targeted nftables trace This identifies the failing layer instead of relying on trial and error.
A production nftables firewall should not be designed as an ever-growing list of accept and drop commands.
The architecture should move from:
hundreds of independent rules toward:
NETWORK DESIGN
↓
SECURITY ZONES
↓
BASE CHAINS
↓
CONNTRACK
↓
SETS / MAPS / OBJECTS
↓
POLICY CHAINS
↓
COUNTERS + LOGGING
↓
VERDICT Each component should have a clear responsibility.
Routing determines where traffic travels.
Conntrack identifies connection state.
Sets represent reusable groups.
Maps represent relationships.
Custom chains organize security policy.
Counters show whether policy is actually being used.
Logging records important events.
NAT performs translation.
Verdicts enforce the final decision.
This structure also connects the CorgiNetwork Linux networking cluster into one logical learning path.
Start with Linux Bridge Networking with Network Namespaces and veth when building virtual network topologies, then move to Linux Inter-VLAN Routing with Network Namespaces for routed segmentation.
Use nftables Inter-VLAN Firewall to secure those routed networks and Linux DMZ with nftables when public services require a separate trust zone.
For NAT problems, continue with nftables DNAT Port Forwarding Not Working and nftables Hairpin NAT and NAT Loopback.
For stateful filtering, use nftables Conntrack States and Troubleshooting, followed by nftables Sets and Maps and nftables Dynamic Sets and Meters.
When packets still follow the wrong path despite correct firewall rules, move outside nftables and inspect Linux Traffic Uses the Wrong Interface, Linux Policy Routing with ip rule and Multiple Routing Tables, Linux rp_filter and Asymmetric Routing, and Linux ARP Flux with Multiple NICs on the Same Subnet.
The result is not merely a collection of tutorials. It is a connected technical architecture covering the complete path from Linux routing and Layer 2 behavior to VLAN segmentation, NAT, stateful filtering, advanced nftables policy, and production troubleshooting.
And the key design principle is simple:
A good firewall should make the network’s security policy easier to understand—not hide it behind more rules.
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…