How to create its own custom SELinux policy module wisely

Updated

Even though Red Hat doesn't recommend customers to create custom SELinux policy modules, which makes the system potentially unsupported, it is sometimes necessary to add custom rules, for example if an application requires access to some specific resource in a non-standard system path.
This article describes the procedure to create a custom SELinux policy module step by step, enabling the administrator or custom policy writer to understand the security implications of writing such module.


IMPORTANT: Red Hat doesn't support creating custom SELinux policy modules, because this falls outside of the Production Support Scope of Coverage. If you are not an expert, consult your Red Hat sales representative and request consulting services.


Table of contents


1. Preliminary notes

1.1. Why audit2allow should never be used to build a module

Red Hat provides a tool named audit2allow as part of the package policycoreutils-python (on RHEL7) and policycoreutils-python-utils (on RHEL8).
This tool blindly proposes rules based on the audit log.

Please don't use this tool to write a custom policy module, because it's necessary to understand the consequence of adding a rule to your system's security configuration.

The only usage you may make is to use audit2allow -a to have a quick look of what fails to have some idea of the problem.

1.2. When you may write a custom policy module

There are multiple cases when you may write your own custom policy module:

  1. when you hit a known bug (i.e. some missing rule), but then please stick to the Knowledge Base article that should be linked to the Bugzilla
  2. when you know you are customizing an installation on purpose, for example you want to let mysqld read files hosted on the remote file system, which is currently not possible

1.3. When you should not write a custom policy module

There are also cases when you should not write a custom policy module:

  1. when audit2allow detects that files are mislabeled
  2. when audit2allow detects that the functionality is available through enabling a boolean
  3. when you believe there is some bug in the policy
  4. when audit2allow proposes a very long list of rules to add for a given SELinux type, indicating likely that something is wrong
  5. when audit2allow proposes rules related to writing/appending/creating on default_t or usr_t contexts

If unsure, create a case on the Customer Portal, describe the issue and provide a sosreport.


2. Setup the system

2.1. Install the tools required to build a custom policy module

We need to install a few packages to build a custom policy module wisely.
In particular we will want to check the current SELinux policy content to mimic what is done there.

# yum -y install policycoreutils-devel setools-console yum-utils rpm-build make

The policycoreutils-devel package provides the tools to build a custom policy module.
The setools-console package provides tools to search into the installed policy, e.g. sesearch.
The rpm-build package provides tools to be able to extract sources from source packages.

2.2. Configure the system to be able to download source packages (optional but recommended)

You may use This content is not included.Package Browser to download the selinux-policy SRPM matching your system.
An alternative is to subscribe to source packages repositories.

  • On RHEL7

    # subscription-manager repos --enable rhel-7-server-source-rpms
    # yumdownloader --source selinux-policy
    
  • On RHEL8

    # subscription-manager repos --enable rhel-8-for-x86_64-baseos-source-rpms
    # yumdownloader --source selinux-policy
    

2.3. Install the SELinux policy sources

# rpm -ivh ./selinux-policy-xxx.src.rpm
# cd rpmbuild/SPECS/
# rpmbuild -bp --nodeps selinux-policy.spec

This will extract the package content and policy to /root/rpmbuild/BUILD.

The system is now ready to build custom policy modules.


3. Overview of building a SELinux custom module

There are a few steps to build a custom module.

3.1. Collect the (potentially) missing rules

Reading the audit log, you may collect the potentially missing rules.
We employ here the word potentially because there may be false positives, in particular when running the system in Permissive mode or when running a particular domain as a permissive domain.

Additionally, there may be cases where some paths are mislabeled, causing AVCs to pop up. This kind of issues can be fixed using a relabeling or using customized contexts created through using semanage fcontext -a ... command.

Usually we do not recommend moving the whole system to Permissive mode but making the failing domain run as a permissive domain instead, as shown in the example below for smartctl daemon running in fsdaemon_t context:

# semanage permissive -a fsdaemon_t

