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.posix2.1.0;community.general9.5.2.
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.
~/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.firewalldfor services, ports, zones, and runtime versus permanent rulesansible.posix.selinuxfor enforcing, permissive, and disabled modeansible.posix.sebooleanfor persistent SELinux booleanscommunity.general.sefcontextplusrestoreconfor custom paths such as/srv/www/html- Verification with
firewall-cmd,getenforce,sestatus,getsebool,ls -Z, andsemanage 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:
cd ~/ansible-projectConfirm the managed host answers Ansible:
ansible -i inventory/hosts lab -m ansible.builtin.pingSample 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:
ansible --versionSample output:
ansible [core 2.16.16]
config file = /home/ansible/ansible-project/ansible.cfgPlays 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:
---
collections:
- name: ansible.posix
version: "2.1.0"
- name: community.general
version: "9.5.2"ansible-galaxy collection install -r requirements.ymlList installed versions:
ansible-galaxy collection listSample output:
community.general 9.5.2
ansible.posix 2.1.0Pin 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:
ansible -i inventory/hosts rocky2 -m ansible.builtin.dnf -a "name=firewalld,policycoreutils-python-utils,selinux-policy-targeted state=present" -bConfirm packages are present:
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "rpm -q firewalld policycoreutils-python-utils selinux-policy-targeted" -bSample 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.noarchpolicycoreutils-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:
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "python3 -c 'import firewall; print(\"python-firewall ok\")'" -bSample output:
python-firewall okA 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:
- name: Ensure firewalld is enabled and running
ansible.builtin.service:
name: firewalld
state: started
enabled: trueAfter the play runs, confirm the daemon is active:
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "systemctl is-active firewalld" -bSample output:
activeIf 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:
- 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: enabledList the active zone after the play:
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "firewall-cmd --list-all" -bSample 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:
- name: Open custom TCP port 8080 permanently and immediately
ansible.posix.firewalld:
port: 8080/tcp
permanent: true
immediate: true
state: enabledCheck open ports:
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "firewall-cmd --list-ports" -bSample output:
8080/tcpClose 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:
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "firewall-cmd --list-all" -bSample 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:
- name: Verify SELinux mode
ansible.builtin.command: getenforce
register: selinux_mode
changed_when: falseRuntime 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:
- name: Open port 9090/tcp at runtime only
ansible.posix.firewalld:
port: 9090/tcp
permanent: false
immediate: true
state: enabledCompare runtime and permanent port lists:
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "firewall-cmd --list-ports" -bSample output:
8080/tcp 9090/tcpansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "firewall-cmd --permanent --list-ports" -bSample output:
8080/tcpPort 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:
- name: Reload firewalld
ansible.builtin.service:
name: firewalld
state: reloadedAfter reload on the lab host, the runtime-only 9090/tcp rule was gone:
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "firewall-cmd --list-ports" -bSample output:
8080/tcpUse 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:
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "firewall-cmd --get-default-zone" -bSample output:
publicTarget a specific zone when a host has multiple zones:
- name: Allow http in public zone
ansible.posix.firewalld:
zone: public
service: http
permanent: true
immediate: true
state: enabledThis 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:
- name: Ensure SELinux is enforcing
ansible.posix.selinux:
policy: targeted
state: enforcingConfirm mode on the host:
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "getenforce" -bSample output:
EnforcingFor short troubleshooting you can set permissive—SELinux logs denials but does not block:
- name: Set permissive for troubleshooting
ansible.posix.selinux:
policy: targeted
state: permissiveReturn to enforcing when you finish:
- name: Return to enforcing
ansible.posix.selinux:
policy: targeted
state: enforcingChanging 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.
- name: Allow httpd to connect to network
ansible.posix.seboolean:
name: httpd_can_network_connect
state: true
persistent: truepersistent: true keeps the boolean across reboots.
Verify on the host:
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "getsebool httpd_can_network_connect" -bSample output:
httpd_can_network_connect --> onSet 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:
- 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: presentThe (/.*)? suffix labels the directory and everything beneath it. A pattern without it may only label the directory inode.
Confirm the stored rule:
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "semanage fcontext -l" -bSample output:
/srv/www/html(/.*)? all files system_u:object_r:httpd_sys_content_t:s0sefcontext 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:
- 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:
Relabeled /srv/www/html from unconfined_u:object_r:var_t:s0 to unconfined_u:object_r:httpd_sys_content_t:s0After copying a new file, relabel that path:
- 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:
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "sestatus" -bSample 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:
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "ls -Zd /srv/www/html" -bSample output:
unconfined_u:object_r:httpd_sys_content_t:s0 /srv/www/htmlFor a single file:
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "ls -Z /srv/www/html/index.html" -bSample output:
system_u:object_r:httpd_sys_content_t:s0 /srv/www/html/index.htmlThe type httpd_sys_content_t is what httpd-related services expect for static content.
Common Examples
Allow HTTP and HTTPS through firewalld
- name: Allow web services
ansible.posix.firewalld:
service: "{{ item }}"
permanent: true
immediate: true
state: enabled
loop:
- http
- httpsOpen a custom TCP port permanently
- name: Open application port
ansible.posix.firewalld:
port: 8080/tcp
permanent: true
immediate: true
state: enabledClose a custom TCP port permanently
- name: Close application port
ansible.posix.firewalld:
port: 8080/tcp
permanent: true
immediate: true
state: disabledEnable a SELinux boolean persistently
- name: Allow httpd outbound connections
ansible.posix.seboolean:
name: httpd_can_network_connect
state: true
persistent: trueLabel a custom web directory
- name: Persistent httpd content label
community.general.sefcontext:
target: "/srv/www/html(/.*)?"
setype: httpd_sys_content_t
state: presentRestore SELinux context after copying files
- 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:
ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "firewall-cmd --list-all" -bSample output:
public (default, active)
target: default
...
services: cockpit dhcpv6-client http https ssh
ports: 8080/tcp
...ansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "getenforce" -bSample output:
Enforcingansible -i inventory/hosts rocky2 -m ansible.builtin.command -a "ls -Zd /srv/www/html" -bSample output:
unconfined_u:object_r:httpd_sys_content_t:s0 /srv/www/htmlCommon 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
- firewalld documentation — zones, services, and runtime versus permanent configuration
- ansible.posix.firewalld module
- ansible.posix.selinux module
- ansible.posix.seboolean module
- community.general.sefcontext module
- SELinux User and Administrator Guide (Red Hat)
- Collection and variable errors — FQCN and Galaxy install issues
- SELinux file contexts with Ansible — deeper
sefcontextand troubleshooting

