silverjam и др. Настройка FastCGI и PHP с индивидуальными правами пользователей, 2010

Перевод статьи: Setup FastCGI and PHP with individual user permissions

Автор: silverjam и другие.

Перво-наперво отметим, что этот рецепт является только примером, поэтому не предъявляйте претензий, если предпочитаете делать такие вещи другим способом, или рассердили клиентов или что-то в этом роде...

Execwrap или php-fpm можно использовать в тех же целях, но этот рецепт не касается этих методов.

Замечание: Этот рецепт работает только на операционных системах типа Unix. Я не знаю, как сделать подобное на Windows.

1. Введение

Запуск хостинга веб-сайтов для обслуживания индивидуальных пользователей/клиентов потребует дополнительного доведения до ума настроек вашего веб-сервера.

Для начала, нужно назначить каждому пользователю собственную учётную запись на веб-сервере. Пользователь загрузит файлы своих скриптов PHP в его собственный виртуальный корневой каталог документов.

Нужно, чтобы все файлы скриптов PHP выполнялись с правами того же пользователя, который управляет этим виртуальным узлом. Если это так, то можно быть уверенным, что ни один из пользователей не сможет увидеть чужие скрипты PHP.

Рассмотрим следующий скрипт PHP, выполняемый на веб-сервере без индивидуальных разрешений пользователя (не пытайтесь сделать это, поскольку вы можете кончить тем, что полиция постучится в вашу дверь!):

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

Этот сценарий прочитает и покажет исходный код скрипта PHP какого-то другого пользователя. Исходный код может содержать пароли, используемые для подключения к пользовательским базам данных MySQL, или другую интересную информацию. Можно даже сделать скрипт PHP, который сможет перезаписать файлы в каталогах пользователя другого виртуального узла!

Мы хотим этого избежать!

Как насчёт встроенного в PHP безопасного режима safe_mode?

Я не хочу сказать что-то плохое о PHP, но я не рекомендую использовать PHP с возможностями встроенного безопасного режима safe_mode. (Обратитесь к документации по safe_mode на php.net за более подробным описанием.)

Однако, некоторые настройки php.ini могут предотвратить или замедлить большинство видов атак без необходимости изменять исходный код. Для предотвращения удалённого доступа из php воспользуйтесь allow_url_fopen, а для предотвращения вставки удалённых файлов в php вы можете воспользоваться allow_url_include. Настройка open_basedir - это хороший способ усложнить работу взломщика, но этот способ не заменит разрешений пользователей. Для замедления некоторых видов угона сеансов можно отключить опцию session.use_trans_sid.

Но всё же будет лучше, если положиться на возможности ограничения доступа пользователей, встроенные в операционную систему.

2. Установка

Подразумевается, что на сервере уже установлены Lighttpd и PHP с поддержкой FastCGI. (Как установить PHP с поддержкой FastCGI.)

Чтобы выполнить установку, нужно войти под пользователем root.

2.1. Добавим пользователей в операционную систему

(Это необходимо только в том случае, если пользователи ещё не добавлены.)

Нужно добавить учётную запись пользователя в операционную систему для каждого пользователя, права которого требуется отделить, чтобы предотвратить возможность доступа к чужому исходному коду.

Предположим, что таких пользователей три (fred, george и ron).

Выполним следующие команды:

# useradd fred
# useradd george
# useradd ron

2.2. Добавим группы пользователей в операционную систему

Нужно добавить по одной группе для каждого из добавленных выше пользователей. Для простоты назовём группы так же, как и пользователей:

Выполним следующие команды:

# groupadd fred
# groupadd george
# groupadd ron

Теперь нужно добавить каждого пользователя в его группу. В каждой группе пользователей будет два члена: сам пользователь и пользователь демона lighttpd.

Группы пользователей можно настроить, отредактировав файл /etc/group в любом текстовом редакторе.

Файл должен выглядеть примерно так (номера групп могут отличаться):

... [пропущенные строки]
fred:x:441:fred,lighttpd
george:x:442:george,lighttpd
ron:x:443:ron,lighttpd

Можно также воспользоваться командой sed:

# 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

Эти команды добавят самого пользователя и пользователя lighttpd в группы.

2.3. Настроим структуру файловой системы

