I’m a Debian person. I mean, I have done the basic distro hopping over the years and have run all sort of distros as my daily driver, but my work history has dictated that I have had to learn Debian and its variants best.

The fact that I have worked mostly with Debian has also dictated that from Mandatory Access Control (MAC) solutions I have worked most with AppArmor. SELinux is something I have always thought I should learn better and have managed to deal with it by researching it when necessary, but gaps between these situations have ensured that its quirks have not sticked in my head. Now, I’m trying to remediate this and decided to write down some basics which could help me with it.

Checking SELinux state

SELinux state is checked with sestatus command.

[root@localhost]# sestatus 
SELinux status:                 enabled
SELinuxfs mount:                /sys/fs/selinux
SELinux root directory:         /etc/selinux
Loaded policy name:             targeted
Current mode:                   enforcing
Mode from config file:          enforcing
Policy MLS status:              enabled
Policy deny_unknown status:     allowed
Memory protection checking:     actual (secure)
Max kernel policy version:      33

SELinux configuration file

SELinux configuration file is found in path /etc/selinux/config. With fresh Rocky Linux installation it looks like this:

# This file controls the state of SELinux on the system.
# SELINUX= can take one of these three values:
#     enforcing - SELinux security policy is enforced.
#     permissive - SELinux prints warnings instead of enforcing.
#     disabled - No SELinux policy is loaded.
# See also:
# https://docs.fedoraproject.org/en-US/quick-docs/getting-started-with-selinux/#getting-started-with-selinux-selinux-states-and-modes
#
# NOTE: In earlier Fedora kernel builds, SELINUX=disabled would also
# fully disable SELinux during boot. If you need a system with SELinux
# fully disabled instead of SELinux running with no policy loaded, you
# need to pass selinux=0 to the kernel command line. You can use grubby
# to persistently set the bootloader to boot with selinux=0:
#
#    grubby --update-kernel ALL --args selinux=0
#
# To revert back to SELinux enabled:
#
#    grubby --update-kernel ALL --remove-args selinux
#
# SELINUXTYPE= can take one of these three values:
#     targeted - Targeted processes are protected,
#     minimum - Modification of targeted policy. Only selected processes are protected.
#     mls - Multi Level Security protection.

SELINUXTYPE=targeted
SELINUX=enforcing

There’s only two settings defined, SELINUXTYPE and SELINUX. Those are explained quite well in the comments of the file, but I’ll break down those anyways and maybe add something to that.

SELinux modes (SELINUX=<mode>)

The SELINUX setting sets the state of SELinux. There are three different modes:

enforcing - SELinux security policy is enforced.

This means that if some process tries do something that is not allowed by SELinux policy, then SELinux will prevent the action.

permissive - SELinux prints warnings instead of enforcing.

Meaning it will not prevent anything, but you can audit what it would have prevented if in enforcing state.

disabled - No SELinux policy is loaded.

Meaning SELinux is (almost) turned off. You can see in config file’s comment that selinux=0 is needed to be set in kernel command line to fully disable SELinux. One tip, if you break something enoug with SELinux that it prevents the system from booting, then setting selinux=0 during the boot allows you to fix things.

You can change the enforcing state with setenforce command, but this doesn’t stick over reboots.

[root@localhost]# setenforce permissive
[root@localhost]# getenforce 
Permissive
[root@localhost]# reboot 
...
[root@localhost]# getenforce 
Enforcing

In sestatus output you can also see that it shows the current state and mode from configuration file.

[root@localhost]# sestatus 
...
Current mode:                   enforcing
Mode from config file:          enforcing
...

SELinux type (SELINUXTYPE=<type>)

The SELINUXTYPE option sets the used SELinux policy. This is where things can start to feel a bit complicated. There are three different type options in the SELinux version of my Rocky Linux installation:

targeted - Targeted processes are protected.

But what does the “targeted processes” mean? The below quote from Oracle Linux’s documentation explains this a bit further.

A targeted policy applies access controls to a limited number of processes that are believed to be most likely to be the targets of an attack on the system. Targeted processes run in their own SELinux domain, known as a confined domain, which restricts access to files that an attacker could exploit. If SELinux detects that a targeted process is trying to access resources outside the confined domain, it denies access to those resources and logs the denial. Only specific services run in confined domains. Examples are services that listen on a network for client requests, such as httpd, named, and sshd, and processes that run as root to perform tasks on behalf of users, such as passwd. Other processes, including most user processes, run in an unconfined domain where only DAC rules apply. If an attack compromises an unconfined process, SELinux doesn’t prevent access to system resources and data.

