Enforcing Host Firewall Policy Across a RHEL Fleet with the Firewall System Role
Use the Red Hat firewall system role to define host packet filtering once in Ansible and enforce it consistently across RHEL 8/9 fleets. Covers minimal design, trust boundaries, and failure modes.
30 Aug 2026, 17:13 UTC

The Problem: Drift and Inconsistency at Scale
Managing host-level packet filtering on dozens or hundreds of Red Hat Enterprise Linux (RHEL) systems by hand leads to configuration drift, audit gaps, and the risk of administrator lockout when a manual firewall-cmd change accidentally removes SSH access. The Red Hat firewall system role (redhat.rhel_system_roles.firewall) provides an Ansible-native, supported interface to firewalld (the default RHEL firewall manager), allowing you to define policy once and apply it consistently across RHEL 8 and 9 fleets.
Requirements
- Consistent, auditable host firewall policy on RHEL 8/9 without per-host manual edits.
- Ansible control node with SSH access to managed hosts using key-based authentication.
firewalldinstalled and running on every target host.rhel-system-rolespackage (or the equivalent Ansible collection) available on the control node.- Inventory grouping that reflects security zones (e.g.,
webservers,databases,bastions).
Smallest Suitable Design
The minimal design consists of a single playbook, one inventory group, and the firewall role. This avoids unnecessary middleware and relies on the native system role provided by Red Hat.
# playbooks/firewall-baseline.yml
---
- name: Apply baseline host firewall policy
hosts: all
become: true
vars:
firewall:
- service: ssh
state: enabled
permanent: true
zone: public
- port: 8080/tcp
state: enabled
permanent: true
zone: public
- service: cockpit
state: disabled
permanent: true
zone: public
roles:
- redhat.rhel_system_roles.firewall
Run the following command from the control node to preview changes:
ansible-playbook -i inventory/prod.yml playbooks/firewall-baseline.yml --check --diff
The --check --diff flags allow you to see exactly which rules will be added or removed before they are applied to the production environment.
Trust and Data Boundaries
| Boundary | Description | Risk if Compromised |
|---|---|---|
| Ansible control node | Holds inventory, playbooks, and SSH private keys | Full fleet firewall control; ability to open/close any port |
| Managed node SSH trust | Each host accepts the controller's public key | Potential for lateral movement if the control node is breached |
| Firewall policy data | Port/service lists defined in playbooks or vars files | Not secret, but misconfiguration can expose sensitive services |
| firewalld runtime | Local daemon enforcing rules on the host | Manual firewall-cmd edits create drift from the source of truth |
To mitigate these risks, restrict control node access via a bastion host with MFA, rotate SSH keys regularly, and treat playbook repositories as privileged infrastructure code requiring peer review and CI linting.
Operational Checks
- Pre-apply validation: Use
ansible-playbook --check --diffto verify the intended state against the current state. - Post-apply verification: On a managed host, run the following commands to confirm the policy is active:
Expect to seefirewall-cmd --list-all firewall-cmd --get-active-zonessshand8080/tcpenabled under thepubliczone, withcockpitabsent. - Log monitoring: Monitor
journalctl -u firewalldforREJECTentries or failed reload events. - Drift detection: Schedule a daily
--checkrun against a canary host; any reported changes indicate manual intervention or unauthorized drift.
Failure Modes
| Failure | Symptom | Root Cause | Mitigation |
|---|---|---|---|
| SSH lockout | Controller cannot reconnect to host | ssh service omitted or disabled in vars | Always explicitly declare service: ssh, state: enabled |
| Role version mismatch | Task fails with "unsupported parameter" | Collection version differs from RHEL release | Pin collection version in requirements.yml |
| Missing firewalld | "firewalld service not found" error | Minimal install omitted the package | Add pre-task to ensure firewalld is installed and started |
| Manual drift | Rules differ from playbook | Admin ran firewall-cmd --permanent locally | Enforce a "no manual changes" policy |
Conditions That Change the Design
- Per-host exceptions: If hosts require unique rules, move from static variables to data-driven inventory (
host_vars/group_vars) or use Jinja2 templates to merge baseline and exception lists. - Container workloads: For OpenShift or Kubernetes, shift enforcement to NetworkPolicies; the host firewall should then only manage node-level services like
kubeletandSSH. - RHEL Image Mode: In immutable deployments (Bootc), runtime
firewalldmanagement is replaced by baking the policy into the container image at build time. - Multi-zone requirements: If hosts require distinct zones per interface (e.g., DMZ vs. Internal), extend the role variables with
firewall_zonesandfirewall_interfacemappings.
Verification Checklist
- Run
ansible-playbook --check --diff; confirm only expected changes appear. - Execute
firewall-cmd --list-allon a target host to verify declared services. - Verify
changed=0on subsequent runs to ensure idempotence. - Check
rpm -q rhel-system-roleson the control node to ensure the version matches the target RHEL minor release. - Confirm service status:
systemctl is-active firewalldshould returnactive.
Limitations
- The role manages
firewalldspecifically; it does not configurenftablesoriptablesdirectly. - There is no built-in automated rollback; version control (Git) for playbooks is the primary recovery mechanism.
- Requires a RHEL subscription for supported collection access.
- IPv6 rules follow standard service/port declarations; explicit IPv6-only rules require
firewall-cmd --direct, which the role does not abstract.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.