Setup FastCGI and PHP with individual user permissions

''First of all: please notice that this how-to is only a suggestion on how to do this, so please don't blame anybody if you prefer to do things differently, or get mad customers, or whatever....''

''Execwrap or php-fpm can be used to do this too, but this howto does not cover those methods.''

''Note: This only works on *nix like operating systems. I don't know how to do this on Windows.''

Introduction

Running a website hosting service for individual users/customers requires some extra brain-work when you set up your web-server.

Basically, you give every user an individual (ordinary) user account on your web-server. The user then uploads her PHP script files to her own virtual host document root.

What we want to do, is to execute all PHP script files with the exact same user permissions as the user that manages the virtual host in question. If this is accomplished, you can be sure that none of your users will be able to browse through other users' PHP scripts.

Consider the following PHP script executed on a web-server without individual user permissions on PHP-scripts (please do not attempt to do this, since you might end up with the police knocking on your door!):


#!php
<?php
$filename = "/path_to_other_users_vhost_root/index.php";
$handle = fopen($filename, "rb");
$contents = fread($handle, filesize($filename));
fclose($handle);

echo $contents;
?>

This will read (and show) the source code of PHP script of some other user. The source code might contain passwords that gives access to that user's MySQL databases, or other interesting stuff. You could even make a PHP script that writes PHP script files to other user's virtual host directories!

This is the setup we want to get rid of!

What about PHP's built-in safe_mode

I will not say any bad things about PHP here, and it is NOT recommended to use PHP's built-in safe_mode features. (See the safe_mode documentation at php.net for a detailed description.)

There are, however, some php.ini settings that can stop or slow down the most common forms of attack without touching the source code. To stop php's remote access, see allow_url_fopen, and to stop php from including remote files you can see allow_url_include. Setting open_basedir is a good way to slow down an attacker, but is no replacement for user permissions. And to slow down some forms of session stealing, session.use_trans_sid can be turned off.

It is always best if you rely on your operating system's build-in user permissions.

Installation

We assume that you already have Lighttpd installed, and installed PHP with FastCGI support. (How to install PHP with FastCGI support)

You need to log in as root to do this.

1. Add users to the operating system

