Once upon a time, many years ago, I wrote a blog post titled Stop Disabling SELinux! as a response to seeing many users, hosting companies, and development shops disabling SELinux as a first resort without any consideration of the increased security it was bringing them. The post outlines -- in a few easy steps -- how to configure SELinux for a common Drupal setup. But it's applicable to any LAMP application (plus memcached).

I'm still a big proponent of running SELinux, and continue to encourage its use. In that same vein, I'd like to share how we at Tag1 Consulting typically deploy SELinux configuration using Puppet. Levering a configuration management system such as Puppet enables us to deploy SELinux configuration across many hosts with minimal work, as well as ensuring nothing is ever missed by making configuration changes manually -- a must for any scalable (and stable!) infrastructure.

Too Long; Didn't Read Version

"Holy shit, I'm not going to read a multiple-page blog post just to see what this guy has to say about Puppet and SELinux, just show me the code!"

OK, OK! I understand I can be long winded at times. If you just want to see Puppet and SELinux in action, take a look at the site_selinux module within our Puppet CentOS git repo.

Here's a quick summary of the classes there:

  • init.pp - Should be called by all servers; calls the puppet/selinux class to enable SELinux on the system.
  • drupal.pp - Common SELinux for Drupal web servers, can be configured to meet your local site needs.
  • newrelic.pp - Deploy an SELinux module to grant appropriate access to a web server running New Relic reporting.
  • php-systemd.pp - SELinux configuration for web servers running php-fpm via systemd sockets. See Greg's blog post: Zero Downtime PHP-FPM Restarts Using Systemd for more information about that configuration.
  • puppetmaster.pp - SELinux configuration for servers running as a Puppet Master.

For the remainder of this post I'll focus exclusively on init.pp and drupal.pp for SELinux configuration on a Drupal web server.

About the SELinux Puppet Module

The module we leverage within our Puppet tree is puppet/selinux (GitHub repo) which was originally authored by James Fryman, and is now maintained by the Vox Pupuli group of Puppet module maintainers.

The module provides a simple framework for working with SELinux within your Puppet manifests, including: deploying custom SELinux modules, controlling SELinux booleans, and controlling file and port SELinux contexts.

The module README provides a very basic overview of usage. I'll elaborate on that a bit to describe how we use the module to configure CentOS web servers running Drupal applications.

Overview of a Common Drupal SELinux Configuration

The Tag1 site_selinux::drupal module provides common configuration for SELinux on Drupal web servers. While it may need some per-site customization, the general tasks required remain the same:

  • Set SELinux booleans as needed for a web server: e.g. allowing httpd to access MySQL and Memcached on the network, or to send out email.
  • Configure SELinux filesystem contexts to allow read-only or read-write access for httpd to various system paths (independent of UNIX file permissions).
  • Load any custom SELinux modules needed for your environment -- in our example we have custom Solr and Varnish SELinux modules which allow httpd (or php) to access networked Solr and Varnish control ports.
  • Manage custom SELinux port types as needed -- in our example we configure port types used to control access to Solr.

You may be need to add or remove parts of that for your site-specific configuration, but this should give a solid start for configuring most web servers.

Step Zero: Enable SELinux!

Enabling SELinux is done by including the site_selinux class somewhere in your Puppet configuration. By setting the hiera value selinux::mode: 'enforcing', that will ensure SELinux is enabled (it will default to targeted mode on CentOS).

You may need to reboot the system after enabling if SELinux was disabled previously.

Step One: Managing Booleans

SELinux booleans are a quick and easy way to enable permissions commonly granted to applications. Httpd has quite a few of these that ship with SELinux on CentOS -- see getsebool -a | grep httpd for a full list.

In the case of a Drupal web server, we typically enable a few httpd-related SELinux booleans: httpd_can_network_connect_db, httpd_can_network_memcache, and httpd_can_sendmail. Those are configured in hiera as follows:

# Hash of SELinux booleans to set -- this list is an example of a Drupal Web Server.
    persistent: true
    ensure: 'on'
    persistent: true
    ensure: 'on'
    persistent: true
    ensure: 'on'

You can customize that within hiera per-host, per-server-group, or whatever makes the most sense in your case, as long as you use the hash name site_selinux::drupal::selbooleans. That hash is read in by the site_selinux::drupal module with the following code snippet:

$drupal_selbooleans = hiera_hash('site_selinux::drupal::selbooleans', {})
create_resources('selinux::boolean', $drupal_selbooleans)

We pull in the values from hiera using hiera_hash instead of passing them in as class parameters to make it easier to customize and extend within a hiera tree. For example, you may set the above hash in a hiera configuration applied to all web servers; you could then further customize hiera settings for specific hosts -- extending the existing hash and/or overriding certain values in it -- without having to include the above in each host definition.

Step Two: Managing File Contexts