-- https://docs.oracle.com/en/operating-systems/oracle-linux/selinux/selinux-AdministeringSELinuxPolicies.html#ol-pol-selinux

Same documentation also provides a table that shows some examples of domains.

Domain Description
init_t systemd
httpd_t HTTP daemon threads
kernel_t Kernel threads
syslogd_t journald and rsyslogd logging daemons
unconfined_t Processes that are started by Oracle Linux users run in the unconfined domain

In my head I’m still comparing this (too much propably) to AppArmor. Is domain a bit same as profile in AppArmor world? Maybe to some extend?

SELinux project wiki states:

Types

This is the primary means of determining access (this will be further discussed later). The type of a process is also referred to as its domain. By convention, a type has the suffix “_t”, such as user_t.

Which seems to say that type = domain, which is quite confusing when SELINUXTYPE=<type> sets the profile. Ther are a few more terms in SELinux terminology that are relevant here; labels and contexts.

The term label is used for the SELinux context of a file or other object on a system. Whenever a document talks about a file context or file label, both actually mean the same thing. The term comes from the SELinux permissions relabelfrom and relabelto which inform the policy if a relabel operation (change of context) is allowed from a particular label (context) or towards a particular label (context).

--https://wiki.gentoo.org/wiki/SELinux/Labels

Context (or label) of a file can be checked, for example, with stat command.

[root@localhost]# stat /etc/passwd
  File: /etc/passwd
  Size: 1015      	Blocks: 8          IO Block: 4096   regular file
Device: fd00h/64768d	Inode: 33965123    Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Context: system_u:object_r:passwd_file_t:s0
Access: 2023-09-09 17:24:21.485378335 +0300
Modify: 2023-09-09 15:32:57.229649760 +0300
Change: 2023-09-09 17:24:14.280424716 +0300
 Birth: 2023-09-09 15:32:57.229649760 +0300

I’ll try to break down what the context system_u:object_r:passwd_file_t:s0 means.

  • system_u -> User

The SELinux user identity is an identity known to the policy that is authorized for a specific set of roles, and for a specific MLS/MCS range. Each Linux user is mapped to an SELinux user via SELinux policy. This allows Linux users to inherit the restrictions placed on SELinux users. The mapped SELinux user identity is used in the SELinux context for processes in that session, in order to define what roles and levels they can enter.

-- Red Hat documentation

This is related to the fact that SELinux can confine users.

Each Linux user is mapped to an SELinux user according to the rules in the SELinux policy. Administrators can modify these rules by using the semanage login utility or by assigning Linux users directly to specific SELinux users. Therefore, a Linux user has the restrictions of the SELinux user to which it is assigned. When a Linux user that is assigned to an SELinux user launches a process, this process inherits the SELinux user’s restrictions, unless other rules specify a different role or type.

By default, all Linux users in Red Hat Enterprise Linux, including users with administrative privileges, are mapped to the unconfined SELinux user unconfined_u. You can improve the security of the system by assigning users to SELinux confined users.

-- Red Hat documentation

The command semanage can be used to view these user mappings.

[root@localhost]# semanage login -l

Login Name           SELinux User         MLS/MCS Range        Service

__default__          unconfined_u         s0-s0:c0.c1023       *
root                 unconfined_u         s0-s0:c0.c1023       *
  • object_r -> Role

Part of SELinux is the Role-Based Access Control (RBAC) security model. The role is an attribute of RBAC. SELinux users are authorized for roles, and roles are authorized for domains. The role serves as an intermediary between domains and SELinux users. The roles that can be entered determine which domains can be entered; ultimately, this controls which object types can be accessed. This helps reduce vulnerability to privilege escalation attacks.

-- Red Hat documentation

  • passwd_file_t -> Type

The type is an attribute of Type Enforcement. The type defines a domain for processes, and a type for files. SELinux policy rules define how types can access each other, whether it be a domain accessing a type, or a domain accessing another domain. Access is only allowed if a specific SELinux policy rule exists that allows it.

-- Red Hat documentation

  • s0 -> Level

The level is an attribute of MLS and MCS. An MLS range is a pair of levels, written as lowlevel-highlevel if the levels differ, or lowlevel if the levels are identical (s0-s0 is the same as s0). Each level is a sensitivity-category pair, with categories being optional. If there are categories, the level is written as sensitivity:category-set. If there are no categories, it is written as sensitivity.

