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_selinux
and 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/id_rsa.pub
) to the remote systems, saving it as~/.ssh/authorized_keys
:
# ssh-keygen
# scp ~/.ssh/id_rsa.pub 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
rem1
- 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:
main.yml
---
- name: Create /usr/share/selinux/custom directory
file:
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:
site.yml
---
- hosts: all
roles:
- 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_t
SELinux 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_u
as its SELinux user component.
Let’s update the definition inmain.yml
and explicitly set the SELinux user and type:
main.yml
---
- name: Create /usr/share/selinux/custom directory
file:
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:setype
andseuser
. The Ansible file module supports the following SELinux-related parameters:
seuser
is the SELinux user of the resource. Set this tosystem_u
for system resources, as used in the example.serole
is 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_r
role, which is correct most of the time.setype
is the SELinux type of the resource and is the most commonly used SELinux parameter in file modules.selevel
is 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 called
test.cil
to 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 the
test.cil
file in thefiles/
folder of thepackt_selinux
role.
- Create a new code block in the
main.yml
file of the role, with the following content:
main.yml
- name: Upload test.cil file to /usr/share/selinux/custom
copy:
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 thetest_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
selinux:
policy: targeted
state: enforcing
- With the
seboolean
module, the SELinux booleans can be adjusted at will:
- name: Set httpd_builtin_scripting to true
seboolean:
name: httpd_builtin_scripting
state: yes
- The
sefcontext
module allows us to change SELinux file context definitions:
- name: Set the context for /srv/web
sefcontext:
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
selinux_permissive:
name: zoneminder_t
permissive: true
- The
selogin
module can be used to map a login to an SELinux user, as withsemanage login
:
- name: Map taylor's login to the unconfined_u user
selogin:
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
seport:
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.