SELinux ships with a lot of default file contexts to help keep additional configuration to a minimum, for example, if you have your Drupal sites/default/files/ directory within a subdirectory of /var/www/html, you may not need any special configuration at all -- that defaults to the fcontext httpd_sys_rw_content_t to allow httpd to write to that directory (as is required by Drupal). However, many sites serve out of different directories than that, and may need to set default file contexts to ensure appropriate read or write access from httpd. To configure those contexts with Puppet, we list the paths in hiera within two different hashes: site_selinux::drupal::drupal_file_paths, which are assigned a default context of httpd_sys_rw_content_t if no context is specified in hiera, and site_selinux::drupal::httpd_readable_paths, which are assigned a default context of httpd_sys_content_t (httpd can read but not write). Since these two hashes of file paths are handled almost identically, we'll just look at the writable file paths here. For those, the following is configured in hiera:

# Hash of paths (include valid regex as expected by selinux) to assign selinux contexts too.
# Default context of 'httpd_sys_rw_content_t' (httpd-writable) is used unless overridden here.
    pathname: '/var/www/drupal_private_files(/.*)?'
    restorecond_path: '/var/www/drupal_private_files'
    pathname: '/var/www/drupal_public_files(/.*)?'
    restorecond_path: '/var/www/drupal_public_files'

These are pulled in by the site_selinux::drupal class with the following code snippet:

$httpd_readable_paths = hiera_hash('site_selinux::drupal::httpd_readable_paths', {})
create_resources('selinux::fcontext', $httpd_readable_paths, { context => 'httpd_sys_content_t' })

This is very similar to how we handle SELinux booleans, but in this case we set a default file context of httpd_sys_rw_content_t as described above. If you need to set a different context for a certain path, add a context value to the hiera hash and that will be applied instead of the default. If your pathname includes wildcards (which most will), you also need to include the restorecond_path setting without regular expressions -- that path will be passed to the restorecon command which can't handle regular expressions.

Step Three: Deploying Custom SELinux Modules

Although it's not terribly common, there are times when you may need to deploy a custom SELinux module. Writing custom modules is better left for a separate post; for now we'll focus on how to deploy the custom modules via Puppet. In this example we deploy two custom modules: one which allows httpd to communicate with a networked Solr instance, and one which allows httpd to communicate with Varnish control ports (this is useful for flushing certain items from the cache programmatically, using e.g. the Drupal Varnish module).

The puppet/selinux module provides a selinux::module definition which we leverage to deploy our custom modules. Within hiera, we use the following hash to define which SELinux modules to install on the target server:

# Hash of custom SELinux modules to install.
    source: 'puppet:///modules/site_selinux/httpdsolr/httpdsolr.te'
    source: 'puppet:///modules/site_selinux/httpdvarnish/httpdvarnish.te'

Those are then pulled in by the site_selinux::drupal module with the following code snippet:

$drupal_selinux_modules = hiera_hash('site_selinux::drupal::selinux_modules', {})
create_resources('selinux::module', $drupal_selinux_modules)

Each entry in the hiera hash contains a source value which is the path to the .te file (within your Puppet tree) to copy out to the server. The selinux::module defined type will handle copying that file out to the target server, compiling the (human-readable) .te file into a loadable SELinux module .pp file, and then load that .pp module file into SELinux.

Once you have this setup, it becomes very easy to add or update SELinux modules: make a change to your .te module file (don't forget to bump the version number!), commit it to your Puppet tree, and the next Puppet run will install/update the module for you!

Need An Expert to Secure Your Drupal Sites?

Tag1 has you covered with our Drupal security monitoring solution, Tag1 Quo.

Step Four: Custom SELinux Port Types

Something even less common than custom SELinux modules is the need to custom label ports with SELinux contexts. However, we needed to do this for our Solr module, so it's certainly not unheard of. We customize the port labels with Puppet exec blocks because depending on the port in question you may need to run different commands (for example, if it's already assigned some other context by default).

In this example class we set the custom solr_port_t type (defined in the custom SELinux module httpdsolr.te copied out above). We do this by looking for the port in /etc/selinux/targeted/modules/active/ports.local, and if it's not listed there, executing an semanage port call to configure it. This is done the same for the three ports in question, so we'll only look at code for one:

exec { 'semanage-port-8112':
  command => '/usr/sbin/semanage port -a -t solr_port_t -p tcp 8112',
  unless  => '/bin/grep solr_port_t /etc/selinux/targeted/modules/active/ports.local | /bin/grep -q 8112',
  require => Selinux::Module['httpdsolr'],

The exec requires the httpdsolr module to be installed first since the solr_port_t type is defined there.

Step Five: Enjoy Your Newfound Security

That's it! In most cases you can use this class with only a few changes to hiera and you'll be running your web servers with SELinux enabled. These same ideas can be applied to any other server type with minimal changes, so what are you waiting for? Go out there and automate your SELinux configurations with Puppet!