SELinux Based Isolation
Sun Oct 05 2025
661 words · 5 min

SELinux Based Isolation


Table of Contents

SELinux (Security-Enhanced Linux) is one of the most powerful tools to manage user space access control, although it can be particularly complex to implement. A very steep learning curve causes it to be usually overlooked. The web hosting industry hardly ever uses SELinux, especially when products like cPanel requires it to be disabled.

Traditional access control in the simplest terms, that being users and groups can be referred to as “Discretionary Access Control” (DAC). In contrast, SELinux offers the ability to add a layer of “Mandatory Access Control” (MAC). Even if a process has relevant user and permissions, SELinux can still block unauthorised actions. As an added bonus, all blocked actions are logged. The added observability alone could be reason enough to build around it.

Shown is an example of php-fpm for two sites selinux.rskeens.com and private.rskeens.com. DAC based tightening with open_basedir is effective and almost every web host will leave it at that because it is simple and straight forward. Perhaps even housing each site in a container will be considered in an effort to achieve even stronger isolation.

Since we are using php-fpm, we need a new SELinux domain to initiate per pool, making systemd a great option. Otherwise if those processes were forked, then they would normally all inherit the same SELinux domain.

Once php-fpm is set up with the pool as its own service, we need two categories of config files:

  • Type Enforcement (.te) = define policy rules against labeled subjects / objects.
  • File Contexts (.fc) = map filesystem paths to labels.

Let us start with a very basic Type Enforcement file for selinux.rskeens.com:

PLAINTEXT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
policy_module(selinux_rskeens_com, 1.0)

require {
  class file { read getattr open map };
  class dir { read getattr search open };
  class sock_file { create write read append getattr unlink };
}

type selinux_rskeens_com_php_t;
type selinux_rskeens_com_php_exec_t;

type selinux_rskeens_com_docroot_t;
files_type(selinux_rskeens_com_docroot_t)

type selinux_rskeens_com_socket_t;
files_pid_file(selinux_rskeens_com_socket_t)

allow selinux_rskeens_com_php_t selinux_rskeens_com_docroot_t:dir { read getattr search open };
allow selinux_rskeens_com_php_t selinux_rskeens_com_docroot_t:file { read getattr open map };

manage_files_pattern(selinux_rskeens_com_php_t, selinux_rskeens_com_socket_t, selinux_rskeens_com_socket_t)
manage_sock_files_pattern(selinux_rskeens_com_php_t, selinux_rskeens_com_socket_t, selinux_rskeens_com_socket_t)
files_pid_filetrans(selinux_rskeens_com_php_t, selinux_rskeens_com_socket_t, { file sock_file })

init_daemon_domain(selinux_rskeens_com_php_t, selinux_rskeens_com_php_exec_t)

The syntax can be intimidating at first but we now have a quite beautiful layout for the service via init_daemon_domain comprising of php type selinux_rskeens_com_php_t with exec selinux_rskeens_com_php_exec_t plus document root selinux_rskeens_com_docroot_t and even socket file selinux_rskeens_com_socket_t. Real world setups would have more complex configurations to adjust mutability per path such as uploads, sessions and logs.

Now that we have selinux_rskeens_com policy, we can move on to the File Contexts file:

PLAINTEXT
1
2
3
/usr/sbin/php-fpm-selinux-rskeens-com -- gen_context(system_u:object_r:selinux_rskeens_com_php_exec_t,s0)
/srv/www/selinux.rskeens.com(/.*)?	     gen_context(system_u:object_r:selinux_rskeens_com_docroot_t,s0)
/run/php-selinux-rskeens-com\.sock       gen_context(system_u:object_r:selinux_rskeens_com_socket_t,s0)

Everything is now ready to compile a Policy Package (.pp) file:

BASH
1
2
3
make -f /usr/share/selinux/devel/Makefile selinux_rskeens_com.pp
Compiling targeted selinux_rskeens_com module
Creating targeted selinux_rskeens_com.pp policy package

Even if a Policy Package file successfully compiles, its usability is still not certain until semodule is used. This is admittedly frustrating and can make debugging difficult. Fortunately everything works well for us:

BASH
1
semodule -i selinux_rskeens_com.pp

A final implementation step requires contexts to be restored, this is done using restorecon:

BASH
1
2
3
4
5
6
7
restorecon -Rv /usr/sbin/php-fpm-selinux-rskeens-com \
/srv/www/selinux.rskeens.com

Relabeled /srv/www/selinux.rskeens.com from unconfined_u:object_r:httpd_sys_content_t:s0 to unconfined_u:object_r:selinux_rskeens_com_docroot_t:s0
Relabeled /srv/www/selinux.rskeens.com/index.html from unconfined_u:object_r:httpd_sys_content_t:s0 to unconfined_u:object_r:selinux_rskeens_com_docroot_t:s0
Relabeled /srv/www/selinux.rskeens.com/info.php from unconfined_u:object_r:httpd_sys_content_t:s0 to unconfined_u:object_r:selinux_rskeens_com_docroot_t:s0
Relabeled /srv/www/selinux.rskeens.com/test.php from unconfined_u:object_r:httpd_sys_content_t:s0 to unconfined_u:object_r:selinux_rskeens_com_docroot_t:s0

Service php-fpm-selinux-rskeens-com can then be restarted. Check the setup:

BASH
1
2
3
4
5
6
7
ps -eZ | grep php
system_u:system_r:selinux_rskeens_com_php_t:s0 40639 ? 00:00:00 php-fpm-selinux
system_u:system_r:selinux_rskeens_com_php_t:s0 40640 ? 00:00:00 php-fpm-selinux
system_u:system_r:selinux_rskeens_com_php_t:s0 40641 ? 00:00:00 php-fpm-selinux
system_u:system_r:selinux_rskeens_com_php_t:s0 40642 ? 00:00:00 php-fpm-selinux
system_u:system_r:selinux_rskeens_com_php_t:s0 40643 ? 00:00:00 php-fpm-selinux
system_u:system_r:selinux_rskeens_com_php_t:s0 40644 ? 00:00:00 php-fpm-selinux

Looks good. Now a test can be performed by attempting to read the contents of private file secret of document root private.rskeens.com from selinux.rskeens.com:

PLAINTEXT
1
2
3
type=AVC msg=audit(1759666772.576:14792): avc:  denied  { open } for  pid=40846 comm="php-fpm-selinux" path="/srv/www/private.rskeens.com/secret" dev="sda1" ino=133665 scontext=system_u:system_r:selinux_rskeens_com_php_t:s0 tcontext=unconfined_u:object_r:httpd_sys_content_t:s0 tclass=file permissive=1

type=AVC msg=audit(1759666772.576:14792): avc:  denied  { read } for  pid=40846 comm="php-fpm-selinux" name="secret" dev="sda1" ino=133665 scontext=system_u:system_r:selinux_rskeens_com_php_t:s0 tcontext=unconfined_u:object_r:httpd_sys_content_t:s0 tclass=file permissive=

At this point extensive testing should first be done with permissive mode remaining active. There will be a lot of tweaks needed but eventually the setup will be working perfectly.

Once ready, switch to enforce mode by editing /etc/selinux/config to set SELINUX=enforcing. Reboot the system and use the command getenforce to be absolutely certain enforcement is live across system restarts.

Thanks for reading!