Red Hat Certified Engineer (RHCE) #17 RHCSA Automation 4: firewall, SELinux, SSH keys
In #16 RHCSA Automation 3: storage (LVM), filesystems (NFS) we set up storage with a playbook. This time we cover the last area of RHCSA automation — security configuration. We’ll automate all of it with Ansible: permanently allowing ports with firewalld, adjusting SELinux booleans and file contexts, and deploying SSH public keys to user accounts.
These three are tasks you learned by hand in RHCSA. RHCSA #12 firewalld and SSH key authentication covered firewall-cmd --permanent and ssh-copy-id, and RHCSA #13 SELinux in depth drilled setsebool -P, semanage fcontext, and restorecon. RHCE is about producing the same results as an idempotent playbook.
Start with the collection each module lives in #
Most modules used for security tasks live in the ansible.posix and community.general collections. They may not be bundled with core, so let’s first confirm whether they’re installed in the exam environment.
| Task | Module | Collection |
|---|---|---|
| firewalld rules | firewalld | ansible.posix |
| SELinux boolean | seboolean | ansible.posix |
| SELinux mode | selinux | ansible.posix |
| File context | sefcontext | community.general |
| SSH public key deployment | authorized_key | ansible.builtin |
Confirm the module is visible with ansible-doc -l | grep firewalld, and if it isn’t, install it with ansible-galaxy collection install ansible.posix. Build the habit of looking up a module’s full set of options with ansible-doc ansible.posix.firewalld in an environment without internet access.
Automating firewalld #
We move the work you did in RHCSA with firewall-cmd --add-service=http --permanent and --reload to the ansible.posix.firewalld module. The key point of this module is that it controls permanent apply and immediate apply with separate options.
| Option | Meaning |
|---|---|
service or port | What to allow. A service name or port/protocol |
state | enabled to allow, disabled to block |
permanent: true | Permanent rule (survives a reboot). Corresponds to --permanent |
immediate: true | Applies to the runtime right away. Takes effect immediately without --reload |
zone | The zone to apply to. The default zone if omitted |
If you apply only --permanent, it isn’t reflected in the runtime until a reboot, so on the exam it’s safest to set permanent: true and immediate: true together. Setting only one leads to partial deductions like “it’s open now but closes after a reboot” or “it opens after a reboot but is closed now.”
---
- name: Configure firewalld
hosts: webservers
become: true
tasks:
- name: Ensure firewalld is running
ansible.builtin.service:
name: firewalld
state: started
enabled: true
- name: Allow http service permanently and immediately
ansible.posix.firewalld:
service: http
state: enabled
permanent: true
immediate: true
- name: Allow https service
ansible.posix.firewalld:
service: https
state: enabled
permanent: true
immediate: true
- name: Allow custom tcp port 8080
ansible.posix.firewalld:
port: 8080/tcp
state: enabled
permanent: true
immediate: trueFor service, use a name defined in /usr/lib/firewalld/services, and open a port that has no definition directly in the port: 8080/tcp form. To handle several services at once, you can group them with a loop.
- name: Allow multiple services
ansible.posix.firewalld:
service: "{{ item }}"
state: enabled
permanent: true
immediate: true
loop:
- http
- https
- cockpitAutomating SELinux #
SELinux work splits into three branches: setting the mode, toggling booleans, and assigning file contexts. Each uses a different module, so let’s learn them separately.
1) Setting the mode: the selinux module #
Use the ansible.posix.selinux module to set enforcing or permissive mode. state: enforcing is the policy-enforcing mode, and policy: targeted is the standard policy.
- name: Ensure SELinux is enforcing
ansible.posix.selinux:
state: enforcing
policy: targetedFor a change that requires a reboot — such as switching from disabled to enabled — this module reports reboot_required in its result. In that case, the safe pattern is to follow up with an ansible.builtin.reboot task.
2) Toggling booleans: the seboolean module #
The work you did with setsebool -P httpd_can_network_connect on is the ansible.posix.seboolean module. persistent: true corresponds to RHCSA’s -P, and leaving it out resets the value after a reboot.
- name: Allow httpd to make network connections
ansible.posix.seboolean:
name: httpd_can_network_connect
state: true
persistent: true| Option | Meaning |
|---|---|
name | The boolean name. Check it with getsebool -a |
state | true to turn on, false to turn off |
persistent: true | Survives a reboot. Corresponds to setsebool -P |
3) File context: the sefcontext module #
If you place content under a non-standard path (e.g., web content under /web), the SELinux labels won’t match and the service can’t access it. In RHCSA you added a rule to the policy with semanage fcontext and applied the label to the actual files with restorecon.
The community.general.sefcontext module only adds the policy rule. That is, it does only the work that semanage fcontext does, and applying the label to actual files (restorecon) must be handled as a separate task. Pairing the two is the standard approach.
- name: Add fcontext rule for /web
community.general.sefcontext:
target: '/web(/.*)?'
setype: httpd_sys_content_t
state: present
- name: Apply the new context to existing files
ansible.builtin.command:
cmd: restorecon -Rv /webIf you run only sefcontext, the rule goes into the policy but the labels of files that already exist don’t change. So you have to run restorecon once with command for it to take effect on the actual files. command is flagged as changed every time, which throws off idempotency slightly, but on the exam dropping restorecon is a far bigger deduction, so I recommend keeping them together.
Deploying SSH public keys #
We automate the public-key deployment you did with ssh-copy-id in RHCSA using the ansible.builtin.authorized_key module. It’s a module that idempotently adds a public key to a specific user’s ~/.ssh/authorized_keys.
- name: Deploy public key for deploy user
ansible.builtin.authorized_key:
user: deploy
state: present
key: "{{ lookup('file', 'files/deploy_id_ed25519.pub') }}"| Option | Meaning |
|---|---|
user | The target user account |
key | The public-key string to register. Read from a file with lookup('file', ...) |
state | present to add, absent to remove |
exclusive: true | Keep only this key and remove all existing keys |
For key, the common approach is to read a public-key file placed on the control node with lookup('file', ...). To deploy each user’s own key to multiple users, combine a variable dictionary with a loop.
- name: Deploy keys for multiple users
ansible.builtin.authorized_key:
user: "{{ item.name }}"
state: present
key: "{{ lookup('file', item.keyfile) }}"
loop:
- { name: alice, keyfile: files/alice.pub }
- { name: bob, keyfile: files/bob.pub }The system role alternative #
This area too can be handled with the rhel-system-roles covered in #13 system roles instead of wiring up modules by hand. If the exam explicitly tells you to use a system role, you must use the role; otherwise, pick whichever approach you are comfortable with.
firewall system role #
Declare the rules as variables for the redhat.rhel_system_roles.firewall role. Since the role internals handle even the permanent apply, there’s less need to chase down each option one by one.
- name: Configure firewall via system role
hosts: webservers
become: true
roles:
- redhat.rhel_system_roles.firewall
vars:
firewall:
- service: http
state: enabled
- service: https
state: enabled
- port: 8080/tcp
state: enabledselinux system role #
With the redhat.rhel_system_roles.selinux role, declare the mode, booleans, and file contexts all at once. Since the role handles applying the context as well, you don’t have to call restorecon separately.
- name: Configure SELinux via system role
hosts: webservers
become: true
roles:
- redhat.rhel_system_roles.selinux
vars:
selinux_state: enforcing
selinux_booleans:
- name: httpd_can_network_connect
state: true
persistent: true
selinux_fcontexts:
- target: '/web(/.*)?'
setype: httpd_sys_content_t
state: present
selinux_restore_dirs:
- /webIntegrated example #
Tying the three areas into a single playbook gives you something close to the exam’s security-automation problem. The flow is: open ports with firewalld, turn on an SELinux boolean, and deploy an SSH key.
---
- name: Secure web hosts
hosts: webservers
become: true
tasks:
- name: Open http and https permanently
ansible.posix.firewalld:
service: "{{ item }}"
state: enabled
permanent: true
immediate: true
loop:
- http
- https
- name: Enforce SELinux
ansible.posix.selinux:
state: enforcing
policy: targeted
- name: Enable httpd network boolean
ansible.posix.seboolean:
name: httpd_can_network_connect
state: true
persistent: true
- name: Add fcontext for content dir
community.general.sefcontext:
target: '/web(/.*)?'
setype: httpd_sys_content_t
state: present
- name: Restore context on content dir
ansible.builtin.command:
cmd: restorecon -Rv /web
- name: Deploy admin public key
ansible.builtin.authorized_key:
user: deploy
state: present
key: "{{ lookup('file', 'files/deploy.pub') }}"Exam points #
- For firewalld, set
permanent: trueandimmediate: truetogether — it’s the safe choice. Setting only one makes the before-and-after-reboot state inconsistent and earns a partial deduction. - For seboolean, always include
persistent: true. It corresponds to RHCSA’ssetsebool -P, and leaving it out resets the value after a reboot. - sefcontext only adds the rule. For the label to take effect on actual files,
restoreconmust be run as a separate task. - The selinux module reports
reboot_requiredfor a change that needs a reboot (disabled to enabled). If needed, follow up with areboottask. - For authorized_key, the
keyreads the control node’s public key withlookup('file', ...).exclusive: trueerases existing keys, so use it only when you intend to. - If a module isn’t found, run
ansible-galaxy collection install ansible.posixorcommunity.generalas needed.
Wrap-up #
What this post locked in:
- firewalld. Permanently allow services and ports with
ansible.posix.firewalldusingpermanent: true+immediate: true - SELinux mode. Set enforcing/targeted with
ansible.posix.selinux. Reboot if needed - SELinux boolean. Toggle with
ansible.posix.seboolean. Apply permanently withpersistent: true - SELinux context. Add the rule with
community.general.sefcontext, then apply withrestorecon - SSH keys. Deploy per-user public keys with
ansible.builtin.authorized_key - The system role alternative. Handle the same work as variable declarations with the firewall and selinux roles
That wraps up the four RHCSA-automation posts starting from #14. Users/packages, services/time/logs, storage, and security — we’ve moved all of RHCSA’s manual work into playbooks. Since this area accounts for half the exam’s weight, getting comfortable with each module’s persistent-apply option is the shortcut to clearing the passing line.
Next: exam tips #
Now that we’ve covered the whole automation area, it’s time to lay out how to actually run your 4 hours.
In #18 Exam tips and time management, we’ll lay out practical, exam-ready tips: how to use ansible-doc, the habit of verifying idempotency, a checklist for confirming permanent apply, and a time-allocation strategy.