I really dislike Apache for it’s terrible RAM usage, so I prefer to use nginx as a web server. nginx does not have built-in PHP support (as module), so PHP can be used only as FastCGI server. Let’s see…
First of all, about PHP. When running PHP as FastCGI server, it is very good to use FPM (FastCGI processes manager). PHP-FPM is currently a patch for PHP, but it was included into the main source, so it will be available with PHP soon. The official FPM website is http://php-fpm.org.
The task of php-fpm is to start worker processes as described it it’s configuration file. The worker processes can be grouped into pools that serve 1 TCP port or UNIX socket. Any pool can be chroot-ed into any directory for security purposes. So, let’s see 2 situations
It is simple, because you don’t need to think about security so much as when serving many users. So, for example, you run a VPS server for your own web projects. In this case using nginx+php-fpm is a very good idea because it can save much RAM that costs money for you. You probably don’t need to chroot your PHP worker processes. So, the configuration will be easy:
<!-- FPM config file fragment --> <section name="pool"> <value name="name">default</value> <value name="listen_address">/tmp/php.sock</value> <value name="listen_options"> <value name="backlog">-1</value> <value name="owner">web</value> <value name="group">web</value> <value name="mode">0666</value> </value> <value name="php_defines"></value> <value name="user">dbb</value> <value name="group">dbb</value> <value name="pm"> <value name="style">static</value> <value name="max_children">4</value> <value name="apache_like"> <value name="StartServers">2</value> <value name="MinSpareServers">1</value> <value name="MaxSpareServers">1</value> </value> </value> <value name="request_terminate_timeout">0s</value> <value name="request_slowlog_timeout">0s</value> <value name="slowlog">logs/slow.log</value> <value name="rlimit_files">1024</value> <value name="rlimit_core">0</value> <value name="chroot"></value> <value name="chdir"></value> <value name="catch_workers_output">yes</value> <value name="max_requests">10</value> <value name="allowed_clients">127.0.0.1</value> <value name="environment"> <value name="HOSTNAME">$HOSTNAME</value> <value name="PATH">/usr/local/bin:/usr/bin:/bin</value> <value name="TMP">/tmp</value> <value name="TMPDIR">/tmp</value> <value name="TEMP">/tmp</value> <value name="OSTYPE">$OSTYPE</value> <value name="MACHTYPE">$MACHTYPE</value> <value name="MALLOC_CHECK_">2</value> </value> </section>
/tmp/php.sock in this example is the UNIX socket for connecting to the FastCGI server,
The corresponding nginx configuration will look like this:
# nginx vhost config
server {
listen 80;
server_name your_website.com;
location / {
root /home/dbb/www/your_website.com;
index redir.php index.php index.html index.htm;
}
location ~ \.php$ {
root html;
fastcgi_pass unix:/tmp/php.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME /home/dbb/www/your_website.com/$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.ht {
deny all;
}
}Just ensure that all the paths here are valid. If something doesn’t work, check your nginx server’s logs.
In this situation everything is easy. You may use little count of php workers because all of them will serve your sites. nginx uses very little RAM, so your total RAM usage will be much less than with Apache.
It is the most complex situation. In this case each user can upload some php shell script and browse the server’s filesysytem. He can look MySQL passwords of the other users, corrupt their data & so on… Also your users are able to run any UNIX command (if you didn’t disable exec/system/passthu/etc in your php.ini) which is insecure, too.
One of possible solutions is using chroot for worker pools. But using chroot causes a huge count of problems. Some of them are listed below:
5–6 worker processes work MUCH as hell more effectively when they all serve all users than when each process serves it’s 1 user. It can cause 502 Bad Gateway & 504 Gateway Timeout errors. Making more processes per user solves the problem, but makes PHP to consume too much RAM.
PHP inside chroot environment cannot resolve domain names. This problem may be solved by creating needed hard links (or copies) in the chroot directory. The needed files are (for Debian lenny): /etc/resolv.conf, /etc/nsswitch.conf, /lib/libnss_dns.so.2. Note that /lib/libnss_dns.so.2 in Debian lenny is a link by itself.
PHP cannot access /tmp directory. The same solution — to create tmp directory inside the chroot environment.
PHP cannot send mail becase it cannot access sendmail. This problem is a little more complex than 2 & 3, because the solution may differ for various MTAs, but another possible solution is to put inside chroot some sendmail alternative that connects to local SMTP server with TCP/IP. There is a little problem: PHP does not run sendmail directly, it requires /bin/sh executable (wtf? why?)… So, you have to put /bin/sh to your chroot environment. I used my system’s /bin/bash for it. Also it is needed to put some dependancies:
us:~# ldd /bin/bash libncurses.so.5 => /lib/libncurses.so.5 (0xb7ee2000) libdl.so.2 => /lib/libdl.so.2 (0xb7ede000) libc.so.6 => /lib/libc.so.6 (0xb7da0000) /lib/ld-linux.so.2 (0xb7f1c000) us:~#
Another possible solution is to try to pass open_basedir setting to a worker before launching user’s script. I tried to use PHP’s auto_prepend_file, but it was not successful. Some days later my friend shade suggested another way — to launch special script security.php which will set security settings and launch the user’s script. I tried it on my website and found this solution to be suitable for me. Let’s see how it works.
In this situation you have to make some changes in your nginx vhost configuration.
fastcgi_param SCRIPT_FILENAME /path/to/your/security.php; fastcgi_param USER_SCRIPT_FILENAME /home/username/www/sitename.co.cc/$fastcgi_script_name;
The structure of the script security.php may vary depending on your directory layout. I have users’ home directories in /home and each of the users has his/her websites in ~/www. So, possible code may look like this example:
<?php
for($d = opendir('/home'); $d_res = readdir($d); true) {
if($d_res == '.' || $d_res == '..') continue;
for($h = opendir('/home/www' . $d_res); $h_res = readdir($h); true) {
if($h_res == '.' || $h_res == '..') continue;
if($h_res == $_SERVER['SERVER_NAME']) {
ini_set('open_basedir', '/home/' . $d_res . ':/tmp:/opt/nginx/html/errors');
}
}
closedir($h);
}
closedir($d);
ini_set('disable_functions', 'ini_set,system,exec,passthru,popen,shell_exec,proc_open,phpinfo');
unset($users);
if(!file_exists($_ENV['USER_SCRIPT_FILENAME'])) {
header('HTTP/1.1 404 Not Found');
readfile('/opt/nginx/html/errors/404.html');
} elseif(!is_readable($_ENV['USER_SCRIPT_FILENAME'])) {
header('HTTP/1.1 403 Forbidden');
readfile('/opt/nginx/html/errors/403.html');
} else {
chdir(dirname($_ENV['USER_SCRIPT_FILENAME']));
require($_ENV['USER_SCRIPT_FILENAME']);
}
?>
It may decrease your PHP perfomance a little, because it scans your directory structure every time. You may solve this problem by changing the main loop in security.php:
$users = array();
$users['user1'] = array('site1.net.ru', 'site2.net.ru', 'site3.net.ru');
$users['user2'] = array('site4.co.cc');
$users['user3'] = array('site5.web.id', 'site6.co.cc');
foreach($users as $username=>$vhosts) {
foreach($vhosts as $hostname) {
if($hostname == $_SERVER['SERVER_NAME']) {
ini_set('open_basedir', '/home/' . $username . ':/tmp:/opt/nginx/html/errors');
break 2;
}
}
}