introduction to SELinux
(Written by Paul Cobbaut, https://github.com/paulcobbaut/, with contributions by: Alex M. Schapelle, https://github.com/zero-pytagoras/)
Security Enhanced Linux or SELinux is a set of modifications to the Linux kernel developed by the United States National Security Agency (NSA) to provide a variety of security policies. SELinux was released as open source at the end of 2000. Since kernel version 2.6 it is an integrated part of Linux.
SELinux offers security! Several recent and highly publicized security vulnerabilities in the Linux world (like e.g. Heartbleed) would not be able to exploit a system with SELinux enabled.
SELinux is based on the principle of Mandatory Access control and basically answers the question "Is thing allowed to perform action on object?", e.g. "is the web server allowed network access on a remote database?" The default answer is no, but you can configure SELinux policies to allow specific actions. Even when a file received chmod 777
, SELinux can still prevent applications from accessing it! SELinux does this by placing users in roles that represent a security context.
Administrators have very strict control on access permissions granted to roles. This may sound quite daunting, since every single action that should be allowed needs to be explicitly configured. However, distributions that support SELinux already come with a set of predefined policies that cover most common use cases.
SELinux is present in many recent Linux distributions. Red Hat Enterprise Linux (and other distros within the EL family like AlmaLinux, Rocky Linux, Fedora, etc.) have SELinux installed by default. Debian has SELinux available, but it is not installed or enabled by default. For Ubuntu, Canonical has chosen AppArmor instead of SELinux for security hardening.
Since SELinux is a default part of Red Hat Enterprise Linux, we'll use that in the examples in this chapter.
Further reading:
- RHEL 9 documentation: Using SELinux: https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/using_selinux/
- Setting up SELinux on Debian: https://wiki.debian.org/SELinux
- SELinux project wiki: https://selinuxproject.org/
basic operation
selinux modes
SELinux knows three modes: enforcing, permissive and disabled. The enforcing mode will enforce policies, and may deny access based on SELinux policies. Any action that is denied is logged. The permissive mode will not enforce policies, but still log actions that would have been denied in enforcing mode. In the disabled mode, SELinux is inactive.
Use the getenforce
command to see the current mode and setenforce
(as superuser) to change it.
[student@el ~]$ getenforce
Permissive
[student@el ~]$ sudo setenforce 1
[student@el ~]$ getenforce
Enforcing
More elaborate information about the current SELinux status can be found with the sestatus
command.
[student@el ~]$ sudo setenforce 0
[student@el ~]$ sestatus
SELinux status: enabled
SELinuxfs mount: /sys/fs/selinux
SELinux root directory: /etc/selinux
Loaded policy name: targeted
Current mode: permissive
Mode from config file: permissive
Policy MLS status: enabled
Policy deny_unknown status: allowed
Memory protection checking: actual (secure)
Max kernel policy version: 33
[student@el ~]$ sudo setenforce 1
[student@el ~]$ 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: permissive
Policy MLS status: enabled
Policy deny_unknown status: allowed
Memory protection checking: actual (secure)
Max kernel policy version: 33
To make the change permanent, edit the /etc/selinux/config
file and set the SELINUX
variable to the desired value (which should of course be enforcing
).
[student@el ~]$ grep '^SELINUX' /etc/selinux/config
SELINUX=permissive
SELINUXTYPE=targeted
[student@el ~]$ sudo sed -i 's/^SELINUX=.*/SELINUX=enforcing/' /etc/selinux/config
[student@el ~]$ grep '^SELINUX' /etc/selinux/config
SELINUX=enforcing
SELINUXTYPE=targeted
After permanently changing the SELinux mode to enforcing, a reboot is highly recommended. SELinux depends on objects being "labeled" correctly. While inactive, labeling is turned off, which may result in SELinux not behaving correctly after it is turned on. On the next boot, SELinux will relabel the entire system.
The SELINUXTYPE
variable in the /etc/selinux/config
file specifies the policy type in use and can take the values targeted
or mls
. The targeted
policy is the most common policy type. It protects targeted (i.e. specified) processes, but allows some processes and users (e.g. root
) to run unconfined. In some situations, like highly regulated environments with strict compliance requirements, or where highly sensitive data is managed, this is insuffficient. In these cases, the mls
(Multi-Level Security) policy can be used. When this policy is applied, unconfined processes are not allowed, which would imply that even root
can't access every object or perform every action they want.
logging
When in enforcing mode, SELinux will log all denied actions. These logs can be found in /var/log/audit/audit.log
. The ausearch
command can be used to search these logs. You need root privileges to read the audit logs.
[student@el ~]$ sudo ausearch -m AVC,USER_AVC,SELINUX_ERR,USER_SELINUX_ERR -ts today
----
time->Fri Oct 11 14:44:22 2024
type=PROCTITLE msg=audit(1728657862.580:608): proctitle=2F7573722F62696E2F7368002F7573722F6C69622F4E6574776F726B4D616E616765722F646973706174636865722E642F32302D6368726F6E792D646863700065746831007570
type=SYSCALL msg=audit(1728657862.580:608): arch=c000003e syscall=257 success=yes exit=3 a0=ffffff9c a1=558e921a2790 a2=0 a3=0 items=0 ppid=620 pid=3395 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="20-chrony-dhcp" exe="/usr/bin/bash" subj=system_u:system_r:NetworkManager_dispatcher_chronyc_t:s0 key=(null)
type=AVC msg=audit(1728657862.580:608): avc: denied { open } for pid=3395 comm="20-chrony-dhcp" path="/etc/sysconfig/network-scripts/ifcfg-eth1" dev="sda2" ino=612 scontext=system_u:system_r:NetworkManager_dispatcher_chronyc_t:s0 tcontext=unconfined_u:object_r:user_tmp_t:s0 tclass=file permissive=1
type=AVC msg=audit(1728657862.580:608): avc: denied { dac_read_search } for pid=3395 comm="20-chrony-dhcp" capability=2 scontext=system_u:system_r:NetworkManager_dispatcher_chronyc_t:s0 tcontext=system_u:system_r:NetworkManager_dispatcher_chronyc_t:s0 tclass=capability permissive=1
----
time->Fri Oct 11 16:22:01 2024
type=PROCTITLE msg=audit(1728663721.409:30): proctitle=2F7573722F62696E2F7368002F7573722F6C69622F4E6574776F726B4D616E616765722F646973706174636865722E642F32302D6368726F6E792D646863700065746831007570
type=SYSCALL msg=audit(1728663721.409:30): arch=c000003e syscall=257 success=no exit=-13 a0=ffffff9c a1=55e6dfb85790 a2=0 a3=0 items=0 ppid=617 pid=675 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="20-chrony-dhcp" exe="/usr/bin/bash" subj=system_u:system_r:NetworkManager_dispatcher_chronyc_t:s0 key=(null)
type=AVC msg=audit(1728663721.409:30): avc: denied { dac_override } for pid=675 comm="20-chrony-dhcp" capability=1 scontext=system_u:system_r:NetworkManager_dispatcher_chronyc_t:s0 tcontext=system_u:system_r:NetworkManager_dispatcher_chronyc_t:s0 tclass=capability permissive=0
type=AVC msg=audit(1728663721.409:30): avc: denied { dac_read_search } for pid=675 comm="20-chrony-dhcp" capability=2 scontext=system_u:system_r:NetworkManager_dispatcher_chronyc_t:s0 tcontext=system_u:system_r:NetworkManager_dispatcher_chronyc_t:s0 tclass=capability permissive=0
----
[...some output omitted...]
Or, alternatively, you can search the /var/log/audit/audit.log
file for the string denied
:
[student@el ~]$ sudo grep denied /var/log/audit/audit.log
type=AVC msg=audit(1728657862.580:608): avc: denied { dac_read_search } for pid=3395 comm="20-chrony-dhcp" capability=2 scontext=system_u:system_r:NetworkManager_dispatcher_chronyc_t:s0 tcontext=system_u:system_r:NetworkManager_dispatcher_chronyc_t:s0 tclass=capability permissive=1
type=AVC msg=audit(1728657862.580:608): avc: denied { open } for pid=3395 comm="20-chrony-dhcp" path="/etc/sysconfig/network-scripts/ifcfg-eth1" dev="sda2" ino=612 scontext=system_u:system_r:NetworkManager_dispatcher_chronyc_t:s0 tcontext=unconfined_u:object_r:user_tmp_t:s0 tclass=file permissive=1
/sys/fs/selinux
When selinux is active, there is a new virtual file system (comparable to /proc
and /dev
) mounted at /sys/fs/selinux
.
[student@el ~]$ mount | grep selinux
selinuxfs on /sys/fs/selinux type selinuxfs (rw,nosuid,noexec,relatime)
[student@el ~]$ ls -l /sys/fs/selinux/
total 0
-rw-rw-rw-. 1 root root 0 Oct 11 16:22 access
dr-xr-xr-x. 2 root root 0 Oct 11 16:22 avc
dr-xr-xr-x. 2 root root 0 Oct 11 16:21 booleans
-rw-r--r--. 1 root root 0 Oct 11 16:22 checkreqprot
dr-xr-xr-x. 137 root root 0 Oct 11 16:21 class
--w-------. 1 root root 0 Oct 11 16:22 commit_pending_bools
-rw-rw-rw-. 1 root root 0 Oct 11 16:22 context
-rw-rw-rw-. 1 root root 0 Oct 11 16:22 create
-r--r--r--. 1 root root 0 Oct 11 16:22 deny_unknown
--w-------. 1 root root 0 Oct 11 16:22 disable
-rw-r--r--. 1 root root 0 Oct 11 16:22 enforce
dr-xr-xr-x. 2 root root 0 Oct 11 16:22 initial_contexts
-rw-------. 1 root root 0 Oct 11 16:22 load
-rw-rw-rw-. 1 root root 0 Oct 11 16:22 member
-r--r--r--. 1 root root 0 Oct 11 16:22 mls
crw-rw-rw-. 1 root root 1, 3 Oct 11 16:22 null
-r--r--r--. 1 root root 0 Oct 11 16:22 policy
dr-xr-xr-x. 2 root root 0 Oct 11 16:22 policy_capabilities
-r--r--r--. 1 root root 0 Oct 11 16:22 policyvers
-r--r--r--. 1 root root 0 Oct 11 16:22 reject_unknown
-rw-rw-rw-. 1 root root 0 Oct 11 16:22 relabel
dr-xr-xr-x. 2 root root 0 Oct 11 16:22 ss
-r--r--r--. 1 root root 0 Oct 11 16:22 status
-rw-rw-rw-. 1 root root 0 Oct 11 16:22 user
--w--w--w-. 1 root root 0 Oct 11 16:22 validatetrans
Although some files in /sys/fs/selinux
appear wih size 0, they often contain a boolean value (or rather, a 0 to denote false or 1 to denote true). Check /selinux/enforce
to see if selinux is running in enforced mode.
[student@el ~]$ ls -l /sys/fs/selinux/enforce
-rw-r--r--. 1 root root 0 Oct 11 16:22 /sys/fs/selinux/enforce
[student@el ~]$ cat !$
cat /sys/fs/selinux/enforce
1
DAC vs MAC
Standard Unix permissions use Discretionary Access Control to set permissions on files. This means that a user that owns a file, can make it world readable and writeable by typing chmod 777 $file
.
With SELinux the kernel will enforce Mandatory Access Control which strictly controls what processes or threads can do with files (superseding DAC). Processes are confined by the kernel to the minimum access they require.
SELinux MAC is about labeling and type enforcing! Files, processes, etc are all labeled with an SELinux context. For files, these are extended attributes, for processes this is managed by the kernel.
The format of the labels is as follows:
We'll elaborate on these concepts in the following sections, but in short:
-
The user may be e.g.
root
,staff_u
,user_u
,system_u
,guest_u
, etc. The policy maps each Linux user to one of these SELinux users. -
The role can be e.g.
unconfined_r
,staff_r
,user_r
,system_r
, etc. The policy maps each Linux user to one of these SELinux roles. -
Each SELinux role corresponds to an SELinux type, which determines what access rights are given. Possible values are e.g.
unconfined_t
,staff_t
,user_t
, etc. -
The level is only used in
MLS
(Multi-Level Security) policies, where it denotes the security clearance level from thes0
level (equivalent to unclassified) tos15
(top secret). Users can only access objects at their own level or lower.
ls -Z
To see the DAC permissions on a file, use ls -l
to display user and group owner
and permissions.
For MAC permissions there is a new option added to ls
: -Z
or in the long form --context
.
When a file is created on the filesystem, it is assigned a label according to some rules. The ls -Z
command will show the label of the file. For example, if a normal user creates a file in their home directory, it will be labeled with the user_home_t
type:
[student@el ~]$ touch owned_by_student.txt
[student@el ~]$ ls -lZ
total 0
-rw-r--r--. 1 student student unconfined_u:object_r:user_home_t:s0 0 Oct 11 18:17 owned_by_student.txt
When you do the same as the root user in the /root
directory, the file will be labeled with the admin_home_t
type:
[student@el ~]$ sudo touch /root/owned_by_root.txt
[student@el ~]$ sudo ls -lZ /root
total 0
-rw-r--r--. 1 root root unconfined_u:object_r:admin_home_t:s0 0 Oct 11 18:16 owned_by_root.txt
In /tmp
the file will be labeled with the user_tmp_t
type:
[student@el ~]$ touch /tmp/tempfile.txt
[student@el ~]$ ls -lZ /tmp/tempfile.txt
-rw-r--r--. 1 student student unconfined_u:object_r:user_tmp_t:s0 0 Oct 11 18:20 /tmp/tempfile.txt
... and so on! The rules are kept in /etc/selinux/targeted/contexts/files/
. For example, the rule that determines the label for files in the home directory of a user can be found in file_contexts.homedirs
:
So paths that match the regular expression /home/[^/]+/.+
will be labeled with the user_home_t
type.
When selinux is not running, you can list SELinux labels by showing extended file attributes with the command getfattr
(provided by the attr
package on both Debian and EL systems):
[student@el ~]$ sudo getfattr --dump --match . /root/owned_by_root.txt
getfattr: Removing leading '/' from absolute path names
# file: root/owned_by_root.txt
security.selinux="unconfined_u:object_r:admin_home_t:s0"
[student@el ~]$ getfattr -d -m . ./owned_by_student.txt
# file: owned_by_student.txt
security.selinux="unconfined_u:object_r:user_home_t:s0"
[student@el ~]$ getfattr -d -n security.selinux ./owned_by_student.txt
# file: owned_by_student.txt
security.selinux="unconfined_u:object_r:user_home_t:s0"
The option --dump
or -d
will show all values selected by the option --match
or -m .
(matching pattern .
which means all) or --name
/ -n
(which selects only the named attribute).
the -Z option
Other commands have also been extended with the -Z
(with capital Z
!) and --context
option. For example:
mkdir -Z
: apply default SELinux context to each created directorycp -Z
,mv -Z
: set SELinux security context of destination file to default typeps -Z
: show SELinux security context of processesss -Z
: show SELinux security context of processes (like-p
, needs superuser privileges)- ...
identity
The SELinux Identity of a user is distinct from the user ID. An identity is part of a security context, and (via domains) determines what you can do. The screenshot shows user student
having identity unconfined_u
.
role
The SELinux role defines the domains that can be used. A role
is denied to enter a domain, unless the role
is explicitely authorized to do so.
To list the mappings between SELinux identities/users and roles, execute the command semanage user -l
as root:
[student@el ~]$ sudo semanage user -l
Labeling MLS/ MLS/
SELinux User Prefix MCS Level MCS Range SELinux Roles
guest_u user s0 s0 guest_r
root user s0 s0-s0:c0.c1023 staff_r sysadm_r system_r unconfined_r
staff_u user s0 s0-s0:c0.c1023 staff_r sysadm_r system_r unconfined_r
sysadm_u user s0 s0-s0:c0.c1023 sysadm_r
system_u user s0 s0-s0:c0.c1023 system_r unconfined_r
unconfined_u user s0 s0-s0:c0.c1023 system_r unconfined_r
user_u user s0 s0 user_r
xguest_u user s0 s0 xguest_r
type (or domain)
An SELinux type determines what a process can do. The example below shows the first ten processes running. The process with ID (PID) 1 is systemd
, running in domain init_t
. The other processes in the list are running in domain kernel_t
.
[student@el ~]$ ps -eZ | head
LABEL PID TTY TIME CMD
system_u:system_r:init_t:s0 1 ? 00:00:00 systemd
system_u:system_r:kernel_t:s0 2 ? 00:00:00 kthreadd
system_u:system_r:kernel_t:s0 3 ? 00:00:00 rcu_gp
system_u:system_r:kernel_t:s0 4 ? 00:00:00 rcu_par_gp
system_u:system_r:kernel_t:s0 5 ? 00:00:00 slub_flushwq
system_u:system_r:kernel_t:s0 6 ? 00:00:00 netns
system_u:system_r:kernel_t:s0 8 ? 00:00:00 kworker/0:0H-events_highpri
system_u:system_r:kernel_t:s0 10 ? 00:00:00 mm_percpu_wq
system_u:system_r:kernel_t:s0 12 ? 00:00:00 rcu_tasks_kthre
The security context for processes visible in /proc
defines both the type (of the file in /proc
) and the domain (of the running process). Let's take a look at the systemd
process and /proc/1/
.
The init process runs in domain init_t
.
The /proc/1/
directory, which identifies the init
process, also has type init_t
. This is not a coincidence!
The SELinux type is similar to an selinux domain
, but refers to directories and files instead of processes.
Hundreds of binaries also have a type:
[student@el ~]$ ls -lZ /usr/sbin/user* /usr/sbin/sem* /usr/sbin/ip
-rwxr-xr-x. 1 root root system_u:object_r:ifconfig_exec_t:s0 774136 Apr 30 20:24 /usr/sbin/ip
-rwxr-xr-x. 1 root root system_u:object_r:semanage_exec_t:s0 41614 Apr 3 2024 /usr/sbin/semanage
-rwxr-xr-x. 1 root root system_u:object_r:semanage_exec_t:s0 32928 Apr 3 2024 /usr/sbin/semodule
-rwxr-xr-x. 1 root root system_u:object_r:useradd_exec_t:s0 141144 Sep 26 2023 /usr/sbin/useradd
-rwxr-xr-x. 1 root root system_u:object_r:useradd_exec_t:s0 90968 Sep 26 2023 /usr/sbin/userdel
-rwxr-xr-x. 1 root root system_u:object_r:useradd_exec_t:s0 132784 Sep 26 2023 /usr/sbin/usermod
Ports also have a context, which can be queried with ss
with option -Z
. Just like with -p
, you need superuser privileges to see the output. You don't need to pipe the output through cut
, we do it here for brevity.
[student@el ~]$ sudo ss -tlnZ | cut -c28-41,59-
Address:Port Process
0.0.0.0:22 users:(("sshd",pid=640,proc_ctx=system_u:system_r:sshd_t:s0-s0:c0.c1023,fd=3))
0.0.0.0:111 users:(("rpcbind",pid=533,proc_ctx=system_u:system_r:rpcbind_t:s0,fd=4),("systemd",pid=1,proc_ctx=system_u:system_r:init_t:s0,fd=31))
0.0.0.0:3306 users:(("mariadbd",pid=741,proc_ctx=system_u:system_r:mysqld_t:s0,fd=21))
[::]:22 users:(("sshd",pid=640,proc_ctx=system_u:system_r:sshd_t:s0-s0:c0.c1023,fd=4))
[::]:111 users:(("rpcbind",pid=533,proc_ctx=system_u:system_r:rpcbind_t:s0,fd=6),("systemd",pid=1,proc_ctx=system_u:system_r:init_t:s0,fd=34))
You can also get a list of ports that are managed by SELinux. In the example below, we show the ports 22 (SSH), 80 (HTTP), 443 (HTTPS), and 3306 (MySQL/MariaDB). Try it yourself without piping to grep
to see all ports.
[student@el ~]$ sudo semanage port -l | grep '\b\(22\|80\|443\|3306\)\b'
http_port_t tcp 80, 81, 443, 488, 8008, 8009, 8443, 9000
mysqld_port_t tcp 1186, 3306, 63132-63164
ssh_port_t tcp 22
managing file contexts
chcon
Use chcon
to change the selinux security context.
Let's say that you want to share a directory /var/www/html/fileshare
both through Apache and FTP. Normally, the files in /var/www/html
are labeled with the httpd_sys_content_t
type. This type is not allowed to be shared through FTP. You can change the type of the directory and contents to public_content_t
with the chcon
command:
[student@el ~]$ cd /var/www/html/
[student@el html]$ sudo mkdir fileshare
[student@el html]$ sudo touch fileshare/README.md
[student@el html]$ ls -ldZ fileshare/
drwxr-xr-x. 2 root root unconfined_u:object_r:httpd_sys_content_t:s0 23 Oct 14 07:43 fileshare/
[student@el html]$ ls -lZ fileshare/
-rw-r--r--. 1 root root unconfined_u:object_r:httpd_sys_content_t:s0 0 Oct 14 07:43 README.md
14 07:43 fileshare/
[student@el html]$ sudo chcon -R -t public_content_t fileshare/
[student@el html]$ ls -ldZ fileshare/
drwxr-xr-x. 2 root root unconfined_u:object_r:public_content_t:s0 23 Oct 14 07:43 fileshare/
[student@el html]$ ls -lZ fileshare/
total 0
-rw-r--r--. 1 root root unconfined_u:object_r:public_content_t:s0 0 Oct 14 07:43 README.md
Option -t
specifies the new context, and -R
makes the change recursive, i.e. will also change the context of all files and directories in the specified directory.
Be sure to read man chcon
.
an example
Let's elaborate on the previous example. The Apache2 webserver is by default targeted with SELinux. Any file created in /var/www/html
will by
default get the httpd_sys_content_t
type, as you could see in that example.
Files created elsewhere do not get this type. Let's say that you created an index page for the website running on this server in the home directory of a user, and then you move it to /var/www/html
.
If you would try to access the website served by Apache2, you would expect to see the contents of the file. However:
[student@el ~]$ sudo systemctl is-active httpd
active
[student@el ~]$ curl http://localhost/
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>403 Forbidden</title>
</head><body>
<h1>Forbidden</h1>
<p>You don't have permission to access this resource.</p>
</body></html>
[student@el ~]$ ls -l /var/www/html/
total 4
drwxr-xr-x. 2 root root 23 Oct 14 07:43 fileshare
-rw-r--r--. 1 student student 93 Oct 14 07:48 index.html
The index file is world readable, so it should be served, but we get the 403 error (permission denied). We can verify that it was in fact SELinux that caused this error by checking the audit log:
[student@el ~]$ sudo grep denied /var/log/audit/audit.log | tail -1
type=AVC msg=audit(1728892181.505:929): avc: denied { read } for pid=7887 comm="httpd" name="index.html" dev="sda2" ino=62351 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:user_home_t:s0 tclass=file permissive=0
The httpd
process is running in context system_u:system_r:httpd_t:s0
, and it is trying to read a file with context unconfined_u:object_r:user_home_t:s0
. However, the httpd_t
context does not have permission to read files with the user_home_t
context.
The README file created in the previous example does in fact work:
The reason is that the file has the wrong SELinux context. You can see this with ls -Z
:
[student@el ~]$ ls -lZ /var/www/html/
total 4
drwxr-xr-x. 2 root root unconfined_u:object_r:public_content_t:s0 23 Oct 14 07:43 fileshare
-rw-r--r--. 1 student student unconfined_u:object_r:user_home_t:s0 93 Oct 14 07:48 index.html
The file has the user_home_t
type, but Apache does not have the permission to access files with this context. You can change the type with chcon
:
[student@el ~]$ sudo chcon -t httpd_sys_content_t /var/www/html/index.html
[student@el ~]$ curl http://localhost/
<html>
<head>
<title>Hello world</title>
</head>
<body>
<h1>It works!</h1>
</body>
</html>
Now it does work!
restorecon
Alternatively, you can fix the context of files with restorecon
:
[student@el ~]$ sudo restorecon -R /var/www/html
[student@el ~]$ ls -lZ /var/www/html
total 4
drwxr-xr-x. 2 root root unconfined_u:object_r:httpd_sys_content_t:s0 23 Oct 14 07:54 fileshare
-rw-r--r--. 1 student student unconfined_u:object_r:httpd_sys_content_t:s0 93 Oct 14 07:48 index.html
Now the fileshare/
directory (and its contents) have also again received the httpd_sys_content_t
context.
semanage fcontext
If you want to specify the default context for a directory, you can use the semanage
command. For example, to set the default context for the /var/www/html/fileshare
directory to public_content_t
:
[student@el ~]$ sudo semanage fcontext -a -t public_content_t '/var/www/html/fileshare(/.*)?'
[student@el ~]$ sudo restorecon -R /var/www/html/fileshare/
[student@el ~]$ ls -lZ /var/www/html/
total 4
drwxr-xr-x. 2 root root unconfined_u:object_r:public_content_t:s0 23 Oct 14 07:54 fileshare
-rw-r--r--. 1 student student unconfined_u:object_r:httpd_sys_content_t:s0 93 Oct 14 07:48 index.html
managing selinux booleans
SELinux booleans are on/off switches that can be used to enable or disable certain SELinux policies.
getsebool
You can list all booleans and their state with the getsebool -a
command. For example, here are a few booleans related to httpd
:
[student@el ~]$ getsebool -a | grep httpd | head
httpd_anon_write --> off
httpd_builtin_scripting --> on
httpd_can_check_spam --> off
httpd_can_connect_ftp --> off
httpd_can_connect_ldap --> off
httpd_can_connect_mythtv --> off
httpd_can_connect_zabbix --> off
httpd_can_manage_courier_spool --> off
httpd_can_network_connect --> off
httpd_can_network_connect_cobbler --> off
A boolean like httpd_can_network_connect
determines whether the httpd
process can initiate a network connection to another host. In most cirumstances, you may expect that a webserver does not need to connect to other hosts, so this boolean is set to off
by default. If a webserver does initiate a connection over the network, this may be an indication that it has been compromised.
To get the value of a single boolean, just specify it as an argument to getsebool
:
semanage boolean
The getsebool
command only shows the names and states of the booleans. To get a more detailed description, use the semanage boolean -l
command (with superuser privileges):
[student@el ~]$ sudo semanage boolean -l | grep httpd | head
awstats_purge_apache_log_files (off , off) Determine whether awstats can purge httpd log files.
httpd_anon_write (off , off) Allow Apache to modify public files used for public file transfer services. Directories/Files must be labeled public_content_rw_t.
httpd_builtin_scripting (on , on) Allow httpd to use built in scripting (usually php)
httpd_can_check_spam (off , off) Allow http daemon to check spam
httpd_can_connect_ftp (off , off) Allow httpd to act as a FTP client connecting to the ftp port and ephemeral ports
httpd_can_connect_ldap (off , off) Allow httpd to connect to the ldap port
httpd_can_connect_mythtv (off , off) Allow http daemon to connect to mythtv
httpd_can_connect_zabbix (off , off) Allow http daemon to connect to zabbix
httpd_can_manage_courier_spool (off , off) Allow httpd to manage the courier spool sock files.
httpd_can_network_connect (off , off) Allow HTTPD scripts and modules to connect to the network using TCP.
setsebool
You can enable or disable booleans with the setsebool
command:
[student@el ~]$ getsebool httpd_can_network_connect_db
httpd_can_network_connect_db --> off
[student@el ~]$ sudo setsebool httpd_can_network_connect_db on
[student@el ~]$ getsebool httpd_can_network_connect_db
httpd_can_network_connect_db --> on
Remark that this is not persistent over reboots. To make the change permanent with the -P
option:
To list which SELinux booleans were changed from their default settings, use semanage boolean --list -C
(or -l
) with superuser privileges:
[student@el ~]$ sudo semanage boolean --list -C
SELinux boolean State Default Description
httpd_can_network_connect_db (on , on) Allow HTTPD scripts and modules to connect to databases over the network.
troubleshooting selinux
In this section, we start with a scenario of an Apache web server with an index file with the wrong SELinux context (user_home_t
), as described above.
Accessing the website will result in a 403 error:
[student@el html]$ curl http://localhost/
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>403 Forbidden</title>
</head><body>
<h1>Forbidden</h1>
<p>You don't have permission to access this resource.</p>
</body></html>
the audit log
When SELinux denies access, it logs the event in the audit log /var/log/audit/audit.log
. Looking for the string denied
in this file is a way to show this.
[student@el html]$ sudo grep denied /var/log/audit/audit.log
type=AVC msg=audit(1728979437.925:830): avc: denied { read } for pid=3726 comm="httpd" name="index.html" dev="sda2" ino=62351 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:user_home_t:s0 tclass=file permissive=0
ausearch
The preferred way to search the audit log for errors is using the ausearch
command:
[student@el html]$ sudo ausearch -m AVC,USER_AVC,SELINUX_ERR,USER_SELINUX_ERR -ts recent
----
time->Tue Oct 15 08:03:57 2024
type=PROCTITLE msg=audit(1728979437.925:830): proctitle=2F7573722F7362696E2F6874747064002D44464F524547524F554E44
type=SYSCALL msg=audit(1728979437.925:830): arch=c000003e syscall=257 success=no exit=-13 a0=ffffff9c a1=7f7b04041498 a2=80000 a3=0 items=0 ppid=3724 pid=3726 auid=4294967295 uid=48 gid=48 euid=48 suid=48 fsuid=48 egid=48 sgid=48 fsgid=48 tty=(none) ses=4294967295 comm="httpd" exe="/usr/sbin/httpd" subj=system_u:system_r:httpd_t:s0 key=(null)
type=AVC msg=audit(1728979437.925:830): avc: denied { read } for pid=3726 comm="httpd" name="index.html" dev="sda2" ino=62351 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:user_home_t:s0 tclass=file permissive=0
The output is more verbose, you can select the type of messages you want to see with the -m
option and the timestamp with -ts
. The latter option allows you to specify descriptive time ranges like recent
, yesterday
, today
, etc.
setroubleshootd
Finding out what SELinux denies is one thing, solving another. To help system administrators, setroubleshoot
was created. This package contains a daemon (setroubleshootd
) that listens for SELinux denials and, them in a more human-readable format and offers suggestions on how to fix them.
[student@el html]$ dnf list available 'setroubleshoot*'
Last metadata expiration check: 0:00:09 ago on Tue 15 Oct 2024 08:14:37 AM UTC.
Available Packages
setroubleshoot.x86_64 3.3.32-1.el9 appstream
setroubleshoot-plugins.noarch 3.3.14-4.el9 appstream
setroubleshoot-server.x86_64 3.3.32-1.el9 appstream
[student@el html]$ sudo dnf install -y setroubleshoot
[... output omitted ...]
Complete!
We restart the machine and try to access the website again. At this time, it will fail again with the same message. You can now use journalctl
to look for setroubleshoot
log messages:
[student@el ~]$ sudo journalctl -t setroubleshoot
[... some output omitted ...]
Oct 15 08:20:33 el setroubleshoot[6355]: SELinux is preventing /usr/sbin/httpd from read access on the file index.html. For complete SELinux messages run: sealert -l ad1f25d6-6473-4af5-945a-2246b64eecf5
Oct 15 08:20:33 el setroubleshoot[6355]: SELinux is preventing /usr/sbin/httpd from read access on the file index.html.
***** Plugin catchall_boolean (89.3 confidence) suggests ******************
If you want to allow httpd to read user content
Then you must tell SELinux about this by enabling the 'httpd_read_user_content' boolean.
Do
setsebool -P httpd_read_user_content 1
***** Plugin catchall (11.6 confidence) suggests **************************
If you believe that httpd should be allowed read access on the index.html file by default.
Then you should report this as a bug.
You can generate a local policy module to allow this access.
Do
allow this access for now by executing:
# ausearch -c 'httpd' --raw | audit2allow -M my-httpd
# semodule -X 300 -i my-httpd.pp
The log message already gives a lot of information and a suggestion on how to fix te problem. It also shows how to use sealert
to get detailed information about the denial:
[student@el ~]$ sealert -l ad1f25d6-6473-4af5-945a-2246b64eecf5
SELinux is preventing /usr/sbin/httpd from read access on the file index.html.
***** Plugin catchall_boolean (89.3 confidence) suggests ******************
If you want to allow httpd to read user content
Then you must tell SELinux about this by enabling the 'httpd_read_user_content' boolean.
Do
setsebool -P httpd_read_user_content 1
***** Plugin catchall (11.6 confidence) suggests **************************
If you believe that httpd should be allowed read access on the index.html file by default.
Then you should report this as a bug.
You can generate a local policy module to allow this access.
Do
allow this access for now by executing:
# ausearch -c 'httpd' --raw | audit2allow -M my-httpd
# semodule -X 300 -i my-httpd.pp
Additional Information:
Source Context system_u:system_r:httpd_t:s0
Target Context unconfined_u:object_r:user_home_t:s0
Target Objects index.html [ file ]
Source httpd
Source Path /usr/sbin/httpd
Port <Unknown>
Host el
Source RPM Packages httpd-core-2.4.57-11.el9_4.1.x86_64
Target RPM Packages
SELinux Policy RPM selinux-policy-targeted-38.1.35-2.el9_4.2.noarch
Local Policy RPM selinux-policy-targeted-38.1.35-2.el9_4.2.noarch
Selinux Enabled True
Policy Type targeted
Enforcing Mode Enforcing
Host Name el
Platform Linux el 5.14.0-427.26.1.el9_4.x86_64 #1 SMP
PREEMPT_DYNAMIC Wed Jul 17 15:51:13 EDT 2024
x86_64 x86_64
Alert Count 1
First Seen 2024-10-15 08:20:32 UTC
Last Seen 2024-10-15 08:20:32 UTC
Local ID ad1f25d6-6473-4af5-945a-2246b64eecf5
Raw Audit Messages
type=AVC msg=audit(1728980432.972:544): avc: denied { read } for pid=833 comm="httpd" name="index.html" dev="sda2" ino=62351 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:user_home_t:s0 tclass=file permissive=0
type=SYSCALL msg=audit(1728980432.972:544): arch=x86_64 syscall=openat success=no exit=EACCES a0=ffffff9c a1=7f75ac041498 a2=80000 a3=0 items=0 ppid=733 pid=833 auid=4294967295 uid=48 gid=48 euid=48 suid=48 fsuid=48 egid=48 sgid=48 fsgid=48 tty=(none) ses=4294967295 comm=httpd exe=/usr/sbin/httpd subj=system_u:system_r:httpd_t:s0 key=(null)
Hash: httpd,httpd_t,user_home_t,file,read
The output gives two suggestions on how to fix the problem:
- Allow
httpd
to read user content by enabling thehttpd_read_user_content
boolean. - Generate a local policy module to allow this access.
Remark that in this case, it would be better to fix the file context of index.html
, and this is not part of the suggestions! But for the sake of the exercise, let's enable the boolean:
[student@el ~]$ sudo setsebool httpd_read_user_content on
[student@el ~]$ curl http://localhost/
<html>
<head>
<title>Hello world</title>
</head>
<body>
<h1>It works!</h1>
</body>
</html>
It works!
creating a local selinux policy
Let's say that for a lab setup, we want to rebind Apache's server port to 8000 instead of the default 80. We change the configuration in /etc/httpd/conf/httpd.conf
to:
and then check the config file syntax and restart the Apache service:
[student@el ~]$ apachectl configtest
AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 127.0.1.1. Set the 'ServerName' directive globally to suppress this message
Syntax OK
[student@el ~]$ sudo systemctl restart httpd
Job for httpd.service failed because the control process exited with error code.
See "systemctl status httpd.service" and "journalctl -xeu httpd.service" for details.
This fails, apparently, even though the syntax is OK. The error message is not very helpful, but the journalctl
output is:
[student@el ~]$ journalctl -u httpd.service
Oct 15 08:17:31 el systemd[1]: Starting The Apache HTTP Server...
Oct 15 08:17:31 el httpd[733]: AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 127.0.1.1. Set the 'ServerName' directive globally to suppress this message
Oct 15 08:17:31 el httpd[733]: Server configured, listening on: port 80
Oct 15 08:17:31 el systemd[1]: Started The Apache HTTP Server.
Oct 15 08:44:12 el systemd[1]: Stopping The Apache HTTP Server...
Oct 15 08:44:13 el systemd[1]: httpd.service: Deactivated successfully.
Oct 15 08:44:13 el systemd[1]: Stopped The Apache HTTP Server.
Oct 15 08:44:13 el systemd[1]: httpd.service: Consumed 1.513s CPU time.
Oct 15 08:44:13 el systemd[1]: Starting The Apache HTTP Server...
Oct 15 08:44:13 el httpd[6504]: AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 127.0.1.1. Set the 'ServerName' directive globally to suppress this message
Oct 15 08:44:13 el httpd[6504]: (13)Permission denied: AH00072: make_sock: could not bind to address [::]:8000
Oct 15 08:44:13 el httpd[6504]: (13)Permission denied: AH00072: make_sock: could not bind to address 0.0.0.0:8000
Oct 15 08:44:13 el httpd[6504]: no listening sockets available, shutting down
Oct 15 08:44:13 el httpd[6504]: AH00015: Unable to open logs
Oct 15 08:44:13 el systemd[1]: httpd.service: Main process exited, code=exited, status=1/FAILURE
Oct 15 08:44:13 el systemd[1]: httpd.service: Failed with result 'exit-code'.
Oct 15 08:44:13 el systemd[1]: Failed to start The Apache HTTP Server.
The key message here is (13)Permission denied: AH00072: make_sock: could not bind to address [::]:8000
. So, httpd
is not allowed to open a server socket on port 8000. If it was another process already listening on this socket, the error message would include a phrasing like Address already in use
.
These kinds of permission denied errors without an apparent other cause, can often be attributed to SELinux. Let's check:
[student@el ~]$ sudo journalctl -t setroubleshoot
[... some output omitted ...]
Oct 15 08:44:14 el setroubleshoot[6506]: SELinux is preventing /usr/sbin/httpd from name_bind access on the tcp_socket port 8000. For complete SELinux messages run: sealert -l 8ea00061-30d7-4fd5-b2fe-601a94348aea
Oct 15 08:44:14 el setroubleshoot[6506]: SELinux is preventing /usr/sbin/httpd from name_bind access on the tcp_socket port 8000.
***** Plugin catchall (100. confidence) suggests **************************
If you believe that httpd should be allowed name_bind access on the port 8000 tcp_socket by default.
Then you should report this as a bug.
You can generate a local policy module to allow this access.
Do
allow this access for now by executing:
# ausearch -c 'httpd' --raw | audit2allow -M my-httpd
# semodule -X 300 -i my-httpd.pp
Oct 15 08:44:14 el setroubleshoot[6506]: SELinux is preventing /usr/sbin/httpd from name_bind access on the tcp_socket port 8000. For complete SELinux messages run: sealert -l 8ea00061-30d7-4fd5-b2fe-601a94348aea
Oct 15 08:44:14 el setroubleshoot[6506]: SELinux is preventing /usr/sbin/httpd from name_bind access on the tcp_socket port 8000.
***** Plugin catchall (100. confidence) suggests **************************
If you believe that httpd should be allowed name_bind access on the port 8000 tcp_socket by default.
Then you should report this as a bug.
You can generate a local policy module to allow this access.
Do
allow this access for now by executing:
# ausearch -c 'httpd' --raw | audit2allow -M my-httpd
# semodule -X 300 -i my-httpd.pp
Indeed, it is an SELinux issue. Services are not allowed to open a socket on any port. We can check this with semanage port -l
:
[student@el ~]$ sudo semanage port -l | grep http
http_cache_port_t tcp 8080, 8118, 8123, 10001-10010
http_cache_port_t udp 3130
http_port_t tcp 80, 81, 443, 488, 8008, 8009, 8443, 9000
pegasus_http_port_t tcp 5988
pegasus_https_port_t tcp 5989
The line starting with http_port_t
specifies which ports are allowed, and 8000 is not one of them.
The output of setroubleshoot
suggests that we can create a policy module to allow this access using audit2allow
. The audit2allow
command takes the output of ausearch
as input and converts it into code to define a policy module.
[student@el ~]$ sudo ausearch -c 'httpd' --raw
[... some output omitted ...]
type=AVC msg=audit(1728981853.503:590): avc: denied { name_bind } for pid=6504 comm="httpd" src=8000 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:soundd_port_t:s0 tclass=tcp_socket permissive=0
type=SYSCALL msg=audit(1728981853.503:590): arch=c000003e syscall=49 success=no exit=-13 a0=3 a1=556baceb2378 a2=10 a3=7ffda1498eec items=0 ppid=1 pid=6504 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="httpd" exe="/usr/sbin/httpd" subj=system_u:system_r:httpd_t:s0 key=(null)ARCH=x86_64 SYSCALL=bind AUID="unset" UID="root" GID="root" EUID="root" SUID="root" FSUID="root" EGID="root" SGID="root" FSGID="root"
type=PROCTITLE msg=audit(1728981853.503:590): proctitle=2F7573722F7362696E2F6874747064002D44464F524547524F554E44
Let's pipe this to audit2allow
(and specify a name for the module with the -M
option):
[student@el ~]$ sudo ausearch -c 'httpd' --raw | audit2allow -M my-httpd
******************** IMPORTANT ***********************
To make this policy package active, execute:
semodule -i my-httpd.pp
This has created two files in the current directory:
[student@el ~]$ ls -l
total 8
-rw-r--r--. 1 student student 1209 Oct 15 09:02 my-httpd.pp
-rw-r--r--. 1 student student 312 Oct 15 09:02 my-httpd.te
[student@el ~]$ file my-httpd.*
my-httpd.pp: SE Linux modular policy version 1, 1 sections, mod version 21, MLS, module name my-httpd\003
my-httpd.te: ASCII text
my-httpd.pp
is the compiled policy module in a binary formatmy-httpd.te
is the source code of the policy module
[student@el ~]$ cat my-httpd.te
module my-httpd 1.0;
require {
type httpd_t;
type user_home_t;
type soundd_port_t;
class file read;
class tcp_socket name_bind;
}
#============= httpd_t ==============
allow httpd_t soundd_port_t:tcp_socket name_bind;
#!!!! This avc is allowed in the current policy
allow httpd_t user_home_t:file read;
Remark that this policy module also contains solutions for the other problems we encountered earlier in this chapter, i.e. the user_home_t
context of index.html
. The comment on the second to last line even warns us that this is already allowed in the current policy.
Also remark that port 8000 is not mentioned literally. For SELinux, this is the standard port for the soundd
service:
Let's remove all unnecessary lines:
[student@el ~]$ cat my-httpd.te
module my-httpd 1.0;
require {
type httpd_t;
type soundd_port_t;
class tcp_socket name_bind;
}
#============= httpd_t ==============
allow httpd_t soundd_port_t:tcp_socket name_bind;
We now need to recompile the policy module. This in fact is a two-step process. First, we need to convert the source code to a binary format with checkmodule
, and then we need to package it with semodule_package
:
[student@el ~]$ checkmodule -M -m -o my-httpd.mod my-httpd.te
[student@el ~]$ semodule_package -o my-httpd.pp -m my-httpd.mod
Finally, we can install the policy module with semodule
:
And now, httpd
should be able to bind to port 8000:
[student@el ~]$ sudo systemctl start httpd
[student@el ~]$ sudo ss -tlnp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 80 0.0.0.0:3306 0.0.0.0:* users:(("mariadbd",pid=753,fd=21))
LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=639,fd=3))
LISTEN 0 4096 0.0.0.0:111 0.0.0.0:* users:(("rpcbind",pid=532,fd=4),("systemd",pid=1,fd=31))
LISTEN 0 128 [::]:22 [::]:* users:(("sshd",pid=639,fd=4))
LISTEN 0 4096 [::]:111 [::]:* users:(("rpcbind",pid=532,fd=6),("systemd",pid=1,fd=34))
LISTEN 0 511 *:8000 *:* users:(("httpd",pid=6734,fd=4),("httpd",pid=6733,fd=4),("httpd",pid=6732,fd=4),("httpd",pid=6730,fd=4))
[student@el ~]$ curl http://localhost:8000/
<html>
<head>
<title>Hello world</title>
</head>
<body>
<h1>It works!</h1>
</body>
</html>
It works!