Using Ansible for SELinux System Administration

Posted on the 19 June 2021 by Satish Kumar @satish_kumar86

The first orchestration and automation tooling we’ll consider is Ansible, a very popular open source solution for the remote management of systems. Ansible has commercial backing through Red Hat but does not limit its support to Red Hat or even Linux systems. Other environments such as Windows environments or even network setups have significant Ansible-based support.

How Ansible works

Ansiblegenerallyuses a central server that hosts the configuration andinterprets the settings. The Ansible runtime then connects to the remote systems over SSH, sending the necessary data to a temporary location, and then executes the steps locally.

The use of SSH as its main connection approach has significant advantages: administrators know how this protocol works and how to configure and control it. Furthermore, Ansible does not require any additional deployments on the target machines, except for Python and libselinux’s Python bindings (which are often installed on SELinux-enabled machines by default).

Ansible knows how to address the various resources through its modules.Ansible modulescontainthe logic that Ansible uses to execute tasks correctly. The module code is distributed to the target machines and is executed on the remote systems.

The definitions that administrators configure systems with are stored in Ansible playbooks.Playbooksdefinehow a system should be configured, and Ansible will read and interpret playbooks to see what it must execute on each system.

To facilitate the management of Ansible playbooks in larger environments, Ansible usesAnsible rolesto bundlecoherent definitions. Administrators can then, in their playbooks, assign roles to systems to automatically uplift the state of those systems accordingly. For instance, a role can be created to create a properly configured web server, a database, and so on.

In thischapter, we will create a role calledpackt_selinuxand apply it to a remote system. Within that role, we will show how to configure and execute the various SELinux tasks using Ansible.

Installing and configuring Ansible

To install and set up Ansible, most Linux distributions offer out-of-the-box support for the framework. On CentOS, the following steps can be taken. Users of other distributions can easily deduce the steps for their platform:

  • You need to enable Extra Packages for Enterprise Linux (EPEL), after which you can install Ansible easily. Execute this on the master node (from which you want to manage the other systems):
