FirewallD remains the recommended dynamic packet filtering frontend in CentOS Stream 9 and 10. It provides zone-based policy application, rich rule expressiveness, runtime changes without service interruption, and clean integration with nftables (the underlying backend since RHEL 8 / CentOS Stream 8).
Below is a structured progression from beginner essentials to advanced production patterns used in real environments in 2026.
Core Design Principles You Should Internalize
- Zones are trust boundaries assigned to interfaces (or sources). Most production internet-facing servers should use drop or public as the active/default zone — never trusted unless it’s an internal-only management interface.
- Runtime vs permanent changes: –permanent writes to /etc/firewalld/zones/. Runtime changes are immediate but lost on reload/reboot.
- Evaluation order: rich rules → services → ports → source-based rules → interface defaults → zone policy (accept/reject/drop).
- SELinux synergy: FirewallD decisions happen before SELinux AVC checks — both must allow traffic for successful connection.
Beginner: Getting Started Safely
Confirm active state and default zone Most fresh installs default to public with ssh allowed.
Minimal production baseline (internet-facing server)
- Set default zone to drop
- Explicitly allow only ssh + https (or http during setup)
- Restrict ssh to known source IPs or VPN ranges
This single decision eliminates ~95% of random internet noise.
Intermediate: Service-Centric Configuration (Recommended Default)
Always prefer –add-service over –add-port when a predefined service exists:
- Recognized services include: cockpit, docker, freeipa-ldap, http, https, imap, ldap, mdns, mysql, nfs, openvpn, pop3, postgresql, prometheus-node-exporter, samba, smtp, ssh, vnc-server, and many more.
- Service definitions live in /usr/lib/firewalld/services/ and can be overridden/customized in /etc/firewalld/services/.
Example pattern for a typical LAMP + admin server:
- Allow https (port 443/tcp) from anywhere
- Allow http (80/tcp) only from monitoring IPs or during initial setup
- Allow ssh only from management subnet
- Allow cockpit (9090/tcp) only from admin VPN
Advanced: Rich Rules – The Real Power
Rich rules give you conditional, ordered, logging-aware, rate-limited, and forwarding logic.
Common production patterns in 2026:
- Rate-limited administrative access Protect SSH / cockpit from brute-force even before fail2ban/crowdsec engages.
- Time-windowed maintenance ports Temporarily open rsync/nfs/sftp ports during scheduled backup windows (e.g., 02:00–04:00 daily).
- Geoip-like restrictions via source prefixes Combine with external threat feeds or known bad ASN ranges (requires script to update rich rules periodically).
- Port forwarding / DNAT Forward external 443 → internal reverse-proxy at non-standard port, or expose containerized services safely.
- Logging with classification Log dropped packets with meaningful prefixes for SIEM correlation (e.g., “possible-scanner-”, “brute-ssh-”).
- Masquerading + port-forward for edge gateways Enable NAT + forward specific external ports to internal VMs/containers.
Hardening & Observability Best Practices (2026)
- Default zone = drop for any publicly routed interface
- Log denied packets selectively (–set-log-denied=unicast is usually sufficient; all creates noise)
- Avoid direct nft/iptables manipulation unless using –direct interface (breaks firewalld state tracking)
- Backup configuration before bulk changes: tar czf firewalld-backup-$(date +%F).tar.gz /etc/firewalld
- Runtime safety pattern
- Apply change runtime-only
- Verify from another session
- Make permanent
- Reload
- Monitoring integration Forward firewalld journal logs to central system (rsyslog → Graylog / Loki / ELK). Look for REJECT/DROP lines with consistent prefixes from rich rules.
Quick Decision Framework for New Servers
| Scenario | Recommended Zone | Must-Allow Services / Rules | Additional Controls |
|---|---|---|---|
| Internet web server (nginx/apache) | public or drop | https, http (optional) | Source restrict admin ports |
| Database server (mysql/postgres) | drop | 3306/5432 only from app subnet | Rich rule + rate limit if public |
| Container host (docker/podman) | public | Published container ports via –publish | Consider nftables passthrough if needed |
| Management / bastion host | public | ssh (source-restricted), cockpit (VPN only) | Fail2ban/crowdsec + geo-blocking script |
| Internal app server (no public IP) | internal | http/https, app-specific ports | Minimal external exposure |
Validation & Troubleshooting Commands Worth Memorizing
- –list-all-zones — overview of everything
- –get-active-zones — which interfaces are where
- –get-zone-of-interface=eth0
- –query-service=https — is it allowed?
- –query-rich-rule=’rule …’
- journalctl -u firewalld -f — live denied/log traffic
- firewall-cmd –direct –get-all-rules — low-level nft/iptables view (debug only)
Mastering zones + rich rules covers virtually every realistic firewall requirement without dropping to raw nftables syntax. For Kubernetes clusters, edge gateways, or zero-trust micro-segmentation, you may eventually layer Calico, Cilium, or eBPF-based solutions on top — but firewalld remains the reliable host-level foundation.