Предположим, что мы хотим сохранить все связанные с веб-сервером файлы виртуальных узлов в подкаталогах "/var/www". (Конечно, вы можете выбрать другое место, просто удостоверьтесь, что созданные пользователи имеют права на чтение и выполнение их каталогов. (Например, "chmod 755 /var/www && chown root:root /var/www").

2.3.1 Создадим корневой каталог

Теперь, создадим два каталога: один для нескольких сценариев запуска, к которому будет иметь доступ только пользователь root, и другой для всех виртуальных узлов:

Выполним следующие команды:

# 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

2.3.2 Создадим каталог для каждого из виртуальных узлов

Теперь создадим каталог для каждого из виртуальных узлов в каталоге "/var/www/vhosts" и зададим соответствующие права для их пользователей:

Выполним следующие команды:

# 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

Теперь у нас есть три каталога, в которых три пользователя не могут увидеть файлы друг друга, однако демон lighttpd может видеть их все.

2.3.3 Создадим структуру каталогов для каждого виртуального узла

Теперь нужно создать структуру каталогов для каждого виртуального узла:

Выполним следующие команды:

# cd /var/www/vhosts/fred-weasley.com
# mkdir html
# mkdir includes (не обязательно)
# 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

Нужно повторить эти команды для каждого из пользователей виртуального узла, заменяя имя пользователя "fred" на имя соответствующего пользователя.

2.3.4 Создадим каталог FastCGI для каждого пользователя

Теперь мы готовы приступить к самому интересному!

Теперь, перейдём в каталог "/var/www/fastcgi", где мы хотим создать каталог каждого из пользователей. (Когда мы закончим, эти каталоги будут содержать сокеты процессов сервера FastCGI):

Выполним следующие команды:

# 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

(Отметим, что пользователь lighttpd может читать все каталоги, в то время как три пользователя имеют доступ только к собственным каталогам.)

2.4. Создадим скрипт запуска FastCGI для каждого пользователя

Создадим каталог, который будет содержать все скрипты запуска FastCGI, выполнив следующие команды:

# 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

Теперь, перейдём в каталог "/var/www/fastcgi/startup", создадим скрипт запуска для пользователя fred (давайте назовём его fred-startup.sh), воспользовавшись любым текстовым редактором:

Скрипт оболочки:

#!/bin/sh

## АБСОЛЮТНЫЙ путь к двоичному файлу spawn-fcgi
SPAWNFCGI="/usr/bin/spawn-fcgi" 

## АБСОЛЮТНЫЙ путь к двоичному файлу PHP
FCGIPROGRAM="/usr/bin/php-cgi" 

## Прослушивание порта TCP на localhost
FCGISOCKET="/var/www/fastcgi/fred/fred.socket" 

## Раскомментируйте строку PHPRC, если имеется дополнительный файл php.ini для этого пользователя
## Положите этот файл php.ini в /var/www/fastcgi/fred/php.ini
## С помощью этого файла php.ini можно увеличить безопасность системы
## Просто настройте параметр open_basedir на веб-каталоги пользователей
## Например: (добавьте эту строку в ваш настроенный файл php.ini)
## open_basedir = /var/www/vhosts/fred/html
#PHPRC="/var/www/fastcgi/fred/" 

## Количество детей PHP порождаемых дополнительно к основным. Минимум 2.
## Действительное количество детей = PHP_FCGI_CHILDREN + 1
PHP_FCGI_CHILDREN=5

## Количество запросов выполненных сервером к одному процессу php, прежде чем этот процесс будет перезапущен
PHP_FCGI_MAX_REQUESTS=1000

## IP-адреса, с которых PHP должен принимать соединения серверов
FCGI_WEB_SERVER_ADDRS="127.0.0.1" 

# Доступные переменные окружения, разделённые пробелами
ALLOWED_ENV="PATH USER" 

## Если этот скрипт запускается от имени пользователя root, то fastcgi переключится на следующего пользователя:
USERID=fred
GROUPID=fred

################## После этой строки настроек нет
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"

# Скопируем доступные переменные окружения
E=
for i in $ALLOWED_ENV; do
  E="$E $i=$(eval echo "\$$i")" 
done

# Очистим переменные окружения и настроим новые
env - $E $SPAWNFCGI -s $FCGISOCKET -f $FCGIPROGRAM -u $USERID -g $GROUPID -C $PHP_FCGI_CHILDREN
chmod 770 $FCGISOCKET

Будьте аккуратны с путями, USERID и GROUPID.

Отметим, что в этом примере процессы php запускаются от имени пользователя, созданного ранее ("fred"). Это означает, что код php будет иметь права доступа к файлам html и php. Это может быть удобно, но может быть менее безопасно.

В качестве альтернативы можно задать USERID значение "nobody" (или любого другого пользователя без каких-либо особых разрешений), чтобы запретить процессу php запись.

Нужно повторить процесс и создать скрипт запуска для каждого пользователя из каталога "/var/www/fastcgi/startup". (Просто скопируйте фалй и замените FCGISOCKET, USERID и GROUPID на правильные значения).

Не забудьте установить разрешения на выполнение всех скриптов запуска:

Выполним следующие команды:

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

2.5. Проверим настройку PHP

Если вы не знаете точно, где находится файл php.ini, просто выполните следующие команды:

$ php-cgi -i | grep php.ini

Удостоверьтесь, что в файле php.ini имеется следующая строка:

cgi.fix_pathinfo=1

Если вы раскомментировали строку PHPRC в скрипте из раздела 4, проверьте владельца и права доступа к файлу php.ini. Чтобы он использовался, нужно выполнить команды:

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

2.6. Запустим все скрипты запуска FastCGI

Теперь запустим все процессы-серверы FastCGI, выполнив следующие команды:

# /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

Если будут какие-то сообщения об ошибках, проверьте скрипты запуска и права доступа к каталогу "/var/www/fastcgi", включая все подкаталоги.

2.7. Настроим виртуальные узлы на сервере lighttpd

Отредактируем файл "/etc/lighttpd.conf" в любом текстовом редакторе:

...[некоторое количество строк конфигурации]...
$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" 
      )
    )
  )
}

