Securing WordPress with AppArmor

WordPress is a very popular CMS. According to one report, 30% of websites use WordPress, which is an impressive feat.

Despite this popularity, WordPress is built upon PHP which is often lacking in the security department. Add to this that the user that runs the webserver often has a fair bit of access and there is no distinguishing between the webserver code and the WordPress code and you set yourself up for troubles.

So, let’s introduce something that not only can tell the difference between Apache running and WordPress running under it, but also limit what WordPress can access.

As the AppArmor wiki says “AppArmor is Mandatory Access Control (MAC) like security system for Linux. AppArmor confines individual programs to a set of files, capabilities, network access and rlimits…”.  AppArmor also has this concept of hats, so your webserver code (e.g. apache) can be one hat with one policy but the WordPress PHP code has another hat and therefore another policy. For some reason, AppArmor calls a policy a profile, so wherever you see profile translate that to policy.

The idea here is to limit what WordPress can access down to the files and directories it needs, and nothing more. What follows is how I have setup my system but you may need to tweak it, especially for some plugins.

Change your hat

By default, apache will run in its own  AppArmor profile, called something like the “/usr/sbin/apache2” profile.  As the authors of this profile do not know what you will run on the webserver, it is very permissive and with the standard AppArmor setup is what the WordPress code will also run under.

First, you need to enable and install the mod_apparmor Apache module. This module allows you to change what profile is used, depending on what directory or URL is being requested. The link for mod_apparmor describes how to do this.

Once you have the module enabled, you need to tell Apache what directories you want the hat or profile to be changed and the name of the new hat. I put this into /etc/apache2/conf-available/wordpress and then “a2enconf wordpress”

<Directory "/usr/share/wordpress">
  Require all granted
  <IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteBase /
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule . /index.php [L]
  <IfModule mod_apparmor.c>
    AAHatName wordpress

Alias /wp-content /var/lib/wordpress/wp-content/

<Directory /var/lib/wordpress >
  Require all granted
  <IfModule mod_apparmor.c>
    AAHatName wordpress

Most of this configuration is pretty standard WordPress setup for Apache. The important differences are the AAHatName lines.

What we have done here is if Apache serves up files from /usr/share/wordpress (where the WordPress code lives) or /var/lib/wordpress/wp-content (where things like plugins, themes and uploaded images live) then it will use the wordpress sub-profile.

Defining the profile

Now that we have the right profile for our WordPress directories, we need to create a profile. This will tell AppArmor what files and directories WordPress is allowed to access and how they are accessed. Obvious ones are the directories where the code and content live, but you will also need to include the log file locations.

This definition needs to sit “inside” the apache proper profile. In Debian and most other systems, it is just a matter of making a file in /etc/apparmor.d/apache.d/ directory.

^wordpress {
 include <abstractions/apache2-common>
 include <abstractions/base>
 include <abstractions/nameservice>
 include <abstractions/php5>
 include <abstractions/ssl_certs>
 include <abstractions/openssl>

# To make email work, this is for postfix
  /bin/dash rix,
  /usr/sbin/sendmail rix,
  /usr/sbin/postdrop rix,
  /etc/postfix/** r,
  /etc/mailname r,
  /var/spool/postfix/** rw,
 /var/log/apache2/*.log w,
 /etc/wordpress/config-*.php r,
 /usr/share/wordpress/** r,
 /usr/share/wordpress/.maintenance w,

# Debian has links to system javascript
 /usr/share/javascript/** r,

# Change "/var/lib/wordpress/wp-content" to whatever you set
# WP_CONTENT_DIR in the # /etc/wordpress/config-*.php file
 /var/lib/wordpress/wp-content r,
 /var/lib/wordpress/wp-content/** r,
 /var/lib/wordpress/wp-content/uploads/** rw,
 /var/lib/wordpress/wp-content/upgrade/** rw,
 # Uncomment to permit plugins Install/Update via web
 /var/lib/wordpress/wp-content/plugins/** rw,
 # Uncomment to permit themes Install/Update via web
 #/var/lib/wordpress/wp-content/themes/** rw,

# This is what PHP sys_get_temp_dir() returns
 /tmp/* rw,

What we have here is a policy that basically says you can read the WordPress code and read the WordPress content. The plugins and themes sub-directories can their own line because you can selectively permit write access if you want to update plugins and themes using the web GUI.

The /etc file glob is where the Debian package stores its configuration file. The surprise for me was the maintenance dot-file which is created when WordPress is updating some component. Without this write permission, it is unable to update plugins or do many other things.

You will also need to let wordpress see some openssl configuration and if you use some of the system javascript libraries access to that directory too.

WordPress uses email to send out emails like you need to moderate a post or comment, or a new user. This in turn needs to be handled in apparmor as well.

Audit Log

So how do you know its working? The simplest way is to apply the policy and then see what appears in your auditd log (Mine is at /var/log/audit/audit.log).

Two main things will go wrong. The first is the wrong profile will get used. I had this problem when I forgot to add the WordPress content directory.

Wrong Profile

type=AVC msg=audit(1522235033.386:43401): apparmor=”ALLOWED” operation=”open” profile=”/usr/sbin/apache2//” name=”/var/lib/wordpress/wp-content/plugins/akismet/akismet.php” pid=5036 comm=”apache2″ requested_mask=”r” denied_mask=”r” fsuid=33 ouid=33

So, what is AppArmor trying to say here? First we have the wrong profile! It’s not wordpress, but “/usr/sbin/apache2//null-dropbear,xyz” which is basically saying there was no specific sub-profile for this website so we will use the apache2 profile.

The apache2 profile is in complain, not enforce mode, so that’s why it says apparmor=”ALLOWED” yet has denied_mask=”r”.

Adding that second <Directory> clause to use the wordpress AAHatName fixed this.

Profile missing entries

The second type of problem is that you have Apache switching to the correct profile but you missed a line in the profile.  Initially I didn’t know WordPress created a file in the top-level of its code directory when undergoing maintenance. The log file showed:

type=AVC msg=audit(1522318023.409:51143): apparmor=”DENIED” operation=”mknod” profile=”/usr/sbin/apache2//wordpress” name=”/usr/share/wordpress/.maintenance” pid=16165 comm=”apache2″ requested_mask=”c” denied_mask=”c” fsuid=33 ouid=33

We have the correct profile here (wordpress is always sub-profile of apache). But we are getting a DENIED message because the profile (initially) didn’t permit the file /usr/share/wordpress/.maintenance” to be created.

Adding that file to the profile and reloading the profile and Apache fixed this.

Additional Tweaks

The given profile will probably work for most WordPress installations. Make sure you change the code and content directories to wherever you use.  Also, this profile will not let you auto-update the WordPress code. For a Debian package, this is a good thing as the Apache process is not writing the new files, dpkg is and it runs as root. If you are happy for a webserver to update PHP code that runs under it, you can change the permission for read/write for /usr/share/wordpress

I imagine some plugins out there will need additional directories. I don’t use many and none I use do those odd things, but there are plenty of odd plugins out there. Check your audit logs for any DENIED lines.


2 responses to “Securing WordPress with AppArmor”

  1. Kurt Kremitzki Avatar
    Kurt Kremitzki

    Nice article! I work with the FreeCAD project and was just thinking about looking into adding an AppArmor profile for our project myself, so this is a timely posting. Seems like it shouldn’t be too tough at all 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *