February 20th, 2007

dice

PHP, suEXEC, FastCGI, and that elusive Right Way.

When you have a multiuser system running a web server, standard practice for CGI and PHP support is to have the server run these scripts as a global user, e.g. www-data. That's braindead. Your script can't keep any data secret, nor can it write any data anywhere without opening that data up to sabotage by another user. Another user on the system may write their own script which, running as that same global user, will read your secrets or trash your saved state.

Part of the problem is that people are obsessed about performance. Industry standard practice is to link PHP into Apache directly, so that Apache doesn't need to so much as fork in order to run a PHP script. Unless you're setting up an oversold shared web server, your performance bottlenecks are going to be the network or within the scripts themselves. I doubt much is to be gained by linking your scripting language implementation directly into the web server. This practice runs against the goal of ensuring each script is run by the user that owns it.

There are answers. FastCGI is an enhancement to CGI to add support for persistent processes, so that bulky interpreters and all their libraries don't have to be loaded each and every time a dynamic page needs to be calculated and sent to a user. suEXEC is a mechanism to execute CGI scripts as the user who owns them.

Some solutions to this problem involve traditional suEXEC/CGI execution of PHP scripts. If you don't mind the (alleged) performance loss of CGI to FastCGI, that's okay. Except that you have to ensure each PHP script is a proper executable on your server: modify the file mode to allow execution, and add a shebang to the top of the script. Some PHP software makes it hard to tell which ".php" files are scripts for end-user consumption, and which are "includes." So, another one of my goals is to avoid having to do any of that. Leave PHP files as 0644, with no shebang.

Apache configuration is opaque and complicated. I'd love to explain in abstract terms what you should do to enable a sane configuration, but I'll have to settle for posting a recipe. My web server runs Apache 2 and PHP 5 on Debian Etch; paths and configuration conventions for Debian are assumed.

  1. For Debian systems, ensure the following packages are installed:
    • apache2: You may opt for the "worker" MPM, "apache2-mpm-worker", since PHP scripts will be run in their own separate processes.
    • php5-cgi: Includes FastCGI support.
    • libapache2-mod-fastcgi
  2. Modify the following configuration files:
    1. /etc/apache2/mods-available/userdir.conf

      Enable CGI and FastCGI for users by adding "ExecCGI" to the Options directive:

      <IfModule mod_userdir.c>
      	...
      	<Directory /home/*/public_html>
      		...
      		#Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec
      		Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec ExecCGI
      		...
      	</Directory>
      </IfModule>
    2. /etc/apache2/mods-available/fastcgi.conf

      FastCGI needs to run user scripts through suEXEC. We can enable suEXEC in this file by specifying "on" for the FastCgiWrapper directive. —HOWEVER— if you do that, you cannot run non-user FastCGI scripts. At all. Ever. If the script is owned by root and lives in /var/www/default, suEXEC will throw a hissy fit over trying to run a script as root. If you use SuexecUserGroup to specify that non-user scripts are run as www-data, suEXEC will throw a similar hissy-fit. The best part is, you can't have different FastCgiWrapper directives in different scopes, so that you might enable the wrapper only for userdirs.

      To work around this mess, we create a trivial hack script and tell FastCGI to always use this.

      <IfModule mod_fastcgi.c>
      	...
      	FastCgiWrapper /usr/local/sbin/fastcgi-suexec-hack
      	...
      </IfModule>
  3. Create our FastCGI wrapper hack script: /usr/local/sbin/fastcgi-suexec-hack

    #!/bin/sh
    # This hack exists exclusively to work around the restriction that
    # FastCGI wrappers (e.g. suEXEC) are an all-or-nothing ordeal.  Thou
    # shalt not enable wrappers for userdirs but not for the whole site.
    # Thou shalt not configure non-userdir FastCGI scripts to use suEXEC
    # or thou shall suffer my wrath of mysterious suexec policy violation
    # notices for 7 generations.
    
    username="$1"
    group="$2"
    application="$3"
    
    case "$(pwd)/" in
    /home/*/public_html/*)
    	exec /usr/lib/apache2/suexec "$username" "$group" "$application";;
    *)
    	application_abs="$(readlink -f "$application")"
    	exec "$application_abs";;
    esac

    Make it executable: sudo chmod 755 /usr/local/sbin/fastcgi-suexec-hack

  4. Enable the following modules: userdir, suexec, actions, fastcgi

    $ cd /etc/apache2/mods-enabled
    $ sudo ln -s ../mods-available/{userdir,suexec,actions,fastcgi}.{load,conf} ./

    (Ignoring errors about missing .conf files.)

  5. Create a new configuration file: /etc/apache2/conf.d/php-fastcgi

    AddType application/x-httpd-php-fastcgi .php
    Action application/x-httpd-php-fastcgi /cgi-bin/php-fastcgi.fcgi

    This bit tells apache to run /usr/lib/cgi-bin/php-fastcgi.fcgi for all PHP scripts, without the need to make them executable or add a shebang line.

  6. Create our PHP/FastCGI wrapper script: /usr/lib/cgi-bin/php-fastcgi.fcgi

    #!/bin/sh
    
    PHP_FCGI_CHILDREN=4
    PHP_FCGI_MAX_REQUESTS=5000
    export PHP_FCGI_CHILDREN PHP_FCGI_MAX_REQUESTS
    
    exec php-cgi

    Make it executable: sudo chmod 755 /usr/lib/cgi-bin/php-fastcgi.fcgi

  7. For each user:

    1. Copy the php-fastcgi.fcgi script to their public_html directory. It cannot be a symlink, because of suEXEC paranoia checks.
    2. Add the following line to the ".htaccess" file in their public_html directory, to direct PHP scripts to their copy of the wrapper:

      # Required to ensure suEXEC runs PHP scripts under our user ID.
      Action application/x-httpd-php-fastcgi /~username/php-fastcgi.cgi

      Unfortunately, since this includes the user's username, this can't be included in an /etc/skel file. You could put it there as a template for the user (or you) to change after an account is created, though.

  8. Create a test script to ensure PHP scripts are running under the correct user ID. Place it in /var/www/default/test-id.php and ~/public_html/test-id.php. The latter copy should probably be owned by you.

    <?php
    
    header ("Content-Type: text/plain");
    system ("id");
    
    ?>

    Recall that you should not be required to make these files executable, or to add shebang lines at the top.

  9. Test it! After restarting Apache, try /test-id.php and /~you/test-id.php. Respectively, you should see uid=33(www-data) gid=33(www-data) groups=33(www-data) and (e.g.) uid=1000(piranha) gid=1000(piranha) groups=1000(piranha)

Questions? Is this broken for you? Chances are I forgot something bone-headedly simple, so get in touch.

References:

  1. Oregon State University OSL Wiki.
  2. mod_fastcgi documentation.
  3. Apache 2.0 documentation.