Administration

Setting up a chroot for PHP

Posted on

In a chroot'ed PHP DNS-lookups, mail, date-functions and other things don't work. This post shows how to make it work without copying libraries.

In a chroot’ed PHP the following things (at least) won’t function without further ado:

  • Date/timezone-stuff
  • DNS-lookups
  • Session-handling
  • mail()

These things are required for most PHP-Applications, so here’s how to make it work. It doesn’t involve copying of any dynamic libraries, which is how others typically solve it.

In the previous post I’ve shown how to configure php-fpm under Debian Wheezy. This post is a continuation of that post but should also be useful if you’re using other ways of chroot’ing PHP or need some general chroot-inspiration. Just remember it’s written for Debian Wheezy, so your mileage may vary.

chroot != security

Again, I want to stress that chroot was not meant to be a security concept and it can be easily circumvented if not great care is taken. Once more I recommend reading this article, especially the paragraph “General chroot principles”.

Keeping as little as possible in the chroot is a step in the right direction.

If you’re a programmer, take time to understand the implications and limitations of using chroot() before using it as a security-measure.

Basic chroot layout

This is the basic chroot layout I’m using:

 1 [drwxr-xr-x root    ]
 2 ├── [drwxr-xr-x root    ]  bin
 3 ├── [drwxr-xr-x root    ]  dev
 4 │   ├── [crw-rw-rw- root    ]  null
 5 │   ├── [crw-rw-rw- root    ]  urandom
 6 │   └── [crw-rw-rw- root    ]  zero
 7 ├── [drwxr-xr-x root    ]  srv
 8 │   └── [lrwxrwxrwx root    ]  www-php-fpm -> ..
 9 ├── [drwxrwxrwt root    ]  tmp
10 ├── [drw-x--x--x root    ]  usr
11 │   ├── [drwxr-xr-x root    ]  sbin
12 │   └── [drwxr-xr-x root    ]  share
13 │       └── [drwxr-xr-x root    ]  zoneinfo
14 ├── [drwxr-xr-x root    ]  var
15 │   ├── [drwxr-xr-x root    ]  lib
16 │   │   └── [drwx-wx-wt root    ]  php5
17 │   └── [drwxr-xr-x root    ]  run
18 │       └── [drwxr-xr-x root    ]  nscd
19 └── [drwxr-xr-x seb    ]  www
20     ├── [-rw-r--r-- seb    ]  index.php
21     └── [-rw-r--r-- seb    ]  test.php

As mentioned in the previous post, this tree is located at “/srv/www-php-fpm”. Things to note here:

  • Everything including “/srv/www-php-fpm” is owned by root.
  • Nothing except the tmp-dirs should be writable by the php-fpm-processes.
  • The reason for the symlink in line #8 is explained in the previous post.
  • tmp doesn’t have to have the sticky bit (+t) set in this case, but this is just how tmp-dirs are supposed to look.
  • var/lib/php5 is the path where PHP will store its session-data in if not configured otherwise. Note that under Wheezy, PHP’s session garbage-collection (gc) is disabled by default, so you need to make sure this directory is cleaned up somehow. There are two possibilities:
    • Recommended: Use a cron-job like “/etc/cron.d/php5” to delete stale files. Then, “var/lib/php5” doesn’t need to be world-readable.
    • Set “session.gc_probability = 1” in “/etc/php5/fpm/php.ini” to enable PHP’s session gc, make the directory “var/lib/php5” in the chroot readable by www-user and set “php_admin_value[open_basedir]” in the pool-configuration to something like “/tmp/:/www/” to prevent sessions from being snooped.
  • Later, you may want to set some directories to +x only (bin/, usr/sbin/, etc.). It’s best practice, but probably won’t make much of a difference.

Alongside these directories you’ll put one or more directories with the actual content and use permissions like you usually do with Apache. In my case, the user “seb” owns the “www/”-directory and the user “www-data” is member of the “seb”-group. By giving “g+w” to selected directories within “www/”, the PHP-processes will be able to write to them. Business as usual.

Here are a few of the commands I’ve used. This is for reference, not necessarily meant to be run in sequence:

mkdir -p bin dev tmp usr/sbin/ usr/share/zoneinfo/ var/run/nscd/ var/lib/php5
cp -a /dev/zero /dev/urandom /dev/null dev/
chmod --reference=/tmp tmp/
chmod --reference=/var/lib/php5 var/lib/php5
chown -R root:root .
chown -R seb:seb www/

Test-script

To test whether our future efforts are successful, we’ll be using this test-script:

<?
session_start();
header( "Content-Type: text/plain" );
echo( gethostbyname( "localhost" )."\n" );
print_r( getdate() );
mail( "your@address", "subject", "message" );

Temporarily enabling “display_errors” in “/etc/php5/fpm/pnp.ini” will display errors directly in the browser.

Fixing DNS and the Timezone database

Point your browser to the test-script and you should see two errors:

  • localhost isn’t resolved to “127.0.0.1” or “::1”, which means DNS-resolving doesn’t work.
  • getdate() complains that the timezone database is corrupt – in fact it’s not there at all.

Other articles about this topic typically involve copying some libraries for name-resolution and the timezone-database. I’ve taken a different approach as I don’t like having unnecessary cruft lying around on my filesystem.

First, let’s take care of DNS:

