Network Security Zones

Production engineering guide for network security zones covering patterns, implementation strategies, and operational best practices.

Network security zones are administrative and logical boundaries that enforce traffic flow policies between different trust levels or environmental contexts. They matter whenever traffic needs to be segmented—such as between development and production, between internal services and external-facing components, or between workloads with different compliance requirements. Zones prevent accidental data leakage and enforce least-privilege by controlling what can enter, leave, and move within each boundary.

The following sections address the most common zone-related tasks, organized by what you’re trying to accomplish. All commands use standard Linux networking tools (iptables, nftables) or firewalld’s command-line interface, which are available on any standard Linux distribution.

Classifying traffic into zones

The first step in any zone deployment is determining which packets belong to which zone. Classification happens at the boundary point—typically the input, forward, or output hook—and relies on matching criteria that are stable, specific, and easy to audit.

Interface-based marking

If your infrastructure assigns dedicated physical or virtual interfaces to different trust levels, you can classify traffic by interface name. This is the most straightforward approach when network topology is under your control.

# iptables: create a zone chain and jump to it from the input hook
iptables -N ZONE_INTERNAL
iptables -A INPUT -i eth1 -j ZONE_INTERNAL

# nftables: equivalent using a named chain and hook
nft add table ip filter
nft add chain ip filter ZONE_INTERNAL { type filter hook input priority 0; }

Packets arriving on eth1 are immediately directed into the ZONE_INTERNAL chain, where further matching can refine the classification.

CIDR-based classification

When interfaces are shared but traffic sources differ, classify by source address range. This is common in multi-tenant environments or when routing directs different CIDRs to the same egress point.

# iptables: match source CIDR and assign to a zone
iptables -A ZONE_INTERNAL -s 10.0.0.0/8 -j ACCEPT
iptables -A ZONE_INTERNAL -s 192.168.0.0/16 -j DROP

# nftables: use sets for reusable address groups
nft add set ip filter INTERNAL_NET { type ip addr; elements = { 10.0.0.0/8, 172.16.0.0/12 } }
nft add rule ip filter ZONE_INTERNAL ip saddr @INTERNAL_NET accept

Create named sets (INTERNAL_NET) so the same address ranges can be referenced across multiple rules without duplication.

Rich rule matching

Both iptables (with the set match extension, available in newer versions) and nftables support rich rule syntax that combines multiple criteria.

# iptables: match on source address AND destination port
iptables -A ZONE_INTERNAL -m set --match-set INTERNAL_NET src -p tcp --dport 443 -j ACCEPT

# nftables: rich rules are native
nft add rule ip filter ZONE_INTERNAL ip saddr @INTERNAL_NET tcp dport 443 accept

Rich rules let you encode policy intent directly in the matching logic, rather than deferring to separate policy chains.

Zone-internal policies

Once traffic is classified into a zone, the next question is what’s allowed to happen *within* that zone. Internal policies govern connectivity between workloads that share the same trust level but may need to communicate for functional reasons.

Allow-all with audit

A common pattern for internal zones is to permit all traffic but log it, providing visibility without immediate enforcement. This is useful during deployment or when migrating between zone models.

# iptables: accept and log internal zone traffic
iptables -A ZONE_INTERNAL -j ACCEPT
iptables -A ZONE_INTERNAL -j LOG --log-prefix "ZONE_INTERNAL: "

# nftables: accept with logging
nft add rule ip filter ZONE_INTERNAL accept
nft add rule ip filter ZONE_INTERNAL log prefix "ZONE_INTERNAL: "

Remember that LOG in iptables uses the kernel log buffer; in high-traffic environments, this can drop packets if the buffer fills. Adjust log-level and use a dedicated log collector if you rely on this for audit.

Explicit allow list

When you need strict control, replace the allow-all with an explicit permit list. This is the recommended pattern for production zones handling sensitive data.

# iptables: explicit allows only
iptables -N ZONE_INTERNAL_POLICY
iptables -A ZONE_INTERNAL_POLICY -p tcp --dport 8080 -j ACCEPT
iptables -A ZONE_INTERNAL_POLICY -p tcp --dport 9090 -j ACCEPT
iptables -A ZONE_INTERNAL -j ZONE_INTERNAL_POLICY

# nftables: ordered ruleset
nft add chain ip filter ZONE_INTERNAL_POLICY {
  type filter hook input priority 0; policy accept;
  tcp dport 8080 accept
  tcp dport 9090 accept
}
nft add rule ip filter ZONE_INTERNAL ip dport { 8080, 9090 } accept

Order matters in both tools. Rules are evaluated top-to-bottom; the first match wins. Place more specific rules before broader ones to avoid accidental catch-all acceptance.

Implicit deny

Always close the zone with an explicit deny rule. Without it, the kernel may accept packets that fall through all prior rules, depending on the chain policy and whether other tables contribute rules to the same hook.

# iptables: implicit deny at the end
iptables -A ZONE_INTERNAL -j DROP

# nftables: set default policy to drop
nft set chain ip filter ZONE_INTERNAL policy drop

The policy drop in nftables applies to the entire chain, ensuring that any packet not explicitly accepted is dropped. In iptables, you must add a -j DROP rule at the end; the default chain policy is ACCEPT, which is a frequent source of confusion.

Inter-zone traffic controls

Traffic crossing zone boundaries is where most misconfigurations surface. The default behavior at a boundary depends on how the zones are defined, but the safest starting point is to deny all cross-zone traffic and selectively permit what’s needed.

Default-deny at boundaries

When two zones share a host or network segment, the most secure default is to block all traffic in both directions unless a rule explicitly allows it.