# yum install epel-release
# yum install ansible
  • Once installed, create an SSH key pair to use between the master system and the target systems that we will be managing with Ansible. Use the ssh-keygen command to create a key pair on the master system, and then copy the public key (~/.ssh/ to the remote systems, saving it as ~/.ssh/authorized_keys:
# ssh-keygen
# scp ~/.ssh/ rem1:/root/.ssh/authorized_keys
  • Test to see whether the remote connection works properly, for instance, by executing the id command remotely:
# ssh rem1 id
  • If the test is successful, we can configure Ansible to see this remote system as one of the nodes it will be managing. To accomplish this, edit /etc/ansible/hosts and add the hostname to the list:
# cat /etc/ansible/hosts
  • To see whether Ansible can correctly manage the remote system, we can ask it to gather all the facts about the remote system. Facts in Ansible represent the discovered settings of the remote system and can be used to fine-tune playbooks and roles later. For instance, the Ansible facts discovered of the distribution can be used to select which package name an installation uses:
# ansible all -m setup

This instruction asks all managed hosts (all, reflecting all entries in /etc/ansible/hosts) to execute the tasks in the setup module.

The output of the last task is a large set of discovered facts, showing us that the connection succeeded and that Ansible is ready to manage the remote system.

Creating and testing the Ansible role

To allow reusable configurations across multiple systems, Ansible recommends the use of its Ansible roles. We will create a role called packt_selinux, have it create a custom directory, and then assign this role to the remote system:

  • Use ansible-galaxy to create an empty yet ready-to-use role:
# cd /etc/ansible/roles
# ansible-galaxy init packt_selinux --offline
- Role packt_selinux was created successfully

This command will create the necessary files and directories that constitute a role. The file we will use is packt_selinux/tasks/main.yml, which will host all the settings and definitions we want to apply when we assign the packt_selinux role to a system. The other directories are, for our brief introduction to Ansible, less relevant, but play an important role in making sufficiently modular roles.

  • Edit the main.yml file and have it create a custom directory. The content of the file should look like this:


- name: Create /usr/share/selinux/custom directory
        path: /usr/share/selinux/custom
        owner: root
        group: root
        mode: '0755'
        state: directory

In later steps, this file will be extended with more and more blocks. Each block will start with a name that identifies the block, and then the state definition. In the current block, we used Ansible’s file module to assert that a file or directory is available with the parameters given.

  • Assign the role to the remote system and apply the playbook. We accomplish this by first creating an /etc/ansible/site.yml file with the following content:


- hosts: all 
        - packt_selinux
  • Run this playbook to apply the setting defined in our role to the remote systems:
# ansible-playbook /etc/ansible/site.yml

Ansible will display its progress, as well as for which tasks it has executed a change. In our case, a change would mean that the directory has been created.

Now that we have tested our role and assigned the role to the remote system, all we need to do is update the role gradually until it contains all the logic we need. No other configuration is needed, and after each change, we can rerun the ansible-playbook command from the main server.

Assigning SELinux contexts to filesystem resources with Ansible

In thecurrent role, we create a custom directory inside/usr/share/selinux. This parent directory has theusr_tSELinux type set, so the newly created subdirectory has it as well. The SELinux user of this directory, however, will be different, as Ansible has created the directory after remotely logging in to the system. In a default CentOS configuration, this means that the target directory’s context will haveunconfined_uas its SELinux user component.

Let’s update the definition inmain.ymland explicitly set the SELinux user and type:


- name: Create /usr/share/selinux/custom directory
        path: /usr/share/selinux/custom
        owner: root
        group: root
        mode: '0755'
        state: directory
        setype: 'usr_t'
        seuser: 'system_u'

After applying the change (usingansible-playbook), the updated definition results in a correctly set SELinux user and SELinux type for this directory.

In this case, we added two parameters to the file definition:setypeandseuser. The Ansible file module supports the following SELinux-related parameters:

  • seuseris the SELinux user of the resource. Set this tosystem_ufor system resources, as used in the example.
  • seroleis the SELinux role of the resource. This is generally not used, as role inheritance on the system will generally result in the resource being labeled with theobject_rrole, which is correct most of the time.
  • setypeis the SELinux type of the resource and is the most commonly used SELinux parameter in file modules.
  • selevelis the SELinux sensitivity level for the resource. By default, it is set tos0.

As we’ve learned from the example already, you do not need to declare the type if the inherited context is correct.

Loading custom SELinux policies with Ansible

Ansible’scurrent release has no support for loading custom SELinux modules. While custom modules are found on Ansible galaxy (the ecosystem where contributors can add more modules), let’s see how we would handle distributing a custom policy to the systems under Ansible control and loading the module, but only if it is not loaded yet.

While we could start creating custom modules ourselves, let’s use a combination of tasks in the existing role to accomplish this. We will try to accomplish the following tasks in sequence:

  • Upload a custom policy calledtest.cilto the remote system.
  • Check whether this custom policy is already loaded.
  • Load the custom policy, but only if the previous check failed.

These three tasks are handled through three modules: the copy module, the shell module, and the command module. We will use each of these modules in separate steps:

  • Create the custom policy mentioned earlier in this chapter by placing thetest.cilfile in thefiles/folder of thepackt_selinuxrole.
  • Create a new code block in the main.yml file of the role, with the following content:


- name: Upload test.cil file to /usr/share/selinux/custom
        src: test.cil
        dest: /usr/share/selinux/custom/test.cil
        owner: root
        group: root
        mode: '0644'

This will ensure that the test.cil file, currently on the master machine, is distributed to the target nodes in the directory we’ve previously created.

  • Next, we check whether the policy is already loaded. For this, we use the shell module and use the fail or success state later. Hence, we store the return in the test_is_loaded variable, and explicitly tell Ansible to ignore a failure as we use this as a check rather than a state definition:
- name: Check if test SELinux module is loaded
  shell: /usr/sbin/semodule -l | grep -q ^test$
  register: test_is_loaded
  ignore_errors: True
  • The command module loads the policy file, and only if the previous task failed:
- name: Load test.cil if not loaded yet
  command: /usr/sbin/semodule -i /usr/share/selinux/custom/test.cil
  when: test_is_loaded is failed

This approach shows how we can use our knowledge of SELinux to define and set states. This method can be used for other SELinux settings as well, for instance, by validating the output of listings (for example, with semanage) before defining or adjusting settings.

Using Ansible’s out-of-the-box SELinux support

Ansible has quite a few modules available to provide native support for several SELinux-related settings, which we briefly cover here:

  • The selinux module can be used to set or change the SELinux state (enforcing or permissive) as well as to select the appropriate SELinux policy type (such as targeted):
- name: Set SELinux to enforcing mode
          policy: targeted
          state: enforcing
  • With the seboolean module, the SELinux booleans can be adjusted at will:
- name: Set httpd_builtin_scripting to true
          name: httpd_builtin_scripting
          state: yes
  • The sefcontext module allows us to change SELinux file context definitions:
- name: Set the context for /srv/web
          target: '/srv/web(/.*)?'
          setype: httpd_sys_content_t
          state: present
  • With selinux_permissive, we can selectively mark certain SELinux policy domains as permissive:
- name: Set zoneminder_t as permissive domain
          name: zoneminder_t
          permissive: true
  • The selogin module can be used to map a login to an SELinux user, as with semanage login:
- name: Map taylor's login to the unconfined_u user
          login: taylor
          seuser: unconfined_u
          state: present
  • seport can be used to create an SELinux port mapping:
- name: Set port 10122 to ssh_port_t
          ports: 10122
          proto: tcp
          setype: ssh_port_t
          state: present

Other SELinux settings might be supported through custom modules, but with the method presented earlier, administrators can already start configuring SELinux across all systems in their environment.

Back to Featured Articles on Logo Paperblog