The management of network security policies is a critical pillar of infrastructure stability and system integrity. In the ecosystem of Red Hat Enterprise Linux (RHEL), CentOS, Fedora, and their various derivatives, firewalld serves as the primary firewall management tool. Unlike the simplified allow/deny model found in tools such as Uncomplicated Firewall (UFW), firewalld employs a sophisticated zone-based architecture. In this paradigm, network interfaces are assigned to specific zones, and each zone maintains its own distinct set of allowed services and ports. This architecture is specifically engineered for servers featuring multiple network interfaces, as it allows administrators to apply disparate security policies to different network segments—such as a trusted internal LAN and an untrusted external WAN—simultaneously.
When managing these configurations at scale, the complexity of zone definitions and the intricacy of rich rules can quickly become overwhelming for human operators. This is where the integration of Ansible becomes indispensable. By defining firewall configurations as code, organizations transition from manual, error-prone command execution to a declarative model. This shift ensures that every server within a specific group maintains an identical security posture, allows for rigorous peer review of security changes via version control, and enables rapid rollbacks in the event of a catastrophic misconfiguration. The transition to "Infrastructure as Code" for firewall management mitigates the risk of "configuration drift," where individual servers deviate from the intended security baseline over time.
The Architecture of firewalld and the Ansible Integration Layer
To understand how Ansible interacts with firewalld, one must first grasp the underlying logic of the firewall tool itself. firewalld operates on the concept of zones, which act as containers for rules. An interface assigned to the public zone will be subject to different rules than an interface assigned to the internal or trusted zones.
The ansible.posix.firewalld module provides the primary interface for controlling these settings. It allows administrators to manage services, ports, rich rules, and interface assignments. A critical aspect of this module is the distinction between permanent and immediate changes. By default, firewalld has a runtime configuration and a permanent configuration. A change marked as permanent: true is written to the disk but does not take effect until the firewall is reloaded. A change marked as immediate: true takes effect instantly but will be lost upon reboot unless also marked as permanent. For a robust deployment, Ansible playbooks typically set both to true to ensure the rule is active now and persists across system restarts.
Strategic Implementation of Zone Management
The assignment of network interfaces to zones is the first step in securing a multi-homed server. This process defines the trust level of the incoming traffic based on the physical or virtual hardware interface being used.
The following implementation strategy demonstrates how to associate specific interfaces with the external and internal zones while ensuring the service is active:
```yaml
- name: Install FirewallD
ansible.builtin.dnf:
name: firewalld
state: present
name: Enable and start FirewallD
ansible.builtin.service:
name: firewalld
enabled: true
state: startedname: Associate external zone to WAN network interface
ansible.posix.firewalld:
zone: external
interface: "{{interface_wan}}"
state: enabled
permanent: true
immediate: truename: Associate internal zone to LAN network interface
ansible.posix.firewalld:
zone: internal
interface: "{{interface_lan}}"
state: enabled
permanent: true
immediate: true
```
In this technical workflow, the ansible.builtin.assert module should be used prior to these tasks to verify that interface_wan and interface_lan are defined in the host variables. Failure to define these would result in a playbook failure, preventing the deployment of an incomplete security policy that could leave a server exposed.
Advanced Port and Service Configuration
Beyond basic zone assignment, firewalld requires the explicit enabling of services or ports to allow traffic. A service in firewalld is essentially a named group of ports and protocols (e.g., ssh typically refers to TCP port 22).
Managing Default Services and Custom Ports
A common security hardening practice is to remove predefined services that are not required for the specific role of the server. For instance, in an internal zone, services like cockpit, dhcpv6-client, mdns, and samba-client may be unnecessary and should be disabled to reduce the attack surface.
The following logic describes how to prune unwanted services while explicitly allowing essential ones:
```yaml
- name: Remove all predefined services except SSH from internal zone
ansible.posix.firewalld:
zone: internal
service: "{{item}}"
state: disabled
permanent: true
immediate: true
loop:
- cockpit
- dhcpv6-client
- mdns
- samba-client
- name: Allow DNS & DHCP
ansible.posix.firewalld:
zone: internal
service: "{{item}}"
state: enabled
permanent: true
immediate: true
loop:- dns
- dhcp
```
Handling Port Forwarding and Custom Requirements
For more complex scenarios, such as port forwarding, Ansible allows the use of loops to process a list of forwarding rules. This is particularly useful for gateway servers or load balancers.
The implementation for port forwarding often utilizes the following structure:
yaml
- name: Configure port forwards
ansible.posix.firewalld:
zone: "{{ item.zone | default(firewalld_default_zone) }}"
permanent: yes
immediate: yes
state: enabled
loop: "{{ firewalld_port_forwards | default([]) }}"
This approach allows the user to define a list of forwards in a group variable file, ensuring that the zone can either be specified per item or fall back to a global default.
Specialized Deployment Strategies
The "Offline" Mode Role Approach
Some advanced Ansible roles, such as those found in certain community implementations, prioritize an "offline" mode of configuration. The goal of this strategy is to ensure that the firewall is configured entirely before the service is brought online or during the initial boot sequence. This prevents a scenario where the firewall starts and immediately blocks all traffic, including the SSH connection used by Ansible to manage the server.
This methodology involves:
- Creating custom firewalld services with specific ports.
- Removing unwanted default services from public or internal zones.
- Adding IP ranges to specific zones to define trust boundaries.
- Using a specific variable state (e.g., disabled) to remove a service, rather than simply deleting the configuration line, to ensure the Ansible state is explicitly tracked.
One critical caveat for this approach is the interaction with Docker. It is noted that Docker and firewalld often conflict because both attempt to manipulate iptables rules. This can lead to unpredictable routing behavior or the bypassing of firewalld rules by Docker containers.
Dynamic Port Discovery and Hardening
A significant challenge in automated firewall deployment is the "blind spot" where a system administrator is unsure which ports are actually listening on a server before enabling firewalld. Enabling the firewall without knowing the active listening ports can lead to immediate application outages.
To solve this, a specialized workflow using the community.general.listen_ports_facts module can be implemented. This module leverages tools like netstat (from the net-tools package) or the newer ss command to identify all open TCP and UDP ports.
The technical process for dynamic port enabling follows these steps:
- Installation of the
net-toolspackage to ensure thessornetstatcommands are available. - Execution of the
listen_ports_factsmodule to gather a list of all ports currently in aLISTENstate. - Iterating through the discovered ports and using the
ansible.posix.firewalldmodule to add those specific ports to the firewall. - Logging the added ports for audit purposes.
Modularizing Firewall Rules for Multi-Team Environments
In large-scale enterprise environments, a single monolithic firewall configuration is often impractical. Different teams (e.g., Database team, Web team, Security team) may need to manage their own rules. This can be achieved by utilizing a modular role-based dependency system.
By defining a dictionary of rules, such as app_firewall_rules, an application-specific role can pass its requirements to a general firewalld_rules role.
Example of a modular rule dictionary:
yaml
app_firewall_rules:
inbound:
- name: app_inbound
zone:
- local
ports:
- port: 80
protocol: tcp
- port: 443
protocol: tcp
outbound:
- name: app_smtp
protocol: tcp
ports:
- 25
hosts:
- 10.10.1.10
- 10.10.1.11
This data is then passed via a dependency:
yaml
dependencies:
- { role: firewalld_rules, firewalld_rules: "{{ app_firewall_rules }}" }
This architecture allows the security team to maintain the core firewalld_rules role while application teams only need to modify a simple YAML list of ports and zones, ensuring a separation of concerns and reducing the risk of accidental global misconfigurations.
Verification and Validation Framework
A critical component of any infrastructure deployment is the verification phase. Relying on the "changed" status of an Ansible task is insufficient for security auditing. A dedicated verification playbook should be used to query the actual state of the firewall on the target host.
The following tasks are essential for a comprehensive verification routine:
- Checking the current state of the firewall using
firewall-cmd --state. - Identifying which zones are currently active via
firewall-cmd --get-active-zones. - Listing all configurations for each defined zone using
firewall-cmd --zone=[zone_name] --list-all. - Specifically auditing rich rules using
firewall-cmd --zone=[zone_name] --list-rich-rules.
Example verification implementation:
```yaml
- name: Verify firewalld configuration
hosts: all
become: yes
tasks:
- name: Check firewalld is running
ansible.builtin.command:
cmd: firewall-cmd --state
register: fwstate
changedwhen: false
- name: Get active zones
ansible.builtin.command:
cmd: firewall-cmd --get-active-zones
register: active_zones
changed_when: false
- name: Display active zones
ansible.builtin.debug:
msg: "{{ active_zones.stdout_lines }}"
- name: Get all zone details
ansible.builtin.command:
cmd: "firewall-cmd --zone={{ item.name }} --list-all"
register: zone_details
loop: "{{ firewalld_zones }}"
changed_when: false
- name: Display zone configurations
ansible.builtin.debug:
msg: "{{ item.stdout_lines }}"
loop: "{{ zone_details.results }}"
```
Technical Summary of firewalld Components
The following table provides a technical breakdown of the components managed via Ansible and their functions within the firewalld ecosystem.
| Component | Ansible Module/Tool | Purpose | Scope |
|---|---|---|---|
| Zone Assignment | ansible.posix.firewalld |
Links network interfaces to a trust level | Interface/Zone |
| Service Control | ansible.posix.firewalld |
Enables/Disables predefined port groups | Zone |
| Port Management | ansible.posix.firewalld |
Opens specific TCP/UDP ports | Zone |
| Port Forwarding | ansible.posix.firewalld |
Redirects traffic from one port to another | Global/Zone |
| Masquerading | ansible.posix.firewalld |
Enables NAT for internal networks | Zone |
| Rich Rules | ansible.posix.firewalld |
Complex filtering (source IP, etc.) | Zone |
| State Fact | community.general.listen_ports_facts |
Identifies open ports on the OS | System |
Operational Handlers and Service Management
To ensure that changes are applied correctly, Ansible handlers should be utilized to reload or restart the firewalld service. This prevents the service from being restarted unnecessarily on every task execution, which could lead to temporary network interruptions.
The standard handlers for firewalld are defined as follows:
```yaml
# roles/firewalld/handlers/main.yml
name: Reload firewalld
ansible.builtin.command:
cmd: firewall-cmd --reloadname: Restart firewalld
ansible.builtin.service:
name: firewalld
state: restarted
```
The reload command is preferred over restart because it applies the permanent configuration to the runtime environment without dropping existing connections.
Conclusion
The automation of firewalld via Ansible transforms network security from a manual administrative burden into a scalable, auditable, and reliable process. By leveraging a zone-based architecture, administrators can create granular security boundaries that distinguish between external threats and internal trusted traffic. The use of the ansible.posix.firewalld module, combined with strategic a-priori port discovery via community.general.listen_ports_facts, ensures that security hardening does not result in operational downtime. Furthermore, the adoption of a modular, dictionary-based rule system allows large organizations to delegate firewall management to application teams while maintaining centralized control and oversight. Ultimately, treating firewall rules as code allows for the implementation of a "Security as Code" philosophy, where every open port and allowed service is documented in version control, tested in non-production environments, and deployed consistently across the entire infrastructure.