If the category set is a contiguous series, it can be abbreviated. For example, c0.c3 is the same as c0,c1,c2,c3. The /etc/selinux/targeted/setrans.conf file maps levels (s0:c0) to human-readable form (that is CompanyConfidential). Do not edit setrans.conf with a text editor: use the semanage command to make changes. Refer to the semanage(8) manual page for further information. In Red Hat Enterprise Linux, targeted policy enforces MCS, and in MCS, there is just one sensitivity, s0. MCS in Red Hat Enterprise Linux supports 1024 different categories: c0 through to c1023. s0-s0:c0.c1023 is sensitivity s0 and authorized for all categories.

-- Red Hat documentation

A bit more about MLS in mls policy section further below.

Processes also have SELinux labels. You can check the label of a process with ps command

[root@localhost]# ps axZ
LABEL                               PID TTY      STAT   TIME COMMAND
system_u:system_r:init_t:s0           1 ?        Ss     0:02 /usr/lib/systemd/systemd --switched-root --system --deserialize 31
system_u:system_r:kernel_t:s0         2 ?        S      0:00 [kthreadd]
system_u:system_r:kernel_t:s0         3 ?        I<     0:00 [rcu_gp]
...

minimum - Modification of targeted policy. Only selected processes are protected.

But what are the selected processes? Fedora’s Wiki gives some details for this.

In Fedora 10 we introduced selinux-policy-minimum. Minimum policy is built exactly the same as targeted policy, but installs ONLY the base policy package and the unconfined.pp. All of the SELinux policy modules from the targeted policy are in the selinux-policy-minimum RPM package but they are not compiled and loaded into the kernel in the post install.

Pretty much everything on this system runs as initrc_t or unconfined_t so all of the domains are unconfined.

mls - Multi Level Security protection.

The MLS is a whole different can of worms by itself. You can read its details from Red Hat’s documentation, but here is some snippets from there.

The Multi-Level Security technology refers to a security scheme that enforces the Bell-La Padula Mandatory Access Model. Under MLS, users and processes are called subjects, and files, devices, and other passive components of the system are called objects. Both subjects and objects are labeled with a security level, which entails a subject’s clearance or an object’s classification. Each security level is composed of a sensitivity and a category, for example, an internal release schedule is filed under the internal documents category with a confidential sensitivity.

The mentioned Bell-LaPadula model can be summarized in idea of no read up and no write down. It’s used in security clearance world and image in the documentation shows how this can be relevant within the Linux filesystem.

How SELinux maps specific domain to specific process?

This has baffled me for quite a long time and I still might not understand it fully. In AppArmor it (usually) is really clear where a process gets the profile it’s confined by and you can find the profile in its own file under /etc/apparmor.d/. With SELinux I knew that apache is confined by domain httpd_t, but what maps Apache to that domain? Of course Apache service lives by the name httpd in Red Hat ecosystem, so it could have been just the name, but same applies to Nginx, so it’s not that simple.

[root@localhost ansible]# ps auxZ|grep -i nginx
system_u:system_r:httpd_t:s0    root       14980  0.0  0.0  10108   952 ?        Ss   13:46   0:00 nginx: master process /usr/sbin/nginx
system_u:system_r:httpd_t:s0    nginx      14981  0.0  0.1  13908  4764 ?        S    13:46   0:00 nginx: worker process
system_u:system_r:httpd_t:s0    nginx      14982  0.0  0.1  13908  4740 ?        S    13:46   0:00 nginx: worker process

The ps outbut above is after installing nginx and starting the process. No other changes were made, but its SELinux label shows httpd_t as its domain.

There’s a file /etc/selinux/targeted/contexts/files/file_contexts which tells which context is assigned to specific files and this seems to be part of the equation. The /sbin/nginx file has the following context:

[root@localhost ansible]# ls -laZ /sbin/nginx 
-rwxr-xr-x. 1 root root system_u:object_r:httpd_exec_t:s0 1329000 Apr 21 13:43 /sbin/nginx

SELinux seems to have a thing called entrypoints which can allow a file with a specific SELinux type to serve as an entrypoint to specific domain (domain transition). For example, the httpd_t SELinux type can be entered via the “httpd_exec_t” file type.

Usual stuff

Everything above already contains some complex elements like the MLS, SELinux users and groups etc, but 99% of time it seems that all tutorials and instructions ignores those and focuses on the following:

  • Using the default targeted profile
  • Focusing only in the type of a context. E.g. httpd_exec_t in system_u:object_r:httpd_exec_t:s0
  • Fixing issues that come along

This is most likely the sane approach in most of the situations as it allows people to do something else than focus on SELinux while still having many of its security benefits.

Tools

