Manage firewalld and SELinux on Rocky Linux with Ansible

Use Ansible on Rocky Linux 10 to open firewalld services and ports with runtime and permanent rules, set SELinux mode and booleans, add file contexts with sefcontext, apply labels with restorecon, and verify both layers safely.

Published

Updated

Read time 14 min read

Reviewed byDeepak Prasad

Manage firewalld and SELinux host security with Ansible on Rocky Linux 10

Rocky Linux host security has two practical layers Ansible can automate: firewalld decides which network services and ports are reachable, and SELinux decides whether processes may access files and sockets even when the firewall allows traffic. This guide walks through both with idempotent modules—open HTTP and HTTPS, add a custom TCP port, choose runtime versus permanent firewall rules, set SELinux mode, flip booleans, label a custom web directory, run restorecon, and verify the result on the host.

You will use ansible.posix.firewalld, ansible.posix.selinux, ansible.posix.seboolean, and community.general.sefcontext. For persistent file-context troubleshooting beyond the basics here, see SELinux file contexts with Ansible.

Tested on: Rocky Linux 10.2 (Red Quartz); kernel 6.12.0-211.16.1.el10_2.0.1.x86_64; ansible-core 2.16.16; ansible.posix 2.1.0; community.general 9.5.2.

IMPORTANT
This article covers firewalld service and port rules, runtime versus permanent firewall changes, SELinux mode and booleans, and basic file contexts with restorecon on Rocky Linux hosts. It does not cover full firewalld zone design, rich rules, deep SELinux policy theory, AVC log analysis, or CIS hardening benchmarks.
NOTE
This chapter is part of the GoLinuxCloud Ansible tutorial (RHCE EX294). Follow along from ~/ansible-project, inventory group lab, and playbooks in playbooks/. Use your own host names and paths if yours differ.
Task Module
Open/close firewall service ansible.posix.firewalld
Open/close firewall port ansible.posix.firewalld
Make firewall rule persistent ansible.posix.firewalld with permanent: true
Apply firewall rule immediately ansible.posix.firewalld with immediate: true
Set SELinux mode ansible.posix.selinux
Manage SELinux boolean ansible.posix.seboolean
Add persistent file context rule community.general.sefcontext
Apply file context label ansible.builtin.command: restorecon
Verify firewall firewall-cmd --list-all
Verify SELinux getenforce, sestatus, ls -Z

Use these FQCN module names in playbooks and on the exam—short names such as firewalld: or selinux: are ambiguous and may resolve to the wrong collection:

Module FQCN
Firewall rules ansible.posix.firewalld
SELinux mode ansible.posix.selinux
SELinux booleans ansible.posix.seboolean
File context mapping community.general.sefcontext

What This Article Covers

  • ansible.posix.firewalld for services, ports, zones, and runtime versus permanent rules
  • ansible.posix.selinux for enforcing, permissive, and disabled mode
  • ansible.posix.seboolean for persistent SELinux booleans
  • community.general.sefcontext plus restorecon for custom paths such as /srv/www/html
  • Verification with firewall-cmd, getenforce, sestatus, getsebool, ls -Z, and semanage fcontext -l

Not covered: complex zone topology, direct/rich firewall rules, full boolean reference, or deep AVC troubleshooting.


firewalld vs SELinux: What Ansible Manages

Layer What it controls Ansible modules
firewalld Incoming/outgoing network access by zone, service name, or port ansible.posix.firewalld, ansible.builtin.service
SELinux Process and file access labels, host-wide mode, policy booleans ansible.posix.selinux, ansible.posix.seboolean, community.general.sefcontext, restorecon

A common mistake is opening port 80 in firewalld while Apache still cannot serve files because SELinux labels or booleans block access. Custom document roots on dedicated volumes need LVM and persistent mounts before you tune firewall and SELinux for that path. Automate and verify both layers in the same playbook when you deploy a network-facing service.


Prerequisites on Rocky Linux

From the control node project directory:

bash
cd ~/ansible-project

Confirm the managed host answers Ansible:

bash
ansible -i inventory/hosts lab -m ansible.builtin.ping

