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.
-
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. -
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 thewww-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
-
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. Thephp_admin_flag
andphp_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.
-
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.