Red Hat Certified Engineer (RHCE) #17 RHCSA Automation 4: firewall, SELinux, SSH keys

9 min read

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.

TaskModuleCollection
firewalld rulesfirewalldansible.posix
SELinux booleansebooleanansible.posix
SELinux modeselinuxansible.posix
File contextsefcontextcommunity.general
SSH public key deploymentauthorized_keyansible.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.

OptionMeaning
service or portWhat to allow. A service name or port/protocol
stateenabled to allow, disabled to block
permanent: truePermanent rule (survives a reboot). Corresponds to --permanent
immediate: trueApplies to the runtime right away. Takes effect immediately without --reload
zoneThe 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.”

firewalld configuration playbook
---
- 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: true

For 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.

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

Automating 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.

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

For 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.

Toggle an SELinux boolean
    - name: Allow httpd to make network connections
      ansible.posix.seboolean:
        name: httpd_can_network_connect
        state: true
        persistent: true
OptionMeaning
nameThe boolean name. Check it with getsebool -a
statetrue to turn on, false to turn off
persistent: trueSurvives 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.

Add fcontext rule and restorecon
    - 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 /web

If 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.

Deploy a public key
    - name: Deploy public key for deploy user
      ansible.builtin.authorized_key:
        user: deploy
        state: present
        key: "{{ lookup('file', 'files/deploy_id_ed25519.pub') }}"
OptionMeaning
userThe target user account
keyThe public-key string to register. Read from a file with lookup('file', ...)
statepresent to add, absent to remove
exclusive: trueKeep 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.

Deploy keys to multiple users
    - 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.

firewall role playbook
- 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: enabled

selinux 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.

selinux role playbook
- 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:
      - /web

Integrated 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.

Integrated security playbook
---
- 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: true and immediate: true together — 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’s setsebool -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, restorecon must be run as a separate task.
  • The selinux module reports reboot_required for a change that needs a reboot (disabled to enabled). If needed, follow up with a reboot task.
  • For authorized_key, the key reads the control node’s public key with lookup('file', ...). exclusive: true erases existing keys, so use it only when you intend to.
  • If a module isn’t found, run ansible-galaxy collection install ansible.posix or community.general as needed.

Wrap-up #

What this post locked in:

  • firewalld. Permanently allow services and ports with ansible.posix.firewalld using permanent: true + immediate: true
  • SELinux mode. Set enforcing/targeted with ansible.posix.selinux. Reboot if needed
  • SELinux boolean. Toggle with ansible.posix.seboolean. Apply permanently with persistent: true
  • SELinux context. Add the rule with community.general.sefcontext, then apply with restorecon
  • 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.

X