Introduction

Managing firewall rules is essential for server security. The ansible.posix.firewalld module provides full control over firewalld on RedHat-family systems — opening ports, enabling services, configuring zones, and applying rich rules, all idempotently.

This guide covers everything from basic port opening to advanced zone-based firewall configurations with Ansible.

The ansible.posix.firewalld Module

The full module name is ansible.posix.firewalld, part of the ansible.posix collection for POSIX platforms. It requires:

  • Ansible 2.9+
  • firewalld >= 0.2.11 on the target
  • Python firewall bindings on the target
  • Works on: RHEL, CentOS, CentOS Stream, Fedora, Rocky Linux, AlmaLinux, Oracle Linux, EuroLinux

Install the Collection

ansible-galaxy collection install ansible.posix

Parameters Reference

ParameterTypeRequiredDefaultDescription
statestringYes—enabled, disabled, present, absent
servicestringNo—Service name (e.g., http, https)
portstringNo—Port/protocol (e.g., 8080/tcp)
permanentbooleanNofalsePersist across reboots
immediatebooleanNofalse*Apply to running config now
zonestringNosystem defaultTarget zone
rich_rulestringNo—Rich rule string
sourcestringNo—Source IP/network
interfacestringNo—Network interface
masqueradestringNo—Enable masquerading
icmp_blockstringNo—ICMP type to block
icmp_block_inversionstringNo—Invert ICMP block
offlinebooleanNofalseWork with firewalld offline
targetstringNo—Zone target (default, ACCEPT, DROP, REJECT)

*If permanent: false, immediate is assumed true.

Important: To make a change both permanent AND immediately active, set BOTH permanent: true and immediate: true.

Basic Examples

Open HTTP and HTTPS

---
- name: Open web ports
  hosts: webservers
  become: true
  tasks:
    - name: Enable HTTP and HTTPS
      ansible.posix.firewalld:
        service: "{{ item }}"
        state: enabled
        permanent: true
        immediate: true
      loop:
        - http
        - https

Open a Custom Port

---
- name: Open custom ports
  hosts: appservers
  become: true
  tasks:
    - name: Open application port 8080/tcp
      ansible.posix.firewalld:
        port: 8080/tcp
        state: enabled
        permanent: true
        immediate: true

    - name: Open port range 9000-9100/tcp
      ansible.posix.firewalld:
        port: 9000-9100/tcp
        state: enabled
        permanent: true
        immediate: true

    - name: Open UDP port for DNS
      ansible.posix.firewalld:
        port: 53/udp
        state: enabled
        permanent: true
        immediate: true

Close a Port

---
- name: Close unused ports
  hosts: all
  become: true
  tasks:
    - name: Disable telnet service
      ansible.posix.firewalld:
        service: telnet
        state: disabled
        permanent: true
        immediate: true

    - name: Close port 3306 (MySQL not needed externally)
      ansible.posix.firewalld:
        port: 3306/tcp
        state: disabled
        permanent: true
        immediate: true

Advanced Patterns

Zone Configuration

---
- name: Configure firewall zones
  hosts: dmzservers
  become: true
  tasks:
    - name: Move interface to DMZ zone
      ansible.posix.firewalld:
        zone: dmz
        interface: eth0
        state: enabled
        permanent: true
        immediate: true

    - name: Allow HTTP in DMZ zone only
      ansible.posix.firewalld:
        zone: dmz
        service: http
        state: enabled
        permanent: true
        immediate: true

    - name: Block all traffic in drop zone for interface eth1
      ansible.posix.firewalld:
        zone: drop
        interface: eth1
        state: enabled
        permanent: true
        immediate: true

Rich Rules (Fine-Grained Control)

---
- name: Apply rich rules
  hosts: all
  become: true
  tasks:
    - name: Allow SSH only from specific subnet
      ansible.posix.firewalld:
        rich_rule: 'rule family="ipv4" source address="10.0.0.0/24" service name="ssh" accept'
        state: enabled
        permanent: true
        immediate: true

    - name: Rate-limit SSH connections
      ansible.posix.firewalld:
        rich_rule: 'rule service name="ssh" accept limit value="3/m"'
        state: enabled
        permanent: true
        immediate: true

    - name: Log and drop traffic from malicious IP
      ansible.posix.firewalld:
        rich_rule: 'rule family="ipv4" source address="192.168.1.100" log prefix="BLOCKED: " level="warning" drop'
        state: enabled
        permanent: true
        immediate: true

    - name: Forward port 80 to 8080
      ansible.posix.firewalld:
        rich_rule: 'rule family="ipv4" forward-port port="80" protocol="tcp" to-port="8080"'
        state: enabled
        permanent: true
        immediate: true