Sample output:

output
rocky2 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

A pong reply means SSH and Python on the managed host are ready for modules that need become.

Check ansible-core on the control node:

bash
ansible --version

Sample output:

output
ansible [core 2.16.16]
  config file = /home/ansible/ansible-project/ansible.cfg

Plays that change firewalld or SELinux need root on the managed host. Set become: true on the play or rely on become = True in ansible.cfg if your lab already sets it.


Required Collections and Packages

Install collections from a pinned requirements.yml beside your playbooks:

yaml
---
collections:
  - name: ansible.posix
    version: "2.1.0"
  - name: community.general
    version: "9.5.2"
bash
ansible-galaxy collection install -r requirements.yml

List installed versions:

bash
ansible-galaxy collection list

Sample output:

output
community.general        9.5.2
ansible.posix            2.1.0

Pin versions to match your tested ansible-core—newer collection releases may require a newer core than the lab used here. If Galaxy cannot find a module, see collection and variable errors for FQCN and path fixes.

On the managed Rocky Linux host, install firewall and SELinux tooling:

bash
ansible -i inventory/hosts rocky2 -m ansible.builtin.dnf -a "name=firewalld,policycoreutils-python-utils,selinux-policy-targeted state=present" -b

Confirm packages are present:

bash
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "rpm -q firewalld policycoreutils-python-utils selinux-policy-targeted" -b

Sample output:

output
firewalld-2.4.0-1.el10_1.noarch
policycoreutils-python-utils-3.10-1.el10.noarch
selinux-policy-targeted-42.1.18-4.el10.noarch

policycoreutils-python-utils provides SELinux management tools such as semanage, while restorecon is normally provided by the core SELinux policy utilities already present on Rocky/RHEL systems. Installing the package is still useful because sefcontext relies on semanage-style file-context management.

The ansible.posix.firewalld module requires both firewalld and the Python firewalld bindings on the managed host where the module runs. Official docs list firewalld ≥ 0.9.0 and python-firewall ≥ 0.9.0. A dnf install firewalld on Rocky Linux usually pulls the needed dependency automatically, but if the module fails with a Python import error, confirm the bindings are present:

bash
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "python3 -c 'import firewall; print(\"python-firewall ok\")'" -b

Sample output:

output
python-firewall ok

A successful import means the firewalld Python package is available for Ansible to manage rules.


Manage firewalld Service State

Keep firewalld running before you add rules:

yaml
- name: Ensure firewalld is enabled and running
  ansible.builtin.service:
    name: firewalld
    state: started
    enabled: true

After the play runs, confirm the daemon is active:

bash
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "systemctl is-active firewalld" -b

Sample output:

output
active

If firewalld is stopped, ansible.posix.firewalld cannot apply runtime changes.


Open and Close firewalld Services

Service names such as http and https map to predefined port groups in firewalld. Enable a service in the default zone:

yaml
- name: Allow http service permanently and immediately
  ansible.posix.firewalld:
    service: http
    permanent: true
    immediate: true
    state: enabled

- name: Allow https service permanently and immediately
  ansible.posix.firewalld:
    service: https
    permanent: true
    immediate: true
    state: enabled

List the active zone after the play:

bash
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "firewall-cmd --list-all" -b

Sample output:

output
public (default, active)
  target: default
  ...
  services: cockpit dhcpv6-client http https ssh
  ports:
  ...

The services: line now includes http and https alongside existing entries such as ssh.

Prefer service: http or service: https when firewalld already defines a named service. Use port: 8080/tcp only for custom application ports that do not map to an existing firewalld service.

To remove a service, set state: disabled with the same permanent and immediate flags you used to add it.


Open and Close firewalld Ports

When your application listens on a port that is not a named firewalld service, open the port explicitly:

yaml
- name: Open custom TCP port 8080 permanently and immediately
  ansible.posix.firewalld:
    port: 8080/tcp
    permanent: true
    immediate: true
    state: enabled

Check open ports:

bash
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "firewall-cmd --list-ports" -b

Sample output:

output
8080/tcp