Ensure that you have the following packages installed:

  • policycoreutils-python-utils - Provides semanage command.
  • setools-console - Provides seinfo

SELinux aware commands

Commands that are SELinux “aware” usually have -Z option to print SELinux labels. For example, id, ss, ps, and ls commands.

Booleans

Booleans in SELinux are predefined settings that have two states - on or off.

Different booleans can be listed with command getsebool -a or with semanage boolean --list.

Setting a boolean value can be done like this semanage boolean --modify --on http_allow_homedirs or setsebool -P httpd_enable_homedirs 1.

The values changed by the administrator can be listed with command semanage boolean -l -C.

Troubleshooting

There are multiple tools to solve SELinux denied events.

audit2allow

The audit2allow tool should be preinstalled with RHEL system. Some examples:

  • audit2allow -w -a shows all the denied messages.
  • ausearch -m AVC | audit2allow -a -M test_rule && semodule -i test_rule.pp creates test_rule module package (test_rule.pp) and enables it.
    • Check if module was loaded semodule -l | grep test_rule
    • Remove module semodule -r test_rule

Policy packages allow creating a seperate module file for an application instead of having it in the main policy. With semodule -l you can see that there are quite a many modules by default.

Setroubleshoot

Setroubleshoot is a service that gives instructions of how to resolve SELinux issues.

dnf install setroubleshoot
systemctl enable setroubleshootd
systemctl start setroubleshootd
service auditd restart # systemctl does not work here

As an example, I changed Nginx configuration to listen in port 25000. I know that SELinux policy doesn’t allow http service to bind this port. Starting the service gives the expected error.

Sep 10 17:39:46 localhost.localdomain nginx[17269]: nginx: [emerg] bind() to 0.0.0.0:25000 failed (13: Permission denied)

Checking from messages log I can see the related message from setroubleshoot.

Sep 10 17:36:54 localhost setroubleshoot[17168]: SELinux is preventing /usr/sbin/nginx from name_bind access on the tcp_socket port 25000. For complete SELinux messages run: sealert -l 7ddd2701-1367-40f9-ba0c-be6281a2e23b

By running the sealert command I get actual instructions of how to fix this problem.

[root@localhost]# sealert -l 7ddd2701-1367-40f9-ba0c-be6281a2e23b
SELinux is preventing /usr/sbin/nginx from name_bind access on the tcp_socket port 25000.

*****  Plugin bind_ports (92.2 confidence) suggests   ************************

If you want to allow /usr/sbin/nginx to bind to network port 25000
Then you need to modify the port type.
Do
# semanage port -a -t PORT_TYPE -p tcp 25000
    where PORT_TYPE is one of the following: http_cache_port_t, http_port_t, jboss_management_port_t, jboss_messaging_port_t, ntop_port_t, puppet_port_t.
....

Lets try the recommended fix.

[root@localhost]# semanage port -a -t http_port_t -p tcp 25000
[root@localhost]# semanage port -l | grep http_port_t
http_port_t                    tcp      25000, 80, 81, 443, 488, 8008, 8009, 8443, 9000
pegasus_http_port_t            tcp      5988
[root@localhost]# 
[root@localhost]# systemctl restart nginx
[root@localhost]# systemctl status nginx
● nginx.service - The nginx HTTP and reverse proxy server
     Loaded: loaded (/usr/lib/systemd/system/nginx.service; disabled; preset: disabled)
     Active: active (running) since Sun 2023-09-10 17:48:20 EEST; 3s ago

An example of how to remove port 25000 from http_port type:

[root@localhost]# semanage port -d -t http_port_t -p tcp 25000
[root@localhost]# systemctl restart nginx
Job for nginx.service failed because the control process exited with error code.
See "systemctl status nginx.service" and "journalctl -xeu nginx.service" for details.

More complex stuff

Managin SELinux users and roles

Red Hat documentation has a nice list with details for different users and roles: https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/using_selinux/managing-confined-and-unconfined-users_using-selinux.

When adding a new user wit useradd command, it’s enough to specify -Z <SELinux user.

[root@localhost]# useradd -Z staff_u newuser
[root@localhost]# semanage login -l

Login Name           SELinux User         MLS/MCS Range        Service

__default__          unconfined_u         s0-s0:c0.c1023       *
newuser              staff_u              s0-s0:c0.c1023       *
root                 unconfined_u         s0-s0:c0.c1023       *

With an existing user semanage can be used.

[root@localhost ansible]# semanage login -a -s staff_u ansible
[root@localhost ansible]# semanage login -l

Login Name           SELinux User         MLS/MCS Range        Service

