php-fpm и open_basedir

Внимание! Статья утратила свою актуальность. Смотрите комментарии.

Есть широко распространённое мнение, что реализовать поддержку open_basedir на веб-сервере с nginx и PHP-FPM невозможно. 99% сайтов предлагают следующее решение: для каждого пользователя хостинга заводить свой пул рабочих процессов PHP. Это чрезвычайно неэффективно.

В процессе администрирования веб-сервера в рамках моего собственного онлайн-проекта мной совместно с моим товарищем shade было найдено решение этой проблемы, позволяющее эффективно использовать open_basedir в такой связке. Что значит эффективно? Это значит, что у всех запущенных рабочих процессов будет возможность обслуживать всех пользователей.

Решение это предельно просто и, как мне кажется, к нему никто не приходит по одной простой причине — в силу чрезмерной наводнённости интернета слухами о невозможности решения этой задачи. Утверждение о невозможности порой можно встретить в духе «даже не надейтесь», «даже не пытайтесь» и так далее.

Суть решения состоит в том, чтобы FastCGI-серверу передавать на обработку не запрашиваемый файл, а некоторый промежуточный файл, который потом самостоятельно обратится к необходимому. Это невозможно сделать при помощи auto_prepend_file в конфигурации PHP, но это не составит труда сделать средствами nginx.

fastcgi_param SCRIPT_FILENAME /path/to/your/security.php;
fastcgi_param USER_SCRIPT_FILENAME $document_root/$fastcgi_script_name;

Содержимое файла security.php может включать любые инструкции, в том числе и задание требуемых параметров безопасности. Главное — не забыть о том, что перед передачей управления пользовательскому сценарию нужно запретить модификацию этих параметров.

<?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;
		}
	}
}

// см. комментарий ниже
//ini_set('disable_functions', 'ini_set,system,exec,passthru,popen,shell_exec,proc_open');
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']);
}

?>

Разумеется, ничто не мешает вам реализовать сопоставление пользователей и виртуальных узлов так, как вам это нужно. Например, хранить их в отдельном XML-файле или даже сканировать ФС при каждом обращении (если нагрузка не очень высокая).

По материалам собственных публикаций на форуме Шаманграда.

12 комментариев: php-fpm и open_basedir

  1. WST говорит:

    Кстати, нужно сказать, что disable_functions всё-таки не срабатывает. Тем не менее, переопределить open_basedir при помощи ini_set в пользовательском коде уже не удаётся… Странное поведение. Скорее всего, ini_set для одной и той же конфигурационной директивы может быть выполнен только единожды, что, однако, не отражено в документации: http://ru.php.net/ini_set

  2. Avari говорит:

    По памяти: не всякую переменную из php.ini можно (пере)определить, скажем, в апачевском конфиге vhost’а через php_admin_value. Здесь должно быть то же самое.
    Немножко поиска…
    …For example, some settings may be set within a PHP script using ini_set(), whereas others may require php.ini or httpd.conf.
    disable_functions: php.ini only
    В общем, это ограничение php.
    Ничего особо страшного… но фишка php-fpm c запуском от имени разных пользователей и с разными php.ini по-прежнему вне конкуренции.

  3. WST говорит:

    У каждого подхода свой недостаток. Конкуренция у Вашего способа серьёзная, хотя бы потому что nginx в паре с php-fpm чаще всего ставят как легковесную альтернативу Apache на слабых VPS, а на слабых VPS остро стоит проблема использования ОЗУ. Если делать на каждого пользователя свой пул воркеров, ресурсы такой системы будут быстро исчерпаны.

  4. WST говорит:

    По крайней мере, open_basedir задаётся успешно. Возможно, именно по той причине, что он не определён ни в php.ini, ни в php-fpm.xml.

  5. Артем говорит:

    location ~* \.php$
    {
    fastcgi_pass unix:/tmp/fastcgi.socket;
    fastcgi_index index.php;
    fastcgi_param DOCUMENT_ROOT /hc45.ru;
    fastcgi_param SCRIPT_FILENAME /securityhandler.php;
    fastcgi_param USER_SCRIPT_FILENAME $document_root/$fastcgi_script_name;
    include /usr/local/etc/nginx/fastcgi_params;

    Не подскажете куда копнуть ?

    FastCGI sent in stderr: «PHP Notice: Undefined index: USER_SCRIPT_FILENAME in /securityhandler.php on line 19″

  6. Артем говорит:

    php.ini, как оказалось, успешно справляется вот с таким:

    [HOST=hostname]
    open_basedir = somedir

    мне кажется, элегантнее будет.

  7. test говорит:

    я не могу понять если вы используете

    ini_set(‘open_basedir’, ‘/home/’ . $username . ‘:/tmp:/opt/nginx/html/errors’);

    значит в скрипте МОЖНО поставить open_basedir? Если мне надо для одного сайта сделать, я могу просто в php.ini ее выставить, это будет работать, или мне надо вверху каждого скрипта сайта прописывать

    ini_set(‘open_basedir’, ‘/home/’)

  8. WST говорит:

    test, ini_set для параметра open_basedir прокатывает только один раз, то есть если задать его в первом обрабатываемом скрипте, то в скрипте пользователя изменить это значение уже не получится. Мне не известно, является ли это стандартным поведением, но я просто пользуюсь этим и делюсь.

    Артём, выглядит действительно много элегантнее, попробую — отпишусь. Особенно интересно по той причине, что я с товарищами в скором будущем заведу коммерческий хостинг с nginx.

    P.S: недавно публиковал небольшое обновление по ссылке на источник

  9. test говорит:

    Ну что там, в php.ini корректно у вас все работает, затестируйте уже, тут весь рунет ждет :)

  10. WST говорит:

    test, да, работает. Как давно ввели эту штуку? Имеет смысл воспользоваться ею…
    P.S.: рунет бы лучше сам затестил вместо того, чтобы ждать лентяя :)

  11. test говорит:

    Добавлю немного. Если ставить байсдир в php.ini это распространяется в том числе и на скрипты, которые запускаются с командной строки, то есть вы будете везде приввязаны к одному байс диру. Как вариант можно просто во всех своих скриптах инклудить один скрипт, который будет

    ini_set(‘open_basedir’ …

    ставить.

  12. WST говорит:

    test, речь об указании open_basedir в специальной секции вида [HOST=…]. Опции в этой секции, по всей видимости, применяются только к запросам в рамках данного хоста. Что такое хост определить для командной строки затруднительно, поэтому вряд ли эти секции влияют на работу скриптов для командной строки. По крайней мере, мои скрипты (обновляющие RRD-базы итп) остались работоспособными после того как я добавил одну такую секцию…

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*

Можно использовать следующие HTML-теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>