Close a port with state: disabled and the same permanent and immediate flags you used to open it. See Open a custom TCP port permanently under Common Examples.


Verify firewalld Rules

firewall-cmd --list-all is the quickest check for services, ports, and the active zone:

bash
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "firewall-cmd --list-all" -b

Sample output:

output
public (default, active)
  target: default
  ...
  services: cockpit dhcpv6-client http https ssh
  ports: 8080/tcp
  ...

Match what you expect from the playbook: service names under services: and numeric ports under ports:.

Ad-hoc command checks are fine for manual verification. In playbooks, add changed_when: false to read-only checks such as firewall-cmd --list-all, getenforce, getsebool, and ls -Z:

yaml
- name: Verify SELinux mode
  ansible.builtin.command: getenforce
  register: selinux_mode
  changed_when: false

Runtime vs Permanent firewalld Rules

firewalld keeps two rule stores: runtime (active now) and permanent (saved for reboot). The module parameters map directly to those stores.

Rule type Meaning
Runtime Active now but lost after reload/reboot unless also saved permanently
Permanent Saved in config but may not affect current runtime until applied or reloaded
permanent: true Save rule persistently
immediate: true Apply rule to running firewalld immediately
state: enabled Add service/port/rule
state: disabled Remove service/port/rule

Use runtime-only rules mainly for testing or short troubleshooting. Do not use them for production access that must survive reload or reboot.

Open a port at runtime only:

yaml
- name: Open port 9090/tcp at runtime only
  ansible.posix.firewalld:
    port: 9090/tcp
    permanent: false
    immediate: true
    state: enabled

Compare runtime and permanent port lists:

bash
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "firewall-cmd --list-ports" -b

Sample output:

output
8080/tcp 9090/tcp
bash
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "firewall-cmd --permanent --list-ports" -b

Sample output:

output
8080/tcp

Port 9090/tcp appears only in the runtime list—after reload or reboot it disappears unless you also set permanent: true.

For production changes that must survive reboot, set both permanent: true and immediate: true on the same task.


Reload firewalld Safely

Reloading applies permanent configuration to the runtime firewall and drops unsaved runtime-only rules:

yaml
- name: Reload firewalld
  ansible.builtin.service:
    name: firewalld
    state: reloaded

After reload on the lab host, the runtime-only 9090/tcp rule was gone:

bash
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "firewall-cmd --list-ports" -b

Sample output:

output
8080/tcp

Use reload after bulk permanent edits if you want the running firewall to match saved config without a full restart.


Manage firewalld Zones

Most Rocky Linux servers use the public zone on active interfaces. Confirm the default zone:

bash
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "firewall-cmd --get-default-zone" -b

Sample output:

output
public

Target a specific zone when a host has multiple zones:

yaml
- name: Allow http in public zone
  ansible.posix.firewalld:
    zone: public
    service: http
    permanent: true
    immediate: true
    state: enabled

This article stays with the default zone. Full zone design—binding interfaces, sources, and masquerading—is outside scope.


Manage SELinux Mode with ansible.posix.selinux

Set enforcing mode for production hosts:

yaml
- name: Ensure SELinux is enforcing
  ansible.posix.selinux:
    policy: targeted
    state: enforcing

Confirm mode on the host:

bash
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "getenforce" -b

Sample output:

output
Enforcing

For short troubleshooting you can set permissive—SELinux logs denials but does not block:

yaml
- name: Set permissive for troubleshooting
  ansible.posix.selinux:
    policy: targeted
    state: permissive

Return to enforcing when you finish:

yaml
- name: Return to enforcing
  ansible.posix.selinux:
    policy: targeted
    state: enforcing

Changing between enforcing and permissive usually applies immediately. Enabling SELinux after it was disabled, or disabling it, can require a reboot. The module reports when a reboot is required, but it does not reboot the host for you.

Avoid state: disabled in automation unless you have a documented exception. Disabling SELinux removes mandatory access control across the host.


Manage SELinux Booleans with ansible.posix.seboolean

Booleans toggle allowed behavior in the targeted policy—often faster than custom file contexts when a stock boolean already exists.

