Running a Symfony application with SELinux enforcing
This article contains a short introduction to using SELinux to confine web applications on your server.
The introduction is a short and interactive tutorial, which
you can go through by getting the Vagrantfile
linked at the bottom of this
article and creating a VM.
Why do I need SELinux?
SELinux is a great tool to secure a Linux system. It enforces Mandatory Access Control based on a subjects assigned security context from a centrally controlled security policy. This contrasts with Discretionary Access Control which enforces based on the user identity and delegates authorization down to the users own decisions.
In Mandatory Access Control the system and its central policy specifies
which subjects are allowed to access its objects. In SELinux we use
security labels on subjects and objects to compute access decisions,
and can write rules which say subjects with the type gpg_t
can access objects with the type gpg_keyfile_t
and enforce in the policy
that only the GPG process can read GPG keyfiles.
Getting Started
To get started with confining a web application, we will first need a web application to confine. A simple nginx / php-fpm stack / Symfony stack is sufficient for what we need to do.
There won't be any exposure to RDBMS or cache servers (like MySQL or Redis, respectively) but I will touch on how you can allow your web application to use these services all the while playing with SELinux.
Setup
To test in a development environment we need a Linux distribution which has its own maintained SELinux policy. You'll need some way to create a new CentOS 7 box. I'm using Vagrant and the Vagrantfile I'm using can be found at the bottom of this article as a link.
Once you have a virtual machine ready, the first command you want to run as root is:
setenforce 0
don't worry, we're going to setenforce 1
later
System Dependencies
We'll need a PHP environment and web server to run our confined Symfony
application. I typically run with php-fpm
and nginx
on my systems and
that's what I'll be using for this example. Both php-fpm
and nginx
are
already targeted by SELinux policy in CentOS so we don't have to do any
configuration here.
yum install epel-release # We need EPEL for nginxyum install nginx php-common php-fpm composeryum install policycoreutils-python # SELinux management utils
We'll also need a user for our php-fpm
processes to run under,
so create a new user named www
and give it the /sbin/nologin shell.
useradd --system wwwchsh -s /sbin/nologin www
Setting up Symfony
After installing these dependencies we can get ourselves a copy of the Symfony Standard Edition, and drop it into a web directory.
mkdir -p /var/www/symfony-appcd /var/www/symfony-appcurl -L https://github.com/symfony/symfony-standard/archive/v2.3.39.tar.gz | tar --strip 1 -xvz --chown -R www:www ./ # Fix permissions on this folder -- it will be owned by root at this pointsudo -u www composer install # Run "composer install" as the www user, we can't do this in a shell since we've set www's shell to /sbin/nologin
Not much is happening at this point, as we have no php-fpm, and no
nginx. Initially our php-fpm pool will be running under the apache
user and group, so we need to update the configuration with our new user.
sed -i 's/user = apache/user = www/' /etc/php-fpm.d/www.confsed -i 's/group = apache/group = www/' /etc/php-fpm.d/www.conf
Additionally it'd be wise to turning on error reporting, so we can see in our browser what's causing the web server to issue an error page and also set a valid timezone to suppress the annoying PHP warning.
echo 'php_flag[display_errors] = on' >> /etc/php-fpm.d/www.confecho 'date.timezone = Europe/London' >> /etc/php.ini
Superb. We have a working php-fpm
configuration now, ready to use
as soon as we have a vhost for Symfony to run under.
The following step is creating an nginx configuration file which sets up the vhost that we're going to be serving Symfony on. Their documentation provides a good example we can use for getting started:
cat > /etc/nginx/conf.d/symfony.conf <<'EOF'server {server_name symfony-app.dev;root /var/www/symfony-app/web;location / {# try to serve file directly, fallback to app.phptry_files $uri /app.php$is_args$args;}# DEV# This rule should only be placed on your development environment# In production, don't include this and don't deploy app_dev.php or config.phplocation ~ ^/(app_dev|config)\.php(/|$) {fastcgi_pass 127.0.0.1:9000;fastcgi_split_path_info ^(.+\.php)(/.*)$;include fastcgi_params;# When you are using symlinks to link the document root to the# current version of your application, you should pass the real# application path instead of the path to the symlink to PHP# FPM.# Otherwise, PHP's OPcache may not properly detect changes to# your PHP files (see https://github.com/zendtech/ZendOptimizerPlus/issues/126# for more information).fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;fastcgi_param DOCUMENT_ROOT $realpath_root;}# PRODlocation ~ ^/app\.php(/|$) {fastcgi_pass 127.0.0.1:9000;fastcgi_split_path_info ^(.+\.php)(/.*)$;include fastcgi_params;# When you are using symlinks to link the document root to the# current version of your application, you should pass the real# application path instead of the path to the symlink to PHP# FPM.# Otherwise, PHP's OPcache may not properly detect changes to# your PHP files (see https://github.com/zendtech/ZendOptimizerPlus/issues/126# for more information).fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;fastcgi_param DOCUMENT_ROOT $realpath_root;# Prevents URIs that include the front controller. This will 404:# http://domain.tld/app.php/some-path# Remove the internal directive to allow URIs like thisinternal;}error_log /var/log/nginx/project_error.log;access_log /var/log/nginx/project_access.log;}EOF
This should be all we need to get traffic routed to our Symfony app. Restart php-fpm and nginx:
systemctl enable php-fpm && systemctl start php-fpmsystemctl enable nginx && systemctl start nginx
If we hack a symfony-app.dev
entry into the host machines /etc/hosts
file for now we will be able to access our Symfony installation. By default
the virtual machine listens on 192.168.50.59
so we can execute the following on the
host machine to set that up: echo "192.168.50.59 symfony-app.dev >> /etc/hosts"
.
We have liftoff!
Now it's time to put SELinux back into enforcing mode, and see what problems we have to address.
setenforce 1
We have AVCs!
The first step here is to examine what SELinux is preventing
us from doing. We can check AVC denials by using the ausearch
utility.
> $ ausearch -m AVC -ts recent----time->Tue Apr 19 18:40:54 2016type=SYSCALL msg=audit: arch=c000003e syscall=6 success=no exit=-13 a0=7fff478be590 a1=7fff478be480 a2=7fff478be480 a3=20 items=0 ppid=3915 pid=3918 auid=4294967295 uid=995 gid=993 euid=995 suid=995 fsuid=995 egid=993 sgid=993 fsgid=993 tty= ses=4294967295 comm="php-fpm" exe="/usr/sbin/php-fpm" subj=system_u:system_r:httpd_t:s0 key=type=AVC msg=audit: avc: denied
Ouch! What does this mean to us? Well currently our web directory is
labeled with the file context var_t
. This seems like it isn't suitable
for httpd content. Though this seems like a pretty common
path to store files for web applications. Let's see what this path
should be labeled as according to the loaded SELinux policy.
> $ matchpathcon /var/www/var/www system_u:object_r:httpd_sys_content_t:s0
That's weird. Then why is it currently labeled as var_t
? The
answer is that we didn't tell mkdir
to use the default
file contexts for this path when we created the directory,
so the context was inherited from /var.
We can fix this by using the restorecon
utility, which
fixes file contexts to what they should be according to policy.
restorecon -R /var/www
That gets by the first set of errors, but this is now where we really need to consider what our app can and can't do. If we rm the cache and visit our Symfony app again it'll complain that it does not have permissions to write to the cache.
Warming the cache worked the first time because we ran composer install
in an unconfined
user shell. However, to let scripts running in the
httpd_t
domain write to the cache we will have to start modifying policy.
Fortunately we already have a file context defined for writable
httpd content in the upstream policy.
We can relabel and fix the cache directory with this context by running:
semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/symfony-app/cache(/.*)?"restorecon -R /var/www/symfony-app/cache
Now we're back in business! Our /var/www/symfony-app/logs
has already
been labeled for log files by an existing file context so we don't need
to bother with that.
That about concludes this introduction to Symfony with SELinux. There's more we could do to secure our application and some more considerations (like cron) but now you can run Symfony applications without turning SELinux off, which is a great step forward!
Extras
I can't connect to MySQL or Redis
This problem comes from the SELinux policy preventing httpd's from connecting to anywhere on the network (the connection to php-fpm is allowed because port 9000 is labeled as a httpd port).
Since this is such a common use case SELinux presents a mechanism to allow for toggleable policy. We can see which booleans we can toggle for the httpd policy by running:
> $ semanage boolean -l | grep 'httpd'httpd_can_network_relay Allow httpd to can network relayhttpd_can_connect_mythtv Allow httpd to can connect mythtvhttpd_can_network_connect_db Allow httpd to can network connect dbhttpd_use_gpg Allow httpd to use gpghttpd_dbus_sssd Allow httpd to dbus sssdhttpd_enable_cgi Allow httpd to enable cgihttpd_verify_dns Allow httpd to verify dnshttpd_dontaudit_search_dirs Allow httpd to dontaudit search dirshttpd_anon_write Allow httpd to anon writehttpd_use_cifs Allow httpd to use cifshttpd_enable_homedirs Allow httpd to enable homedirshttpd_unified Allow httpd to unifiedhttpd_mod_auth_pam Allow httpd to mod auth pamhttpd_run_stickshift Allow httpd to run stickshifthttpd_use_fusefs Allow httpd to use fusefshttpd_can_connect_ldap Allow httpd to can connect ldaphttpd_can_network_connect Allow httpd to can network connecthttpd_mod_auth_ntlm_winbind Allow httpd to mod auth ntlm winbindhttpd_tty_comm Allow httpd to tty commhttpd_sys_script_anon_write Allow httpd to sys script anon writehttpd_graceful_shutdown Allow httpd to graceful shutdownhttpd_can_connect_ftp Allow httpd to can connect ftphttpd_run_ipa Allow httpd to run ipahttpd_read_user_content Allow httpd to read user contenthttpd_use_nfs Allow httpd to use nfshttpd_can_connect_zabbix Allow httpd to can connect zabbixhttpd_tmp_exec Allow httpd to tmp exechttpd_run_preupgrade Allow httpd to run preupgradehttpd_manage_ipa Allow httpd to manage ipahttpd_can_sendmail Allow httpd to can sendmailhttpd_builtin_scripting Allow httpd to builtin scriptinghttpd_dbus_avahi Allow httpd to dbus avahihttpd_can_check_spam Allow httpd to can check spamhttpd_can_network_memcache Allow httpd to can network memcachehttpd_can_network_connect_cobbler Allow httpd to can network connect cobblerhttpd_use_sasl Allow httpd to use saslhttpd_serve_cobbler_files Allow httpd to serve cobbler fileshttpd_execmem Allow httpd to execmemhttpd_ssi_exec Allow httpd to ssi exechttpd_use_openstack Allow httpd to use openstackhttpd_enable_ftp_server Allow httpd to enable ftp serverhttpd_setrlimit Allow httpd to setrlimit
The httpd_can_network_connect_db
boolean sounds like it
should do the job if you only need to connect to a database.
If this doesn't cover your use cases you can use the more general
httpd_can_network_connect
to allow connections to any service.
To toggle these booleans we run:
setsebool -P httpd_can_network_connect_db 1
The -P
flag we pass tells setsetbool to persist this change so it
is applied whenever policy reloads or the machine is rebooted.
This doesn't work with PHP7!
PHP7 came with some pretty huge engine improvements, along with superior
JIT compilation. To allow PHP7 to do this compilation we need to allow
httpd_t
access to the execmem
permission. Fortunately, there is
also a boolean for this and we can enable it by running:
setsebool -P httpd_can_execmem 1
How can I avoid the issue you ran into with mkdir
?
This is pretty simple to accomplish. coreutils
packages that move, or
create files typically allow you to specify a -Z
option which
indicates that the matching file context for the target path
should be assigned to the new file.
What if someone hijacks the web server process running under root
? Aren't I screwed anyway?
No! The httpd_t domain only has access to whatever the policy explicitly granted to it. Even if we're running as root, if we're not associated with the correct security context to access the target file then we wil be denied access.
Example (as root):
> $ cat > test.c <<'EOF'#include <stdlib.h>#include <stdio.h>#include <stddef.h>int main(int argc, char *argv){FILE *file = fopen("/etc/shadow", "r");if (file == NULL) {printf("We've been thwarted!\n");return -1;}fclose(file);return 0;}EOF> $ gcc test.c -o test> $ chcon "system_u:object_r:httpd_exec_t" ./test> $ runcon "system_u:object_r:httpd_t:s0" ./test> $ ausearch -m AVC -ts recent----time->Tue Apr 19 20:24:53 2016type=SYSCALL msg=audit: arch=c000003e syscall=2 success=no exit=-13 a0=4006a2 a1=0 a2=1b6 a3=21000 items=0 ppid=2817 pid=4495 auid=1000 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty= ses=4 comm="test" exe="/var/www/symfony-app/app/test" subj=system_u:system_r:httpd_t:s0 key=type=AVC msg=audit: avc: denied
If you have any questions and want to get in touch don't hesitate to! Just see my contact page.