(This is only needed if you haven't added users yet.)

You must add a user account to the operating system for each user that you want to give separate user permissions, in order to deny access to other users' source code.

Let's assume that we need to create three users (fred, george, and ron):


#!ShellExample
# useradd fred
# useradd george
# useradd ron

2. Add user groups to the operating system

You need to add one user group for each user added above. To keep things simple, we just name the user groups similar:


#!ShellExample
# groupadd fred
# groupadd george
# groupadd ron

Now you need to add users to each of these user groups. For each user group, there must be two members: the corresponding user and the lighttpd daemon user.

You configure the user groups by editing /etc/group with your favourite text editor.

The file must look something like this (group numbers may vary):


..... [lots of stuff above]
fred:x:441:fred,lighttpd
george:x:442:george,lighttpd
ron:x:443:ron,lighttpd

You might also use a sed command like this:


sed -i "s/^\(fred.*\)$/\1,fred,lighttpd/g" /etc/group
sed -i "s/^\(george.*\)$/\1,george,lighttpd/g" /etc/group
sed -i "s/^\(ron.*\)$/\1,ron,lighttpd/g" /etc/group

These commands add the user and the lighttpd user to the groups.

3. Set up filesystem structure

Let's assume that you want to keep all files associated with the web-server's virtual hosts under the directory ''/var/www''. (Of course you can choose another location, just make sure that the users created above have read and execute rights to the directory. (I.e. ''chmod 755 /var/www && chown root:root /var/www'').

3.1 Create server root directory

Now, create two directories: One for some start-up scripts that only root have access to, and another for all your virtual hosts:


#!ShellExample
# cd /var/www
# mkdir fastcgi
# mkdir vhosts
# chown lighttpd:lighttpd *
# chmod 755 *

# ls -l /var/www

drwxr-xr-x  2 lighttpd lighttpd 4096 Feb 15 12:17 fastcgi
drwxr-xr-x  9 lighttpd lighttpd 4096 Feb 15 11:21 vhosts

3.2 Create a directory for each virtual host

Now create a directory for each virtual host in the directory ''/var/www/vhosts'', and set up appropriate user rights to them:


#!ShellExample
# cd /var/www/vhosts
# mkdir fred-weasley.com
# mkdir george-weasley.com
# mkdir ron-weasley.com
# chown fred:fred fred-weasley.com
# chown george:george george-weasley.com
# chown ron:ron ron-weasley.com
# chmod 750 *

# ls -l /var/www/vhosts

drwxr-x---  7 fred     fred     4096 Feb 15 20:18 fred-weasley.com
drwxr-x---  6 george   george   4096 Feb 15 11:02 george-weasley.com
drwxr-x---  6 ron      ron      4096 Feb 15 11:23 ron-weasley.com

Now we have created three directories where the three users cannot see each others' files; however, the lighttpd daemon user can see it all.

3.3 Create directory structure for each virtual host

Now, we want to create the directory struture needed for each virtual host:


#!ShellExample
# cd /var/www/vhosts/fred-weasley.com
# mkdir html
# mkdir includes    (optional)
# mkdir logs
# chown fred:fred *
# chown lighttpd:fred logs
# chmod 750 *

# ls -l /var/www/vhosts/fred-weasley.com

drwxr-x---  14 fred     fred   4096 Feb 17 11:55 html
drwxr-x---   2 fred     fred   4096 Feb 15 12:05 includes
drwxr-x---   2 lighttpd fred   4096 Feb 15 11:11 logs

'''You need to repeat this for each virtual host, replacing the user name 'fred' with the appropriate user name.'''

3.4 Create a FastCGI directory for each user

Now we have to do all the fun stuff!

Now, go to the ''/var/www/fastcgi'' directory where we want to create a directory for each user. (When we're finished, these directories will hold the sockets to the FastCGI server processes):


#!ShellExample
# cd /var/www/fastcgi
# mkdir fred
# mkdir george
# mkdir ron
# chown fred:fred fred
# chown george:george george
# chown ron:ron ron
# chmod 750 *

# ls -l /var/www/fastcgi

drwxr-x---  7 fred     fred     4096 Feb 15 20:18 fred
drwxr-x---  6 george   george   4096 Feb 15 11:02 george
drwxr-x---  6 ron      ron      4096 Feb 15 11:23 ron

(Note that the lighttpd user can read all directories, while the three users can only access their own directory.)

4. Create a FastCGI start-up script for each user

Create a directory that will hold all your FastCGI start-up scripts:


#!ShellExample
# cd /var/www/fastcgi
# mkdir startup
# chmod 750 startup

# ls -l /var/www/fastcgi

drwxr-x---  7 fred     fred     4096 Feb 15 20:18 fred
drwxr-x---  6 george   george   4096 Feb 15 11:02 george
drwxr-x---  6 ron      ron      4096 Feb 15 11:23 ron
drwxr-x---  6 root     root     4096 Feb 15 11:23 startup

Now, go to the ''/var/www/fastcgi/startup'' directory, create a start-up script for fred (let's call it fred-startup.sh, using your favourite text editor:


#!sh
#!/bin/sh

## ABSOLUTE path to the spawn-fcgi binary
SPAWNFCGI="/usr/bin/spawn-fcgi" 

## ABSOLUTE path to the PHP binary
FCGIPROGRAM="/usr/bin/php-cgi" 

## bind to tcp-port on localhost
FCGISOCKET="/var/www/fastcgi/fred/fred.socket" 

## uncomment the PHPRC line, if you want to have an extra php.ini for this user
## store your custom php.ini in /var/www/fastcgi/fred/php.ini
## with an custom php.ini you can improve your security
## just set the open_basedir to the users webfolder
## Example: (add this line in you custom php.ini)
## open_basedir = /var/www/vhosts/fred/html
##
#PHPRC="/var/www/fastcgi/fred/" 

## number of PHP childs to spawn in addition to the default. Minimum of 2.
## Actual childs = PHP_FCGI_CHILDREN + 1
PHP_FCGI_CHILDREN=5

## number of request server by a single php-process until is will be restarted
PHP_FCGI_MAX_REQUESTS=1000

## IP adresses where PHP should access server connections from
FCGI_WEB_SERVER_ADDRS="127.0.0.1" 

# allowed environment variables sperated by spaces
ALLOWED_ENV="PATH USER" 

## if this script is run as root switch to the following user
USERID=fred
GROUPID=fred

################## no config below this line

if test x$PHP_FCGI_CHILDREN = x; then
  PHP_FCGI_CHILDREN=5
fi

export PHP_FCGI_MAX_REQUESTS
export FCGI_WEB_SERVER_ADDRS
export PHPRC

ALLOWED_ENV="$ALLOWED_ENV PHP_FCGI_MAX_REQUESTS FCGI_WEB_SERVER_ADDRS PHPRC" 

# copy the allowed environment variables
E=

for i in $ALLOWED_ENV; do
  E="$E $i=$(eval echo "\$$i")" 
done

# clean environment and set up a new one
env - $E $SPAWNFCGI -s $FCGISOCKET -f $FCGIPROGRAM -u $USERID -g $GROUPID -C $PHP_FCGI_CHILDREN

chmod 770 $FCGISOCKET

Please be careful with the paths, USERID and GROUPID.

Note that, in this example, the php process runs as the user we created above
('fred'). This means that the php code will have write access to the html and
php files. This can be convenient, but might be a security risk.
Alternatively, you could set USERID to 'nobody' (or any other user without any
specific permissions), to deny write access to the php process.

You need to repeat the process and create a startup-script for each user in the ''/var/www/fastcgi/startup'' directory. (Just copy the file and replace FCGISOCKET, USERID and GROUPID with the correct values).

Remember to set execute permissions on all your startup-scripts:


#!ShellExample
# cd /var/www/fastcgi/startup
# chmod 750 *

5. Check your PHP configuration

If you're uncertain about the location of your php.ini, just run the following command:


#!ShellExample
$ php-cgi -i | grep php.ini

Please check, that you have the following line in your php.ini:


cgi.fix_pathinfo=1

If you have uncommented the PHPRC line in the shell script under issue 4., be sure that the php.ini has the correct owner an rights. To get things work this must be


chmod 644 php.ini
chown root:root php.ini

6. Execute all FastCGI start-up scripts

Now, fire up all your FastCGI server processes:


#!ShellExample
# /var/www/fastcgi/startup/fred-startup.sh
spawn-fcgi.c.170: child spawned successfully: PID: xxxxx
# /var/www/fastcgi/startup/george-startup.sh
spawn-fcgi.c.170: child spawned successfully: PID: xxxxx
# /var/www/fastcgi/startup/ron-startup.sh
spawn-fcgi.c.170: child spawned successfully: PID: xxxxx

If you get any error messages, please re-check your startup-scripts and the permissions to the ''/var/www/fastcgi'' directory, including all user sub-directories.

7. Configure virtual hosts in the lighttpd server

Edit ''/etc/lighttpd.conf'' in your favourite text-editor:


.....[lots of configuration stuff above].....

$HTTP["host"] =~ "(^|\.)fred-weasley.com$" {
    server.document-root = "/var/www/vhosts/fred-weasley.com/html" 
    accesslog.filename = "/var/www/vhosts/fred-weasley.com/logs/access_log" 
    fastcgi.server = ( ".php" =>
                       (
                          ( "socket" => "/var/www/fastcgi/fred/fred.socket",
                            "broken-scriptfilename" => "enable" 
                          )
                        )
                      )
}

$HTTP["host"] =~ "(^|\.)george-weasley.com$" {
    server.document-root = "/var/www/vhosts/george-weasley.com/html" 
    accesslog.filename = "/var/www/vhosts/george-weasley.com/logs/access_log" 
    fastcgi.server = ( ".php" =>
                       (
                          ( "socket" => "/var/www/fastcgi/george/george.socket",
                            "broken-scriptfilename" => "enable" 
                          )
                        )
                      )
}

$HTTP["host"] =~ "(^|\.)ron-weasley.com$" {
    server.document-root = "/var/www/vhosts/ron-weasley.com/html" 
    accesslog.filename = "/var/www/vhosts/ron-weasley.com/logs/access_log" 
    fastcgi.server = ( ".php" =>
                       (
                          ( "socket" => "/var/www/fastcgi/ron/ron.socket",
                            "broken-scriptfilename" => "enable" 
                          )
                        )
                      )
}

Please note the paths to the FastCGI sockets for each virtual host.

'''server.errorlog is NOT working in conditionals, all errors go to the last logfile specified. So just use one global error log.'''

8. Restart the lighttpd daemon process

Simply run this command:


#!ShellExample
# /etc/init.d/lighttpd restart

If you get any errors, please re-check your ''/etc/lighttpd.conf'' configuration file.

9. Hello World!

Now, log in as the user fred and create a PHP script file in his virtual host (e.g. ''/var/www/vhosts/fred-weasley.com/html/index.php''):


#!php
<?php
echo "<h1>Hello World!</h1>";
echo "<p>Current User ID is: ". posix_getuid();
echo "<p>Current Group ID is: ". posix_getgid();
?>

Also, make sure to set the file permissions:


#!ShellExample
# chown fred:fred /var/www/vhosts/fred-weasley.com/html/index.php
# chmod 640 /var/www/vhosts/fred-weasley.com/html/index.php

# ls -l /var/www/vhosts/fred-weasley.com/html

-rw-r-----   1 fred   fred       116 Jul 25  2004 index.php

Now fire up your web-browser and check the output of your PHP script. (Here: http://www.fred-weasley.com/index.php)

If everything went well, you will see an output showing the User ID of the user fred, and the Group ID of the user group fred. (You can see these IDs in the files ''/etc/passwd_ and _/etc/group'').

10. Automatically start the FastCGI startup scripts

Optionally, you may also create a crontab entry to automatically execute the FastCGI startup scripts when your server boots.

Use the following command to edit your crontab:


# crontab -e

Now add the following line:


@reboot for i in /var/www/fastcgi/startup/*.sh; do $i; done

And finally type ":x" to save and exit.

This crontab entry will execute all .sh files found in the /var/www/fastcgi/startup directory after the server has booted.

Congratulations! You now have a working fast server configuration with individual (separate) user rights.

Limitations

Using this model you are creating a separate pool of fastcgi processes for each user. This means that no memory will be shared between these processes. Therefore, if you use this model for a machine with a large number of users you will need a significant amount of available RAM. Also, if you use any
PHP opcode cache such as xcache, apc or eaccelerator, this model means that each user will get their own dedicated cache (which is a good thing from a security perspective, but bad for memory usage). You can tailor the memory used by having different php.ini files that configure the accelerator with differing cache sizes, and by altering the value of PHP_FCGI_CHILDREN in each user's startup.sh script.

In FreeBSD (6.2) every user can be in a maximum of 14 groups. This is the upper bound for webhost-fastcgi-instances, as your lighty-user (www) needs access to those sockets. I installed my webhost 1-2 years ago in this way and run in trouble a few weeks ago while adding www to it's 15th group. No error-msg gaves a hint. Go, and google for it. By the way: Is there a solution? ;) - Yes, FreeBSD 8.0 will raise this limitation to 1024.

Permissions

mod_fastcgi has an option: check-local. When enabled, Lighty uses his user to check if the file exists in document-root. If you want Lighty's user to not have access to the document-root, this option must be disabled.