You’ve probably seen “nscd” running on a glibc-Linux at some point. “nscd” is (e)glibc’s “Name Service Caching Daemon”. Glibc tries consulting it not only for name-lookups like gethostbyname() et al., but also for calls like getpwnam(), which would usually consult /etc/passwd and other files. It talks to nscd by trying to connect to the Unix domain socket /var/run/nscd/socket. If it can’t, it falls back to resolving things by itself.

Instead of copying dynamic libs to the chroot and making up some passwd/group-files, we simply make sure that the mentioned socket is available in the chroot. Obviously, nscd should be running, which doesn’t hurt either way. The package is called “nscd”.

A reliable way of accomplishing this is by “bind-mounting” nscd’s /var/run directory into the chroot. Hard linking the socket will not work, as starting with Debian Wheezy /var/run is a tmpfs and hard links can’t be cross-filesystem. I haven’t found any security-related warnings about using bind-mounts in chroots when briefly searching the net, but who knows.

I’m doing the mounting in an init.d-script named “/etc/init.d/php5-fpm-chroot-setup”:

#!/bin/sh
 
### BEGIN INIT INFO
# Provides:          php5-fpm-chroot-setup
# Required-Start:    nscd
# Required-Stop:
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Bind-mounts needed sockets and data into a php-fpm-chroot
### END INIT INFO
 
CHROOT=/srv/www-php-fpm
DIRS="/var/run/nscd /usr/share/zoneinfo"
 
case "$1" in
  start)
        $0 stop 2>/dev/null
        for d in $DIRS; do
                mkdir -p "${CHROOT}${d}"
                mount --bind -o ro "${d}" "${CHROOT}${d}"
        done
        ;;  
  stop)   
        for d in $DIRS; do
                umount "${CHROOT}${d}"
        done
        ;;  
  *)
        echo "Usage: $N {start|stop}" >&2
        exit 1
        ;;
esac
 
exit 0

Running

update-rc.d php5-fpm-chroot-setup defaults

makes sure it’s executed at boot-time. Because of the “Requires-Start”-tag and the dependency-based boot sequencing, nscd will have created the directory before this init-script is run.

You’ll need to change the CHROOT-variable if a different directory is used for chroot or you may want to change the script to read the settings from an /etc/default/-file.

After starting our new shiny script to put the mounts in place and restarting php5-fpm (so its glibc starts using nscd), our test-PHP-script should show that gethostbyname() and getdate() are working now.

It seems that PHP is using the timezone its glibc-instance is configured to by default. If something isn’t right you can either configure “date.timezone” in the pool-configuration or php5-fpm’s php.ini.

I think this is a clean and simple solution and it will work nicely for a couple of chroots. It probably doesn’t scale well for hundreds of chroots, but that’s not what I wanted to achieve here.

A note to programmers: A nice trick to avoid copying dynamic libs into a chroot is making sure that the dynamic libraries are already loaded (read: mmap()’ed) before chroot’ing(). This is simply done by calling functions of the respective libraries.

Fixing mail

Some PHP-applications use PHP’s built-in “mail()”, some come with their own SMTP implementation. Wordpress uses “mail()” by default, but one could install an SMTP-plugin instead.

PHP itself contains code for SMTP, but for some reason it’s disabled on Unix. Because of that, we need to make sure “/usr/sbin/sendmail” works in the chroot.

The easiest way I’ve found was using mini_sendmail and placing the static mini_sendmail (its Makefile builds static by default) into the chroot as usr/sbin/sendmail. The warnings gcc emits about “getpwuid” etc. can be safely ignored. Mini_sendmail will connect to localhost:25 via SMTP when being run.

But wait – that’s not all. It seems that PHP launches sendmail via system(), which in turn uses “/bin/sh -c” internally.

Instead of placing a static “sh” or copying libs there, I’ve hacked up a small wrapper that supports being called with “-c command” to launch sendmail. It doesn’t support escaping of parameters with quotation marks, but that’s not necessary. It’s called “sh.c”:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
 
#define MAXARG 64
 
int main( int argc, char* const argv[] ) {
    char* args[ MAXARG ] = {};
 
    if( argc < 3 || strcmp( argv[1], "-c" ) != 0 ) {
        fprintf( stderr, "Usage: %s -c <cmd>\n", argv[0] );  
        return 1;
    }
 
    {
        char* token;
        int i = 0;  
        char* argStr = strdup( argv[2] );
        while( ( token = strsep( &argStr, " " ) ) != NULL ) {
            if( token && strlen( token ) )
                args[ i++ ] = token;
            if( i >= MAXARG )
                return 2;
        }
    }  
 
    return execvp( args[0], args );
}

Compile it by calling

gcc sh.c -o sh -static

and place it as “bin/sh” in the chroot.

Alternatively, mini_sendmail could be hacked and deployed as “bin/sh”, but this was less work.

So, assuming that an MTA is properly configured and listening at localhost:25, the mail()-function should now also work.

As a side note, try:

# aptitude install dietlibc-dev
$ diet gcc sh.c -o sh

and compare the file-sizes. Don’t do it with mini_sendmail though, it’s talking to nscd.

MySQL

If you’re using MySQL with e.g. WordPress then the DB-connection will only work if you specify “127.0.0.1” as the server-address. “localhost” makes it want to talk to MySQL via its Unix domain socket. If you’re worried about performance, you can mount “/var/run/mysqld” into the chroot by adding “/var/run/mysqld” to DIRS in the init-script.

Final words

This should be all, at least the stuff needed by Wordpress work. If something isn’t working as expected, attaching to the php5-fpm children via “strace” may help you to find out where things go wrong. The sources posted here are placed into the public domain.