yaml
- name: Allow httpd to connect to network
  ansible.posix.seboolean:
    name: httpd_can_network_connect
    state: true
    persistent: true

persistent: true keeps the boolean across reboots.

Verify on the host:

bash
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "getsebool httpd_can_network_connect" -b

Sample output:

output
httpd_can_network_connect --> on

Set state: false and persistent: true to turn a boolean off again.


Manage SELinux File Context Rules with sefcontext

community.general.sefcontext stores a persistent mapping—equivalent to semanage fcontext:

yaml
- name: Create custom web directory
  ansible.builtin.file:
    path: /srv/www/html
    state: directory
    owner: root
    group: root
    mode: "0755"

- name: Add persistent SELinux file context for web content
  community.general.sefcontext:
    target: "/srv/www/html(/.*)?"
    setype: httpd_sys_content_t
    state: present

The (/.*)? suffix labels the directory and everything beneath it. A pattern without it may only label the directory inode.

Confirm the stored rule:

bash
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "semanage fcontext -l" -b

Sample output:

output
/srv/www/html(/.*)?                                all files          system_u:object_r:httpd_sys_content_t:s0

sefcontext updates the policy database only—it does not relabel files already on disk.


Apply SELinux File Contexts with restorecon

Run restorecon after sefcontext or after copying files into a labeled tree:

yaml
- name: Apply SELinux context to web directory
  ansible.builtin.command: restorecon -Rv /srv/www/html
  register: restorecon_result
  changed_when: restorecon_result.stdout is search('Relabeled')

On first run the lab host reported a relabel from var_t to httpd_sys_content_t:

Sample output:

output
Relabeled /srv/www/html from unconfined_u:object_r:var_t:s0 to unconfined_u:object_r:httpd_sys_content_t:s0

After copying a new file, relabel that path:

yaml
- name: Copy sample index file
  ansible.builtin.copy:
    dest: /srv/www/html/index.html
    content: "<h1>Demo</h1>\n"
    owner: root
    group: root
    mode: "0644"

- name: Relabel copied file
  ansible.builtin.command: restorecon -v /srv/www/html/index.html
  register: restorecon_file
  changed_when: restorecon_file.stdout is search('Relabeled')

Use sefcontext for persistent automation—not chcon, which is easily overwritten by relabeling.


Verify SELinux Mode, Booleans and File Contexts

sestatus summarizes policy and mode:

bash
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "sestatus" -b

Sample output:

output
SELinux status:                 enabled
SELinuxfs mount:                /sys/fs/selinux
SELinux root directory:         /etc/selinux
Loaded policy name:             targeted
Current mode:                   enforcing
Mode from config file:          enforcing
...

Check the label on disk:

bash
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "ls -Zd /srv/www/html" -b

Sample output:

output
unconfined_u:object_r:httpd_sys_content_t:s0 /srv/www/html

For a single file:

bash
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "ls -Z /srv/www/html/index.html" -b

Sample output:

output
system_u:object_r:httpd_sys_content_t:s0 /srv/www/html/index.html

The type httpd_sys_content_t is what httpd-related services expect for static content.


Common Examples

Allow HTTP and HTTPS through firewalld

yaml
- name: Allow web services
  ansible.posix.firewalld:
    service: "{{ item }}"
    permanent: true
    immediate: true
    state: enabled
  loop:
    - http
    - https

Open a custom TCP port permanently

yaml
- name: Open application port
  ansible.posix.firewalld:
    port: 8080/tcp
    permanent: true
    immediate: true
    state: enabled

Close a custom TCP port permanently

yaml
- name: Close application port
  ansible.posix.firewalld:
    port: 8080/tcp
    permanent: true
    immediate: true
    state: disabled

Enable a SELinux boolean persistently

yaml
- name: Allow httpd outbound connections
  ansible.posix.seboolean:
    name: httpd_can_network_connect
    state: true
    persistent: true

Label a custom web directory

yaml
- name: Persistent httpd content label
  community.general.sefcontext:
    target: "/srv/www/html(/.*)?"
    setype: httpd_sys_content_t
    state: present

