Designing Minimal Secure Network Segmentation on CentOS Stream 9 with Firewalld Zones
Build a lean, secure multi‑tier network on CentOS Stream 9 by assigning each tier’s NIC to a firewalld zone, using rich rules and service filtering. The guide covers requirements, design, trust boundaries, checks, failure modes, and redesign triggers.
16 Jul 2026, 06:29 UTC

Problem Statement
You have a three‑tier application (web, app, database) on CentOS Stream 9 and want to limit exposure to the Internet while keeping internal traffic fast and secure. Traditional iptables scripting is error‑prone and hard to audit. Firewalld zones and rich rules provide a declarative, service‑aware approach that scales with the stack and integrates cleanly with systemd services.
Requirements
- CentOS Stream 9 with firewalld enabled (default).
- Three network interfaces or IP ranges:
eth0(DMZ),eth1(internal),eth2(bastion). - Services: HTTP/HTTPS on web tier, SSH on bastion, PostgreSQL on database tier, optional internal API on app tier.
- No legacy
iptablesscripts running concurrently. - IPv4 and IPv6 support; verify IPv6 handling separately if using older CentOS Stream 9 builds.
Smallest Effective Design
The goal is to expose only the necessary ports between defined trust zones and keep all other traffic dropped by default. The design uses:
- One zone per tier:
dmz,internal,bastion. - Default drop policy for all zones.
- Rich rules that reference
servicenames (e.g.,http,ssh) instead of hard‑coded ports. - Service definitions tied to systemd units to stay up‑to‑date automatically.
Zone Files
Firewalld stores zone definitions in /etc/firewalld/zones/. Below is a minimal dmz.xml that allows HTTP/HTTPS from the internal zone and blocks everything else.
<?xml version=\"1.0\"?>
<zone name=\"dmz\">
<short>DMZ</short>
<description>Public facing web tier</description>
<forward>yes</forward>
<rich-rule priority=\"1\">
rule family=\"ipv4\" source zone=\"internal\" service name=\"http\" accept
</rich-rule>
<rich-rule priority=\"2\">
rule family=\"ipv4\" source zone=\"internal\" service name=\"https\" accept
</rich-rule>
<forward-ports></forward-ports>
<masquerade></masquerade>
</zone>
Repeat similar logic for internal.xml and bastion.xml, adjusting services and source zones accordingly.
Assign Interfaces to Zones
# Run as root or with sudo
firewall-cmd --zone=dmz --change-interface=eth0
firewall-cmd --zone=internal --change-interface=eth1
firewall-cmd --zone=bastion --change-interface=eth2
# Persist across reboots
firewall-cmd --permanent --zone=dmz --change-interface=eth0
firewall-cmd --permanent --zone=internal --change-interface=eth1
firewall-cmd --permanent --zone=bastion --change-interface=eth2
# Reload to apply
firewall-cmd --reload
Verify with:
firewall-cmd --get-active-zones
firewall-cmd --zone=dmz --list-all
Trust / Data Boundaries
- DMZ – only accepts inbound HTTP/HTTPS from
internaland the Internet (default drop for other traffic). - Internal – hosts application services; can initiate outbound connections to DMZ and database tier.
- Bastion – restricted SSH access, no other inbound traffic.
Operational Checks
- Firewalld State
firewall-cmd --state # should return "running" - Zone Assignments
firewall-cmd --get-active-zones firewall-cmd --zone=dmz --list-interfaces - Active Rules
firewall-cmd --zone=dmz --list-rich-rules firewall-cmd --zone=internal --list-rich-rules - Service Availability Test
# From a host in internal zone curl -I http:// # From bastion ssh user@ # From DMZ (if allowed) nc -zv 5432 - Audit Log – check
/var/log/firewalldfor rule application events.
Failure Modes & Redesign Triggers
- Firewalld Inactive – If
firewall-cmd --statereturns "not running", the system falls back to the default permissive iptables state. Redesign: enable firewalld at boot or enforce a mandatory firewall policy via systemd. - Zone Overlap – An interface inadvertently assigned to multiple zones will cause unpredictable traffic. Check with
firewall-cmd --get-active-zones. Redesign: enforce strict interface‑to‑zone mapping vianmclior network scripts. - Service Definition Changes – Updating a service package that changes port numbers can silently break connectivity. Redesign: pin service versions or add explicit port rules if the service changes.
- IPv6 Mismatch – Rich rules may not support IPv6 in older CentOS Stream 9 builds. Test IPv6 connectivity separately; if failures occur, add IPv6 rich rules or switch to manual iptables for IPv6.
- Performance Degradation – If traffic between tiers spikes, the default drop policy may cause high latency due to rule evaluation. Redesign: move critical paths to a dedicated high‑performance zone or add stateful rules.
Practical Verification Checklist
| Check | Command | Expected Result |
|---|---|---|
| Firewalld Running | firewall-cmd --state | running |
| Interface Zone | firewall-cmd --zone=dmz --list-interfaces | eth0 |
| Allowed Service | firewall-cmd --zone=dmz --list-services | http, https |
| Blocked Port | nc -zv 22 | Connection refused / timed out |
| SSH from Bastion | ssh user@ | Successful login |
Conclusion
By binding each tier to its own firewalld zone, using service‑based rich rules, and keeping the default policy to drop, you obtain a clear, auditable network segmentation that scales with service changes. Regular operational checks and a simple verification checklist help catch misconfigurations early, while the defined failure modes guide when to revisit the architecture. This minimal design keeps the attack surface small, reduces configuration drift, and leverages CentOS Stream 9’s native firewall tooling.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.