Source-Based Rules

---
- name: Source-based firewall rules
  hosts: dbservers
  become: true
  tasks:
    - name: Add trusted source to internal zone
      ansible.posix.firewalld:
        zone: internal
        source: 10.0.1.0/24
        state: enabled
        permanent: true
        immediate: true

    - name: Allow PostgreSQL from app subnet only
      ansible.posix.firewalld:
        rich_rule: 'rule family="ipv4" source address="10.0.2.0/24" port port="5432" protocol="tcp" accept'
        state: enabled
        permanent: true
        immediate: true

Enable Masquerading (NAT)

---
- name: Configure NAT gateway
  hosts: gateways
  become: true
  tasks:
    - name: Enable masquerading on external zone
      ansible.posix.firewalld:
        masquerade: "true"
        zone: external
        state: enabled
        permanent: true
        immediate: true

Complete Playbook: Web Server Firewall Setup

---
- name: Configure firewall for web server
  hosts: webservers
  become: true
  vars:
    allowed_services:
      - http
      - https
      - ssh
    custom_ports:
      - 8443/tcp    # Alternative HTTPS
      - 9090/tcp    # Prometheus metrics
    admin_subnet: "10.0.0.0/24"
    monitoring_subnet: "10.0.10.0/24"
  tasks:
    - name: Ensure firewalld is installed and running
      block:
        - name: Install firewalld
          ansible.builtin.yum:
            name: firewalld
            state: present

        - name: Start and enable firewalld
          ansible.builtin.systemd:
            name: firewalld
            state: started
            enabled: true

    - name: Enable allowed services
      ansible.posix.firewalld:
        service: "{{ item }}"
        state: enabled
        permanent: true
        immediate: true
      loop: "{{ allowed_services }}"

    - name: Open custom ports
      ansible.posix.firewalld:
        port: "{{ item }}"
        state: enabled
        permanent: true
        immediate: true
      loop: "{{ custom_ports }}"

    - name: Restrict SSH to admin subnet
      ansible.posix.firewalld:
        rich_rule: 'rule family="ipv4" source address="{{ admin_subnet }}" service name="ssh" accept'
        state: enabled
        permanent: true
        immediate: true

    - name: Allow monitoring from monitoring subnet
      ansible.posix.firewalld:
        rich_rule: 'rule family="ipv4" source address="{{ monitoring_subnet }}" port port="9090" protocol="tcp" accept'
        state: enabled
        permanent: true
        immediate: true

    - name: Remove default SSH access (use rich rule instead)
      ansible.posix.firewalld:
        service: ssh
        state: disabled
        permanent: true
        immediate: true

    - name: Verify firewall configuration
      ansible.builtin.command: firewall-cmd --list-all
      register: fw_config
      changed_when: false

    - name: Display firewall status
      ansible.builtin.debug:
        var: fw_config.stdout_lines

Available Services

List all predefined services on a target:

firewall-cmd --get-services

Common services: ssh, http, https, dns, ntp, smtp, ftp, mysql, postgresql, redis, cockpit, kube-apiserver, prometheus, grafana

Common Mistakes

Mistake 1: Permanent but Not Immediate

# Rule saved to disk but NOT active until firewalld restart
- ansible.posix.firewalld:
    service: http
    state: enabled
    permanent: true
    # Missing: immediate: true

Mistake 2: Immediate but Not Permanent

# Rule active NOW but lost on reboot
- ansible.posix.firewalld:
    service: http
    state: enabled
    permanent: false
    immediate: true

Correct: Both

- ansible.posix.firewalld:
    service: http
    state: enabled
    permanent: true
    immediate: true

Troubleshooting

Module Not Found

ERROR! couldn't resolve module/action 'ansible.posix.firewalld'

Fix: Install the collection:

ansible-galaxy collection install ansible.posix

"firewalld is not running"

Fix: Start the service first in your playbook (see complete example above).

Changes Not Persisting

Ensure permanent: true is set. Check with:

firewall-cmd --permanent --list-all

Conclusion

The ansible.posix.firewalld module provides complete control over firewalld on RedHat-family systems. Always use both permanent: true and immediate: true to ensure rules are active immediately AND survive reboots. Use services for standard ports, port for custom ports, rich rules for fine-grained source/destination control, and zones for interface-level segmentation.