Restore SELinux context after copying files

yaml
- name: Relabel web tree
  ansible.builtin.command: restorecon -Rv /srv/www/html
  register: restorecon_result
  changed_when: restorecon_result.stdout is search('Relabeled')

Verify firewall and SELinux state

Run verification as separate ad-hoc tasks or a small verify play—one command per check:

bash
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "firewall-cmd --list-all" -b

Sample output:

output
public (default, active)
  target: default
  ...
  services: cockpit dhcpv6-client http https ssh
  ports: 8080/tcp
  ...
bash
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "getenforce" -b

Sample output:

output
Enforcing
bash
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "ls -Zd /srv/www/html" -b

Sample output:

output
unconfined_u:object_r:httpd_sys_content_t:s0 /srv/www/html

Common Mistakes

Mistake Fix
Opening a port only at runtime Use permanent: true and immediate: true when the rule must survive reboot
Forgetting to reload/apply firewall rules Verify with firewall-cmd --list-all; reload after bulk permanent edits if runtime drifts
Opening firewall but ignoring SELinux Check SELinux boolean and file context for the service
Disabling SELinux instead of fixing labels Use booleans and contexts where possible; keep enforcing in production
Expecting sefcontext to relabel files immediately Run restorecon after adding the rule
Using chcon for permanent automation Use sefcontext for persistent mappings
Forgetting required collections Install ansible.posix and community.general from pinned requirements.yml
Running without become: true firewalld and SELinux changes need root on the managed host

Best Practices

Practice Why it helps
Pin collection versions in requirements.yml Matches tested ansible-core and avoids surprise module changes
Set permanent: true and immediate: true together for production firewall rules Survives reboot and applies now
Verify with firewall-cmd --list-all after firewall plays Catches runtime-only or zone mistakes early
Keep SELinux enforcing; use booleans and contexts first Maintains mandatory access control without turning SELinux off
Add sefcontext then restorecon in the same play Persistent rule and on-disk labels stay aligned
Use FQCN (ansible.posix.firewalld) Makes collection dependencies obvious in shared repos
Re-run the playbook to confirm idempotency Second run should report ok on unchanged resources
SELinux task Recommended action
Temporarily allow troubleshooting Set permissive mode carefully, then return to enforcing
Keep SELinux enabled permanently Use enforcing mode
Allow service behavior Use an SELinux boolean when one exists
Label custom path Add sefcontext rule
Apply label to existing files Run restorecon
Verify actual label ls -Z
Verify persistent rule semanage fcontext -l

Summary

Manage Rocky Linux host security in two Ansible layers: ansible.posix.firewalld for services, ports, and zones with clear runtime versus permanent flags, and SELinux modules for mode, booleans, and file labels. Pair community.general.sefcontext with restorecon, verify with firewall-cmd --list-all, getenforce, and ls -Z, and keep both firewall and SELinux tasks under become: true so network and mandatory access rules stay idempotent across reruns.


References


Frequently Asked Questions

1. Which Ansible modules manage firewalld on Rocky Linux?

Use ansible.posix.firewalld for services, ports, and zones. Set permanent: true to save rules across reboots and immediate: true to apply them to the running firewall. Firewalld tasks need become: true.

2. What is the difference between permanent and immediate in ansible.posix.firewalld?

permanent: true writes the rule to saved configuration. immediate: true applies it to the active runtime firewall. For a rule that works now and after reboot, set both permanent: true and immediate: true.

3. Does sefcontext relabel files immediately?

No. sefcontext stores a persistent mapping like semanage fcontext. Run restorecon on existing paths after adding the rule. For deeper troubleshooting see the SELinux file context guide.

4. Should I disable SELinux when a service fails?

Prefer enforcing mode with the right boolean or file context. Disabling SELinux removes mandatory access control across the host. Use permissive only for short troubleshooting, then return to enforcing.

5. Which collections do I need for this playbook?

Install ansible.posix for firewalld, selinux, and seboolean. Install community.general for sefcontext. Pin versions in requirements.yml to match your ansible-core release.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …