PHP FPM Apparmor

Posted on Sep 8, 2018

I haven’t been using apache for a few years now … oh wow maybe like 15 years now.

There was one feature I really liked with apache and apparmor though … mod_changehat. The module allows me to assign different apparmor scopes to apache scopes. So you could limit that your wordpress vhost can not access the files of your nextcloud vhost even though they are running in the same apache.

With php-fastcgi you could start multiple instances and assign apparmor profiles to those. Though nowadays you will probably use php-fpm. For a while that meant you could split up apps with different user/groups but not with different apparmor scopes, all were in one profile.

Well … then we got this nice proposal https://wiki.php.net/rfc/fpm_change_hat, which is implemented as well!

So lets make use of this.

PHP FPM config

On openSUSE the config goes into /etc/php7/fpm/php-fpm.d/nextcloud.conf

The format is explained in the php documentation. Also check /etc/php7/fpm/php-fpm.d/www.conf.default for a base configuration with a lot of inline comments.

[nextcloud]
user = nextcloud
group = nextcloud

# our hook for apparmor
apparmor_hat = nextcloud

listen = /run/php-fpm/fpm-nextcloud.socket

listen.owner = nginx
listen.group = nginx
listen.mode = 0660

pm = dynamic
pm.max_children = 5
pm.start_servers = 1
pm.min_spare_servers = 1
pm.max_spare_servers = 3

chdir = /srv/www/vhosts/nextcloud

env[PATH]="/usr/bin:/bin"
php_admin_value[upload_tmp_dir] = /srv/www/vhosts/nextcloud/tmp
php_admin_value[session.save_path] = "/srv/www/vhosts/nextcloud/sessions"
php_admin_value[upload_max_filesize]=10G
php_admin_value[post_max_size]=10G

The apparmor side

The easy approach is we stuff it all into one profile for /usr/sbin/php-fpm. But this becomes a maintenance nightmare. Especially if you want to ship subprofiles/hats in packages.

But when poking around in the apparmor profile for apache, I noticed a neat little trick

#include <apache2.d>

The main profile

For the main profile we use /etc/apparmor.d/usr.sbin.php-fpm to follow the naming convention.

The format is explained in the man apparmor.d

#
# LICENSED UNDER AGPL 3.0
#
#include <tunables/global>
profile php-fpm /usr/sbin/php-fpm flags=(attach_disconnected) {
  #include <abstractions/base>
  #include <abstractions/nameservice>
  #include <abstractions/php7>
  #include <abstractions/php-fixes>
  #include <abstractions/openssl>
  #include <abstractions/ssl_certs>

  capability net_admin,
  capability setuid,
  capability setgid,
  capability chown,
  capability kill,

  /etc/php7/fpm/* r,
  /etc/php7/fpm/php-fpm.d/ r,
  /etc/php7/fpm/php-fpm.d/* r,

  /proc/@{pid}/attr/current rw,

  /var/log/php-fpm.log rw,

  /run/php-fpm/fpm-*.socket rwlk,
  /run/php-fpm/php-fpm.pid rwlk,

  deny / rw,

  signal (send) peer=php-fpm//*,

  change_profile -> php-fpm//*,

  #include <php-fpm.d>
  #include <local/usr.sbin.php-fpm>
}

A subprofile for a pool

To follow the example we create a subprofile for our nextcloud pool.

For this we create a /etc/apparmor.d/php-fpm.d/nextcloud file:

  #
  # LICENSED UNDER AGPL 3.0
  #
  profile nextcloud flags=(attach_disconnected) {
    #include <abstractions/php-fpm>

    /var/log/nextcloud/nextcloud.log rwlk,

    /srv/www/vhosts/nextcloud/public/ r,
    /srv/www/vhosts/nextcloud/public/** r,
    /srv/www/vhosts/nextcloud/public/config/config.php k,
    owner /srv/www/vhosts/nextcloud/data/** rwlk,
    owner /srv/www/vhosts/nextcloud/tmp/** rwlk,
    owner /srv/www/vhosts/nextcloud/sessions/ r,
    owner /srv/www/vhosts/nextcloud/sessions/** rwlk,

    ###
    # include snippets provides e.g. by plugin directories
    #
    # We use the directory layout for abstraction includes in AppArmor 3
    #
    #include if exists <php-fpm.d/nextcloud.d>
    #
    # allow admins to add snippets as well.
    #
    #include if exists <local/php-fpm.d/nextcloud.d>
    #
    ###
  }

The last missing piece: abstractions/php-fpm

#
# LICENSED UNDER AGPL 3.0
#
#include <abstractions/base>
#include <abstractions/nameservice>
#include <abstractions/php7>
#include <abstractions/php-fixes>
#include <abstractions/openssl>
#include <abstractions/ssl_certs>

signal (receive) peer=php-fpm,

/usr/share/icu/*/*.dat r,

All in action

If you want to see if a program runs under apparmor/selinux you can use the Z parameter for ps.

$ ps afxZ | grep '^/usr/sbin/php-fpm'
php-fpm (enforce)      4380 ?        Ss     0:00 php-fpm: master process (/etc/php7/fpm/php-fpm.conf)
php-fpm//nextcloud (enforce) 5874 ?  S      0:02  \_ php-fpm: pool nextcloud

If you are running with openSUSE, you can use my php-fpm-apparmor package.

$ zypper ar obs://home:darix:apps home_darix_apps
$ zypper ref
$ zypper in php-fpm-apparmor

The package comes with a /etc/apparmor.d/php-fpm.d/default, which you can use as a starting point.

Dropping things in

So in the beginning I mentioned we want do all the work to allow easy integration with other packages. If we wanted to do something like our example for roundcube, our package only needs to install 2 files

# define a new pool for roundcube
/etc/php7/fpm/php-fpm.d/roundcube.conf
# define apparmor scope for the pool
/etc/apparmor.d/php-fpm.d/roundcube

Reload apparmor and restart php-fpm to activate the profile and you are done.