__default__          unconfined_u         s0-s0:c0.c1023       *
ansible              staff_u              s0-s0:c0.c1023       *
newuser              staff_u              s0-s0:c0.c1023       *
root                 unconfined_u         s0-s0:c0.c1023       *

Confining custom application

Red Hat has document Writing a custom SELinux policy which gives an example of how to write custom policy for a self made application.

I decided to test “writing” an own policy for a simple python app.

[root@localhost]# cat /usr/sbin/testapp 
#!/usr/bin/env python

with open("/etc/passwd") as f:
    for l in f.readlines():
        print(l)

with open("/etc/shadow") as f:
    for l in f.readlines():
        print(l)

Some dependencies are needed for policy creation:

[root@localhost]# dnf install policycoreutils-devel binutils rpm-build
[root@localhost]# mkdir test && cd test
[root@localhost]# sepolicy generate --init /usr/sbin/testapp
nm: /usr/sbin/testapp: file format not recognized
Created the following files:
/home/ansible/test/testapp.te # Type Enforcement file
/home/ansible/test/testapp.if # Interface file
/home/ansible/test/testapp.fc # File Contexts file
/home/ansible/test/testapp_selinux.spec # Spec file
/home/ansible/test/testapp.sh # Setup Script

The sepolicy generate command creates necessary files for installing the application policy. Files are created to current working directory.

By checking the created testapp.te file I can see that it sets the domain to permissive mode by default.

[root@localhost]# cat testapp.te 
policy_module(testapp, 1.0.0)

...
permissive testapp_t;
...

I can install the policy by running the script which was generated by the sepolicy command.

[root@localhost]# ./testapp.sh
Building and Loading Policy
+ make -f /usr/share/selinux/devel/Makefile testapp.pp
make: 'testapp.pp' is up to date.
+ /usr/sbin/semodule -i testapp.pp
...snip...

Checking if the policy was loaded:

[root@localhost]# semodule -l |grep -P 'test'
testapp
[root@localhost]# ls -Z /usr/sbin/testapp 
system_u:object_r:testapp_exec_t:s0 /usr/sbin/testapp

Test run:

runcon system_u:system_r:testapp_t:s0 testapp

While grepping ps auxZ output I can see this.

system_u:system_r:testapp_t:s0  root      152081  0.0  0.2   9024  5920 pts/0    R+   18:59   0:00 python /sbin/testapp

I’m using runcon command which allows me to run program under a specific context as I did not get how I could make the program run as confined when executed directly by the user. Propably I would need to use different policy type with sepolicy to do that.

After this I can check which things should be allowed for the application:

[root@localhost]# ausearch -m AVC |grep testapp| audit2allow -a -R test_rule 

require {
	type testapp_t;
	class capability dac_read_search;
}

#============= testapp_t ==============
allow testapp_t self:capability dac_read_search;
auth_read_passwd_file(testapp_t)
auth_read_shadow(testapp_t)
corecmd_exec_bin(testapp_t)
corecmd_mmap_bin_files(testapp_t)

For testing purposes I added this to previously generated testapp.te file, but removed the auth_read_shadow(testapp_t), and commented out the permmissive statement. Then I re-run the ./testapp.sh script. Not really sure what happend, but it gives no output. No “Permission Denied” related to /etc/shadow or anything. If I rerun audit2allow it gives suggestion about /etc/shadow, which makes sense, but nothing regarding to writing console output.

Well, as a more realistic test I created a systemd service testapp.service:

[Unit]
Description=Test test

[Service]
Type=oneshot
ExecStart=/usr/sbin/testapp

[Install]
WantedBy=multi-user.target

When starting this service everything is “working” as expected.

[root@localhost]# systemctl start testapp
Job for testapp.service failed because the control process exited with error code.
See "systemctl status testapp.service" and "journalctl -xeu testapp.service" for details.
[root@localhost]# journalctl -u testapp
...
Sep 10 19:38:50 localhost.localdomain testapp[183985]: Traceback (most recent call last):
Sep 10 19:38:50 localhost.localdomain testapp[183985]:   File "/usr/sbin/testapp", line 7, in <module>
Sep 10 19:38:50 localhost.localdomain testapp[183985]:     with open("/etc/shadow") as f:
Sep 10 19:38:50 localhost.localdomain testapp[183985]: PermissionError: [Errno 13] Permission denied: '>

As a final test, I added the auth_read_shadow(testapp_t) to policy and re-installed it once again. Now there are no errors as expected and the application can only read those two files.

[root@localhost]# systemctl start testapp
[root@localhost]#