# iptables: deny from INTERNAL to DMZ by default
iptables -A ZONE_INTERNAL -o eth2 -j DROP
iptables -A ZONE_DMZ -i eth1 -j DROP

# nftables: bidirectional deny via separate rules
nft add rule ip filter ZONE_INTERNAL oif eth1 drop
nft add rule ip filter ZONE_DMZ iif eth1 drop

Note that oif (output interface) and iif (input interface) refer to the interface the packet is leaving or entering, respectively. Mixing these up is a common error—packets entering from eth1 are matched with iif eth1, not oif eth1.

Selective permit rules

After establishing the default deny, add rules for the specific services that must cross zones. Place these rules *before* the default deny so they’re evaluated first.

# iptables: allow SSH from INTERNAL to DMZ for management
iptables -A ZONE_INTERNAL -o eth2 -p tcp --dport 22 -j ACCEPT
iptables -A ZONE_INTERNAL -o eth2 -j DROP

# nftables: same pattern, with the permit before the deny
nft add rule ip filter ZONE_INTERNAL oif eth1 tcp dport 22 accept
nft add rule ip filter ZONE_INTERNAL oif eth1 drop

A frequent gotcha: forgetting that tcp dport 22 only matches TCP. If the service also uses UDP or a different protocol, add separate rules for each, or use meta l4proto tcp as a prerequisite match.

NAT and zone boundaries

Network address translation often interacts unexpectedly with zone boundaries. NAT occurs before routing decisions in the nat table, which means a packet’s apparent source address may differ from what zone-match rules see.

# iptables: NAT happening in the nat table, zone matching in the filter table
# A packet SNAT'd from 10.0.0.2 to 203.0.113.5 arriving on eth1
# will be matched by source address 203.0.113.5, not 10.0.0.2

# nftables: same architecture—nat and filter tables are separate
# Use mark-based routing or connection tracking to preserve intent

If you need zone-aware NAT, consider using connection tracking marks (ct mark) to tag packets before NAT, then match on those marks in the filter table. This keeps the NAT and zone logic decoupled and auditable.

Sharp edges

“Zone” vs “chain” confusion

In iptables, it’s common to create a chain named ZONE_trust and assume it’s a zone. The chain is just a rule collection; it has no inherent zone semantics. Unless you also have a mechanism that jumps into or references that chain based on a zone identifier, packets may bypass it entirely.

# BUG: creating a chain named "ZONE_trust" does NOT classify traffic
iptables -N ZONE_trust
# Packets still arrive at INPUT with no zone assignment
# Fix: ensure something routes packets into ZONE_trust based on your classification criteria

In nftables, the equivalent risk is creating a set named zone_trust but never referencing it in a rule that actually processes traffic. Both tools are silent about missing references—packets simply fall through to default policies.

Interface vs. CIDR mismatch

A zone classified by interface eth1 will miss traffic that arrives on a different interface carrying the same CIDR, and vice versa. This surfaces most often during failover or when load balancers redirect traffic.

# If eth1 is marked INTERNAL but traffic arrives on eth0 with 10.0.0.1 source,
# the interface-based rule misses it, and the CIDR rule (if present) may or may not match
# depending on rule order and other tables' contributions

The fix is to make classification criteria orthogonal—combine interface and source address in a single rule, or ensure every path into the host is accounted for.

Silent acceptance through connection tracking

Packets belonging to established connections may be accepted even after a zone policy changes, because connection tracking (conntrack) remembers the earlier allow decision. This can mask policy bugs during rollout.

# After adding a DROP rule for ZONE_A to ZONE_B, existing connections
# still flow because conntrack has already recorded them as ESTABLISHED
# Fix: use ctstate ESTABLISHED,RELATED with explicit zone checks, or flush conntrack
# after policy changes in non-production environments

Flush conntrack deliberately during planned maintenance windows, not after unexpected policy changes in production. In iptables, iptables -F flushes all tables; in nftables, nft delete table ip filter removes everything and must be followed by a recreation.

Default policy gotcha

As noted earlier, iptables chains default to ACCEPT; nftables chains default to accept policy unless changed. This means a zone chain with no rules still accepts everything—a silent failure that’s tripped many operators migrating from iptables to nftables or between distributions.

# Verify your zone’s default behavior
# iptables: check with iptables -L ZONE_INTERNAL -n --line-numbers
# nftables: nft list ruleset | grep ZONE_INTERNAL

# If you see no rules and the chain is accepting, add an explicit policy
nft set chain ip filter ZONE_INTERNAL policy drop

Make verification a routine step after any zone configuration change, especially when promoting from test to production environments.

Debugging checklist

When a zone isn’t enforcing as expected, run through these checks in order:

  1. Confirm the packet is reaching the zone hook. Use tcpdump or nft monitor on the relevant interface to see if the packet arrives at all.
  1. Verify the matching criteria. Run the matching rule with -v (iptables) or inspect the rule set (nftables) to confirm the source address, interface, port, or protocol matches what you think it does.
  1. Check the rule order. In both iptables and nftables, rules are evaluated sequentially. A permit rule placed after a deny rule for the same traffic will never be reached.
  1. Inspect conntrack state. conntrack -L (iptables-nft) or nft list ruleset with attention to ct entries can reveal whether an existing connection is bypassing the new policy.
  1. Validate the default policy. iptables -L ZONE_INTERNAL -n shows ACCEPT as default if no -j DROP rule exists. nft list tables shows policy drop or policy accept for each chain.
  1. Flush and re-apply in a controlled window. If in doubt, flush the table, re-apply all rules in the correct order, and verify with test traffic before committing to production.

This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.