If you don't set the domain as a permissive domain, you will usually only get one AVC related to the first operation that was denied by SELinux, but sometimes this is enough to investigate further.

There are a few tools to collect AVCs:

  • ausearch -m avc,user_avc prints AVCs in human-friendly format

    Using this tool is very convenient to understand how the failing program works and what it wants to access.

  • audit2allow -a -t <selinux_type> prints rules that would avoid having AVCs if they were applied on the system.

    Using this tool is recommended only after having analyzed the output of ausearch command above, because the command output lacks details and can easily make the policy writer misunderstand the real root cause.
    One interesting case where the tool is very useful is when it detects mislabeled files or a missing SELinux boolean.

This step is the most important and difficult. This is where you have to understand what is going on and what is suitable.
We recommend mimicing what is done in the current SELinux policy, as shown in the example in 4. Example: allowing mysqld to read files stored on a Samba filesystem.

3.2. Create a text file named with suffix ".te" that contains the policy to build

In the current working directory, create a file named with ".te" suffix that will describe the policy module.
There are multiple formats available, we recommend using the template below:

policy_module(local_<type_or_daemon>, <version>)

gen_require(`
	type <type>;
	...
')

<rules>

The policy module named (here local_<type_or_daemon>) must match the file name with ".te" suffix.
We recommend prefixing the module name by local_ to easily distinguish between standard modules and custom ones.

3.3. Compile the policy module

Once the ".te" file is created, we use the following command to create the binary representation that will have to be loaded in the SELinux policy:

# make -f /usr/share/selinux/devel/Makefile local_<type_or_daemon>.pp

Note that we specify above the suffix ".pp", not ".te".

3.4. Install the policy module

It's not necessary to rebuild the ".pp" file on all systems, we can install the ".pp" file on all systems as long as the SELinux policy is compatible.
For example, you can reuse a ".pp" created on a RHEL7.5 system on a RHEL7.9 system.
However, you need to build a new ".pp" file to install it on RHEL8 systems.

The installation and enablement of the module is done as shown below:

# semodule -i local_<type_or_daemon>.pp

3.5. Verify that the policy module is installed

You can check that the policy module was installed by verifying that there is a corresponding module in /etc/selinux/<policy>/active/modules/400 (on RHEL7) or /var/lib/selinux/<policy>/active/modules/400 (on RHEL8), as shown in the example below:

# ls /etc/selinux/targeted/active/modules/400/local_nvme/
cil  hll  lang_ext

Particularly, you can uncompress the content of the cil file to see the new rules, in CIL format (which is a bit different from normal format, but still very understandable), as shown in the example below:

# bunzip2 -c /etc/selinux/targeted/active/modules/400/local_nvme/cil
(allow fsdaemon_t nvme_device_t (chr_file (getattr ioctl lock open read)))

In the example above, we added a rule to allow smartctl (running as fsdaemon_t) to read NVME disks, which is not possible due to This content is not included.BZ 1622284 - policy blocks smartd access to nvme device on RHEL7.

Additionally, you can search the policy for the rules you added by using sesearch, as shown in the example below:

# sesearch -A -s fsdaemon_t -t nvme_device_t
Found 1 semantic av rules:
   allow fsdaemon_t nvme_device_t : chr_file { ioctl read getattr lock open } ; 

3.6. Monitor the failing program to see whether it's still failing or not

If the program is still failing, then this indicates that some more rules are missing.
In this case, update your custom module with the missing rules and increase the version of the module for clarity.

If the program is now fully functional and the corresponding SELinux domain was configured as a permissive domain, restore the domain as a regular domain, as shown in the example below:

# semanage permissive -d fsdaemon_t

4. Example 1 - allowing mysqld to read files stored on a Samba filesystem

4.1. Context

In this example, we configured mysqld (running as mysqld_t) to read a file from a Samba filesystem (/cifs-server mounted as cifs_t), for example through using the following SQL command:

LOAD DATA INFILE '/cifs-server/data/file.csv' INTO TABLE ...

This kind of operation is not possible in the current policy, because it's not something common.

4.2. Collect the AVCs

While running mysqld as a regular domain, we can see the following AVC popping up:

# ausearch -m AVC,USER_AVC -ts recent
[...]
type=AVC msg=audit(...): avc:  denied  { getattr } for  pid=XXX comm="mysqld" path="/cifs-server/data/file.csv" dev="cifs" ino=XXX scontext=system_u:system_r:mysqld_t:s0 tcontext=system_u:object_r:cifs_t:s0 tclass=file permissive=0

Due to running as a regular non-permissive domain, mysqld was prevented immediately after trying to check the file attributes, even before opening and reading the file.
If mysqld had been configured as a permissive domain, we would have see similar AVCs for open and read (and even maybe lock).

We can verify that there is no rule for that, even through using a boolean:

# sesearch -C -A -s mysqld_t -t cifs_t -c file -p getattr
--> nothing returned

Note: on RHEL8, there is no -C option; the option is only used on RHEL7 to see rules enabled through a boolean otherwise they are hidden.

4.3. Find out which rules are required

From above we know that getattr operation is required, but we don't know what else.
To be safe and not put the system at risk, we can browse the current policy to try to find out rules related to Samba (cifs_t) for other types that may apply to mysqld_t.

# sesearch -A -t cifs_t -c file -p getattr
[...]
allow ftpd_t cifs_t:file { append create getattr ioctl link lock open read rename setattr unlink write }; [ ftpd_anon_write && ftpd_use_cifs ]:True
allow ftpd_t cifs_t:file { append create getattr ioctl link lock open read rename setattr unlink write }; [ use_samba_home_dirs ]:True
allow ftpd_t cifs_t:file { getattr ioctl lock open read }; [ ftpd_use_cifs ]:True
allow ftpd_t non_security_file_type:file { append create getattr ioctl link lock open read rename setattr unlink write }; [ ftpd_full_access ]:True
[...]

Running the command above, we can see that there are many rules. Still we can see that for ftpd_t, there is some booleans available, such as ftpd_use_cifs.
There are other booleans as well, but they more likely deal with writing to Samba shares, which is not necessary here.

We can check what the boolean effectively does:

# semanage boolean -l | grep ftpd_use_cifs
ftpd_use_cifs                  (off  ,  off)  Determine whether ftpd can use CIFS used for public file transfer services.

From above, this is exactly what we need: we need to mimic the rules available for ftpd_t to browse Samba filesystems for reading.

Let's check the SELinux current policy to find out where ftpd_use_cifs is defined.
This is done through going into the sources, in /root/rpmbuild/BUILD/selinux-policy-xxx directory.

# cd /root/rpmbuild/BUILD/selinux-policy-13935d5ca9a5c6d6a7d4a9688af0cc552c2b492d
# grep -rw ftpd_use_cifs
policy/modules/contrib/ftp.te:gen_tunable(ftpd_use_cifs, false)
policy/modules/contrib/ftp.te:tunable_policy(`ftpd_use_cifs',`
policy/modules/contrib/ftp.te:tunable_policy(`ftpd_use_cifs && ftpd_anon_write',`

The grep command shows us three results: the first one is gen_tunable(ftpd_use_cifs, false) which creates the boolean.
The second one is tunable_policy(ftpd_use_cifs, ...) which is the beginning of the block defining the rules specific to enabling ftpd_use_cifs, exactly what we want.

Below is the block in question:

tunable_policy(`ftpd_use_cifs',`
        fs_read_cifs_files(ftpd_t)
        fs_read_cifs_symlinks(ftpd_t)
')

The block defines rules through interfaces. We could check more about this, but it's not really necessary.
From the names of the interfaces, we know this is what we want:

  • "fs_read_cifs_files" : allow reading cifs files
  • "fs_read_cifs_symlinks" : allow reading cifs symbolic links

4.4. Create the custom policy module

With all the information collected above, we can now create our own policy module.
We will basically create a new boolean mysqld_use_cifs that will enable rules to read CIFS filesystemd.

All this is done in the local_mysqld_use_cifs.te file below:

policy_module(local_mysqld_use_cifs, 1.0)

gen_tunable(mysqld_use_cifs, false)

gen_require(`
	type mysqld_t;
')

tunable_policy(`mysqld_use_cifs',`
        fs_read_cifs_files(mysqld_t)
        fs_read_cifs_symlinks(mysqld_t)
')

We can now compile the module and install it:

# make -f /usr/share/selinux/devel/Makefile local_mysqld_use_cifs.pp
# semodule -i local_mysqld_use_cifs.pp

4.5. Verify that the rules are present in the policy

Finally we verify that the rules are there:

# sesearch -C -A -s mysqld_t -t cifs_t -c file -p getattr
allow mysqld_t cifs_t:file { getattr ioctl lock open read }; [ mysqld_use_cifs ]:True

Since the boolean has not been enabled, the rules won't apply yet, let's enable the boolean:

# semanage boolean --modify --on mysqld_use_cifs

We are now done!


5. Example 2 - Example 2 - allowing a custom Zabbix script to execute a command

5.1. Context

In this example, we have 3rd party Zabbix agent running on the system and we want to execute some custom script /usr/local/bin/ntpcheck through Zabbix agent which internally calls systemctl status chronyd command to check the status of the service.

Red Hat Enterprise Linux ships some rules related to Zabbix (zabbix_* types), but since it's 3rd party, the policy is likely not complete.
Ideally Zabbix vendor should provide the policy, but as far as we know, it's not the case at all, leading customers to think Red Hat is responsible for Zabbix SELinux-related issues.

For demonstration purpose, in order to reproduce without having Zabbix installed on the system, the following scripts can be created on the system

  • /usr/local/bin/zabbix, mimicing running a command as zabbix_agent_t SELinux context

    #!/bin/sh
    echo "$(basename $0): $(id -Z)"
    /usr/local/bin/ntpcheck
    
  • /usr/local/bin/ntpcheck

    #!/bin/sh
    echo "$(basename $0): $(id -Z)"
    systemctl status chronyd
    

After creating the scripts, the scripts need to be made executable and have /usr/local/bin/zabbix be labeled in appropriate context to execute as zabbix_agent_t:

# chmod +x /usr/local/bin/zabbix /usr/local/bin/ntpcheck
# chcon -t zabbix_agent_exec_t /usr/local/bin/zabbix

5.2. Collect the AVCs

While running /usr/local/bin/zabbix in expected context while the system is in Permissive mode, we can see the following AVC popping up:

# semanage permissive -a zabbix_agent_t

# systemd-run --unit fake_zabbix.service /usr/local/bin/zabbix

# ausearch -m AVC,USER_AVC -ts recent | grep AVC
[...]
avc:  denied  { execute } for  pid=13612 comm="ntpcheck" name="systemctl" dev="dm-0" ino=50450229 scontext=system_u:system_r:zabbix_agent_t:s0 tcontext=system_u:object_r:systemd_systemctl_exec_t:s0 tclass=file permissive=1
avc:  denied  { execute_no_trans } for  pid=13617 comm="ntpcheck" path="/usr/bin/systemctl" dev="dm-0" ino=50450229 scontext=system_u:system_r:zabbix_agent_t:s0 tcontext=system_u:object_r:systemd_systemctl_exec_t:s0 tclass=file permissive=1
avc:  denied  { connectto } for  pid=13617 comm="systemctl" path="/run/systemd/private" scontext=system_u:system_r:zabbix_agent_t:s0 tcontext=system_u:system_r:init_t:s0 tclass=unix_stream_socket permissive=1
[...]

# semanage permissive -d zabbix_agent_t

There are more similar AVCs, all related to zabbix_agent_t. In fact, depending on what the custom script does, AVCs may vary, but for sure they will all have scontext=system_u:system_r:zabbix_agent_t line, which is the important thing here.

We can verify that there is no rule for executing systemctl, even through using a boolean:

# sesearch -C -A -s zabbix_agent_t -t systemd_systemctl_exec_t -c file -p execute
--> nothing returned

Note: on RHEL8, there is no -C option; the option is only used on RHEL7 to see rules enabled through a boolean otherwise they are hidden.

5.3. Find out what's wrong here

From above we know that Zabbix agent is not capable of executing binaries, which is fine.
This indicates that some transition must execute to let scripts executed by the agent run in some appropriate context.
Browsing the policy, we can see that zabbix_script_t context enables doing a lot of things, particularly executing the systemctl command:

# sesearch -A -s zabbix_script_t -c file -p execute
Found 17 semantic av rules:
   allow domain lib_t : file { ioctl read getattr lock map execute execute_no_trans open } ; 
   allow kern_unconfined unlabeled_t : file { ioctl read write create getattr setattr lock relabelfrom relabelto append map unlink link rename execute swapon quotaon mounton execute_no_trans execmod open audit_access } ; 
   allow devices_unconfined_type device_node : file { ioctl read write create getattr setattr lock relabelfrom relabelto append map unlink link rename execute swapon quotaon mounton execute_no_trans open audit_access } ; 
   allow unconfined_domain_type systemd_systemctl_exec_t : file { ioctl read getattr lock map execute execute_no_trans open } ; 
   allow unconfined_domain_type systemd_passwd_agent_exec_t : file { ioctl read getattr lock map execute execute_no_trans open } ; 
   [...]

We hence need to find out how to transition from zabbix_agent_t to zabbix_script_t.
For this, we can check the Transition rules, either using sesearch of sepolicy tool:

# sesearch -T -s zabbix_agent_t | grep zabbix_script_t
   type_transition zabbix_agent_t zabbix_script_exec_t : process zabbix_script_t;
# sepolicy transition -s zabbix_agent_t -t zabbix_script_t
zabbix_agent_t @ zabbix_script_exec_t --> zabbix_script_t

From above, we can see that if Zabbix agent is executing something labeled with zabbix_script_exec_t, the resulting process will become zabbix_script_t.

5.4. Create the custom context

Our demonstration script is /usr/local/bin/ntpcheck. By default, it is labeled with bin_t.
Due to having bin_t label, no transition occurs and the script will execute in the context of the caller, zabbix_agent_t, as we can see from the journal:

# journalctl -u fake_zabbix.service
[...]
[...] zabbix: system_u:system_r:zabbix_agent_t:s0
[...] ntpcheck: system_u:system_r:zabbix_agent_t:s0
[...]

We now know that /usr/local/bin/ntpcheck should be labeled with zabbix_script_exec_t instead, for the transition to occur.
Let's try and verify:

# chcon -t zabbix_script_exec_t /usr/local/bin/ntpcheck

# systemd-run --unit fake_zabbix_fixed.service /usr/local/bin/zabbix

# journalctl -u fake_zabbix_fixed.service
[...]
[...] zabbix: system_u:system_r:zabbix_agent_t:s0
[...] ntpcheck: system_u:system_r:zabbix_script_t:s0
[...]

We can also check that no new AVC popped up recently:

# ausearch -m avc -ts recent | grep AVC
<no matches>

We hence now just have to create the context persistently, so that it survives to restorecon commands.
There are 2 possibilities to achieve this:

  1. If the command (here above /usr/local/bin/ntpcheck lives in a standard place), create the context for the command only

    # semanage fcontext -a -t zabbix_script_exec_t /usr/local/bin/ntpcheck
    

    Once done, restore the label to apply the change:

    # restorecon -Fv /usr/local/bin/ntpcheck
    
  2. Otherwise, create the context as an equivalency context, based on standard Zabbix scripts location /usr/lib/zabbix/externalscripts

    # semanage fcontext -a -e /usr/lib/zabbix/externalscripts /my/custom/zabbix/scripts
    

    In the example above, we assumed /my/custom/zabbix/scripts was the location where scripts are stored.
    Once done, restore the labels to apply the change:

    # restorecon -Frv /my/custom/zabbix/scripts
    

We are now done!

SBR
Components
Tags
Article Type