Обратите внимание на пути к сокетам FastCGI каждого виртуального узла.

Условные блоки НЕ оказывают действия на server.errorlog, все сообщения об ошибках попадают в последний указанный файл журнала. Поэтому используется один глобальный журнал.

2.8. Перезапустим процесс демона lighttpd

Просто выполним следующую команду:

# /etc/init.d/lighttpd restart

Если будут какие-то сообщения об ошибках, проверьте файл конфигурации "/etc/lighttpd.conf".

2.9. Здравствуй мир!

Теперь, войдём под пользователем fred и создадим скрипт PHP в его виртуальном узле (например, "/var/www/vhosts/fred-weasley.com/html/index.php"):

<?php
  echo "<h1>Здравствуй мир!</h1>";
  echo "<p>Идентификатор текущего пользователя: ". posix_getuid();
  echo "<p>Идентификатор текущей группы: ". posix_getgid();
?>

Убедитесь, что вы задали разрешения на доступ к файлу:

Выполните следующие команды:

# 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

Теперь, запустим веб-браузер и проверим вывод скрипта PHP. (Здесь: http://www.fred-weasley.com/index.php)

Если всё хорошо, вы увидите страницу, на которой будут отображены идентификаторы пользователя и группы fred. (Вы можете увидеть эти идентификаторы в файлах "/etc/passwd" и "/etc/group").

2.10. Автоматический вызов скриптов запуска FastCGI

При желании можно также создать запись в планировщике задач crontab для автоматического вызова скриптов запуска FastCGI при загрузке сервера.

Воспользуемся следующей командой для редактирования заданий планировщика crontab:

# crontab -e

Теперь добавим следующую строку:

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

А затем введём ":x" для сохранения и выхода.

Эта запись в планировщике задач crontab выполнит все файлы с расширением .sh, найденные в каталоге /var/www/fastcgi/startup, после загрузки сервера.

Поздравляю! Теперь у вас есть быстрый сервер, настроенный с отдельными правами для пользователей.

3. Ограничения

Таким образом мы создали отдельные группы процессов fastcgi для каждого пользователя. Это означает, что у этих процессов нет общей памяти. Поэтому, если этот способ используется на компьютере с большим количеством пользователей, потребуется значительное количество доступной оперативной памяти. Также, если используется какой-нибудь кэш транслированного кода PHP, например, xcache, apc или eaccelerator, этот способ приведёт к созданию собственного кэша у каждого из пользователей (что полезно для повышения безопасности, но приводит к увеличению использования памяти). Можно уменьшить использование памяти с помощью файлов php.ini, в которых настроить ускоритель с разными объёмами кэша, а также изменить значение переменной PHP_FCGI_CHILDREN в каждом из пользовательских скриптов startup.sh.

В FreeBSD (6.2) каждый пользователь может состоять максимум в 14 группах. Этот верхний предел экземпляров fastcgi для виртуальных узлов, поскольку пользователю lighttpd (www) необходим доступ к этим сокетам. Я настроил мой веб-сервер 1-2 года назад этим способом и столкнулся с проблемой несколько недель назад, когда добавил пользователя www в пятнадцатую группу. Не было никаких сообщений об ошибках, иди-свищи ответа у google. Но всё-таки, есть ли решение? ;) Да, в FreeBSD 8.0 этот предел был поднят до 1024.

4. Разрешения

У mod_fastcgi имеется опция check-local. Если она включена, Lighttpd использует своего пользователя для проверки существования файла в document-root. Если вы хотите, чтобы пользователь Lighttpd не имел доступа к document-root, нужно отключить эту опцию.

5. Примечания переводчика:

Для порядка я иногда перевожу материалы, которыми собираюсь воспользоваться. Начав делать этот перевод, я сразу же пожалел о том, что ввязался в это гиблое дело - статья разжёвывает элементарные вещи вроде заведения пользователей, групп, членство в группах и права доступа. Ну и запуск сценариев инициализации из cron'а я, конечно, не одобряю. Кроме того, автор использовал FreeBSD 6.2, в которой существовало ограничение на максимальное количество групп для одного пользователя. В качестве решения автор предлагает перейти на FreeBSD 8.0, однако я думаю, что есть по меньшей мере ещё два решения: 1. воспользоваться TCP-сокетами, вместо сокетов домена Unix, 2. воспользоваться каким-нибудь дистрибутивом Linux.

Написать автору перевода