DesertFlood

Bring life to a barren, technological wasteland

Implementing a Locked Down PHP Site

In my last post I talked about a plan for securely setting up PHP. After making the plan, I had two goals: test it to see if it would work and automate the setup.

Testing the Setup

I started with a simple site: a small Dokuwiki installation. It was an easy site to test because it didn’t require a database connection.

  1. Run nginx as a separate user account, with read access to the web root.

    Done. nginx is already setup to run as the www-data user and the web files are not group writeable.

  2. Create a new user, dokuwiki.

    I set up the user account with a user private group as well as membership to the www-data group. This will let me log in as the dokuwiki user to edit the dokuwiki site (and only the dokuwiki site). I also created a directory for the dokuwiki site, changed the user ownership to the dokuwiki user and the group ownership to the www-data group. Finally, I made the whole thing user writeable and group readable.

    adduser dokuwiki
    usermod -a -G www-data dokuwiki
    mkdir /srv/www/dokuwiki
    chown -R dokuwiki:www-data /srv/www/dokuwiki
    chmod -R 2755 /srv/www/dokuwiki
    
  3. Set up a separate PHP instance for each application and use the open_basedir directive to lock PHP down to the specific application’s folder.

    I setup my VPS to use the Dotdeb repository for Debian and installed the php5-fpm package. I copied the default configuration, to make one specific for Dokuwiki, and edited it.

    apt-get install php5 php-pear php5-mysql php5-suhosin php5-cli php5-cgi php5-fpm
    sudo cp /etc/php5/fpm/pool.d/www.conf /etc/php5/fpm/pool.d/dokuwiki.conf 
    sudo vim /etc/php5/fpm/pool.d/dokuwiki.conf
    

    I made the following changes to my dokuwiki.conf file. This sets up a PHP “pool” just for Dokuwiki. The pool runs as the dokuwiki user, so that PHP will have read-write access to the dokuwiki folders.

    ; pool name
    [dokuwiki]
    
    user = dokuwiki
    group = www-data
    listen = /var/run/php5-fpm/dokuwiki.sock
    
    pm.max_children = 2    ; this is an infrequently used site
    pm.start_servers = 1
    pm.min_spare_servers = 1
    pm.max_spare_servers = 1
    pm.max_requests = 500
    request_terminate_timeout = 30s
    
    chdir = /srv/wwww/dokuwiki/public_html/
    php_admin_flag[log_errors] = on
    php_admin_value[error_log] = /srv/wwww/dokuwiki/log/fpm-php.err.log
    php_admin_value[open_basedir] = /srv/wwww/dokuwiki/
    php_admin_value[session.save_path] = /srv/www/dokuwiki/tmp
    security.limit_extensions = .php .php3 .php4 .php5
    

    The chdir directive ensures that PHP starts from the dokuwiki web directory. The php_admin_flag and php_admin_value directives allow me to set PHP options directly, without needing to create a separate php.ini file for each application. With this setup, PHP will log errors to an application specific log file, limit applications to reading and writing files in the application’s directory, and limit PHP to executing files with various PHP extensions.

    I can’t be sure until someone actually hacks me, of course, but I think this is pretty locked down. Attackers can’t use PHP to read / write other files on the server. And, because I’m using a specific user account for each application, exploiting security holes to gain shell access only gains you shell access for this particular account and application. I may not be able to prevent an individual application from being hacked, but I can limit the damage to that one application.

  4. Set up nginx to serve the site

    After I saved the configuration file, I started up php5-fpm, to launch the persistently running PHP processes: /etc/init.d/php5-fpm start. Next I created an nginx sites file, to enable nginx to serve the application: /etc/nginx/sites-enabled/dokuwiki.

    location ~ \.php$ {
        include /etc/nginx/fastcgi_params;
        fastcgi_pass /var/run/php5-fpm/dokuwiki.sock
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME /srv/www/dokuwiki/public_html$fastcgi_script_name;
    }
    

    Notice that nginx and and PHP-FPM are communicating through Unix sockets, using a unique socket for each PHP application. I tested the configuration and it worked perfectly.

Automating the Setup

I created a shell script to automate each of these steps for me. It needs to be run by the root user. The first parameter is the username that you want the script to create and the second is the name of the application that you’re setting up. The only real rule is that the app name can’t have an '@' symbol in it. I saved this script as phpapp.sh.

First it will create the new user account (and user private group) and add the user account to the www-data group. It will create all of the necessary application folders under /srv/www and set the ownership and permissions appropriately. It will also create a PHP-FPM configuration file (using the default file as a base) and an nginx sites file, setting up the correct Unix socket in each.

 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
26
27
28
29
30
31
32
33
34
35
36
37
#!/bin/sh
# params
# $1 = user
# $2 = app name
# $3 = domain

USER=$1
APP=$2
DOMAIN=$3
APATH="/srv/www/${APP}"
CONF="/etc/php5/fpm/pool.d/${APP}.conf"
NGINXCONF="/etc/nginx/sites-available/${APP}"

adduser ${USER}
usermod -a -G www-data ${USER}
mkdir -p "${APATH}/public_html"
mkdir -p "${APATH}/tmp"
mkdir -p "${APATH}/log"
chown -R ${USER}:www-data "${APATH}"
chmod -R 2755 "${APATH}"

cp /etc/php5/fpm/pool.d/www.old "${CONF}"
sed -i "s/\[www\]/\[${APP}\]/" "${CONF}"
sed -i "s@listen = 127.0.0.1:9000@listen = /var/run/php5-fpm/${APP}.sock@" "${CONF}"
sed -i "s@user = www-data@user = ${USER}@" "${CONF}"
sed -i "s/;listen.allowed_clients/listen.allowed_clients/" "${CONF}"
sed -i "s@chdir = /@chdir = ${APATH}/public_html/@" "${CONF}"
sed -i 's/;security.limit_extensions/security.limit_extensions/' "${CONF}"
sed -i "s/;php_admin_flag\[log_errors\] = on/php_admin_flag\[log_errors\] = on/" "${CONF}"
sed -i "s@;php_admin_value\[error_log\] = /var/log/fpm-php.www.log@php_admin_value\[error_log\] = ${APATH}/log/fpm-php.err.log@" "${CONF}"
echo "php_admin_value[open_basedir] = ${APATH}/" >> "${CONF}"
echo "php_admin_value[session.save_path] = ${APATH}/tmp" >> "${CONF}"

cp /etc/nginx/sites-available/template "${NGINXCONF}"
sed -i "s@/srv/www/app/@/srv/www/${APP}/@" "${NGINXCONF}"
sed -i "s@/var/run/php5-fpm/app.sock@/var/run/php5-fpm/${APP}.sock@" "${NGINXCONF}"
sed -i "s/server_name template.com/server_name ${DOMAIN}/" "${NGINXCONF}"

After running this script, you just need to restart PHP-FPM and reload nginx and your application will be good to go, safely sandboxed in its own directory and account.