PHP

WardPane запускает PHP для WordPress, Laravel, Symfony и любых других PHP-сайтов. У каждого PHP-сайта свой php-fpm и свой пользователь Linux, а работает он в песочнице, из которой не видно ни других сайтов, ни панели. Взломанный плагин на одном сайте не прочитает и не изменит остальные, да и код собственного сайта поменять не сможет.

Включение PHP

Сначала PHP должен появиться на сервере. Установите его командой:

sudo ./install.sh --php

Если запустить install.sh из терминала, он спросит, ставить ли PHP. При обновлениях PHP сохраняется, если он уже установлен. На странице Среды выполнения показаны версии PHP, которые нашла панель (см. Среды выполнения).

Затем выберите PHP обработчиком сайта в окне создания или на вкладке Обработчик. После Сохранить изменения панель за несколько секунд настроит php-fpm сайта и покажет его состояние вверху настроек PHP.

Как это работает

Отдельный php-fpm для каждого сайта

У каждого PHP-сайта свой мастер-процесс php-fpm в отдельном юните systemd, wardpane-php@<id>.service. Общего пула нет, так что процессы, opcache и сессии у сайтов не пересекаются.

  • Сайты, созданные в 2.0, запускают PHP от имени своего владельца (по умолчанию www-root).
  • Сайты, созданные до 2.0, работают от имени собственного пользователя Linux, wp_<id>.

Общий php8.3-fpm.service из дистрибутива замаскирован: он работает от имени www-data, а эта группа может иметь право записи во все сайты.

Песочница

php-fpm каждого сайта работает в песочнице systemd:

  • других сайтов, /opt/wardpane, /var/lib/wardpane, /home и /root внутри неё не существует;
  • вся система доступна только для чтения;
  • никаких привилегий и никаких capabilities;
  • ограничения на память, процессор и количество процессов.

В режиме по умолчанию, PHP_SANDBOX=strict, PHP не может запустить и ни одну программу (файловая система смонтирована с noexec, кроме самого php-fpm и его библиотек), а системные вызовы фильтруются.

Сам PHP настроен так:

  • open_basedir ограничен папкой сайта (а для новых сайтов ещё и папкой tmp/ владельца);
  • exec, system, shell_exec, proc_open, popen, putenv и подобные функции отключены;
  • файлы .user.ini не читаются, и задано expose_php = Off;
  • sendmail_path = /bin/false, поэтому mail() не может запустить shell. Вместе с отключённым putenv это закрывает трюк с LD_PRELOAD.

Панель никогда не получает root

Сама панель работает от имени непривилегированного пользователя. Она записывает нужное состояние в /var/lib/wardpane/php/desired.json, а небольшой root-помощник (bin/site-sync.js, который systemd запускает при каждом изменении этого файла) создаёт пользователей, папки, конфигурации и юниты. Помощник заново проверяет всё, что приходит от панели.

Если настройки сайта не проходят проверки помощника, сайт продолжает работать с прежней конфигурацией, а панель показывает ошибку с пометкой «продолжают действовать прежние настройки».

Код только для чтения и папки с правом записи

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

  • закрытые папки сайта: tmp/ и logs/ (и data/ для сайтов, созданных до 2.0, у которых tmp/ и logs/ тоже лежат рядом с public/);
  • папки, перечисленные в поле Папки с правом записи на вкладке Обработчик, по одной на строку, относительно папки сайта.

Папки с правом записи для популярных приложений:

Приложение Папки с правом записи
WordPress wp-content/uploads (на сайте из 1.x: public/wp-content/uploads)
Laravel storage, bootstrap/cache

Храните базы данных SQLite и другие закрытые файлы вне корня документов. На сайтах, созданных до 2.0, для них есть папка data/ рядом с public/.

Папка с правом записи не может находиться внутри другой папки с правом записи (uploads и uploads/cache): укажите только внешнюю, внутренняя доступна для записи как её часть. Иначе взломанный сайт мог бы заменить внутреннюю папку символической ссылкой, и systemd смонтировал бы в песочницу чужую папку. Настройки с такими вложенными папками, сохранённые более старой версией, очищаются автоматически, без ослабления защиты.

Никакого PHP из папок с правом записи

PHP никогда не выполняется из папки с правом записи: запрос к загруженному uploads/shell.php получит 404.

Чтобы ставить плагины и обновления из своей админки, WordPress должен выполнять код из папок с правом записи. Если вам это нужно, отметьте Выполнять .php из папок с правом записи, но учтите, что тогда выполнится и загруженный злоумышленником скрипт. При выключенной опции ставьте плагины и обновления по SFTP или через WP-CLI из терминала.

Маршрутизация

Маршрутизация работает как try_files $uri $uri/ /index.php в nginx:

  • существующие файлы отдаются как статические;
  • файлы .php выполняются, с поддержкой PATH_INFO (/index.php/route работает);
  • если отмечено Отправлять неизвестные пути в /index.php, всё остальное уходит в /index.php. Это нужно WordPress, Laravel и Symfony для красивых URL.

Если обработка .htaccess включена и для пути запроса есть правила .htaccess с RewriteEngine On и хотя бы одним RewriteRule, маршрутизацию определяют эти правила, и запасного перехода в /index.php нет, как и в Apache. См. .htaccess.

Папка /.well-known/ (файлы для внешних сервисов, например для подтверждения домена) всегда отдаётся как статические файлы: скрипты в ней не выполняются, и такие запросы никогда не уходят в /index.php.

Исходные файлы PHP (.php, .php5, .inc, .phtml, .phar, .phps) никогда не отдаются как файлы. Это касается и PHP-сайтов, и статических, и сайтов, где PHP уже отключили. То же с их резервными копиями: wp-config.php.bak, config.php~, config.php.2024-01-31, .old, .orig, .save, .swp и похожие окончания, а также автосохранения Emacs #...#. x.php.txt и x.php.html остаются обычными файлами.

Что видят скрипты

  • REMOTE_ADDR содержит настоящий IP посетителя, в том числе за Cloudflare.
  • HTTPS=on, когда посетитель пришёл по HTTPS, в том числе через Cloudflare в режиме Flexible, поэтому WordPress не зацикливается.
  • X-Forwarded-For, CF-Connecting-IP и подобные заголовки скриптам не передаются: их можно подделать, а настоящий адрес уже есть в REMOTE_ADDR.
  • Пароль сайта (HTTP Basic из настроек защиты панели) скриптам не виден.

Bot Fight на PHP-сайтах

На PHP-сайте Bot Fight пропускает формы и API (POST, PUT, PATCH, DELETE) и не считает пути вроде /wp-json/... поиском уязвимостей, потому что их может обслуживать index.php. Сканеры всё равно ловятся по ответам 404, которые возвращает PHP.

Настройки

Настройки PHP находятся на вкладке Обработчик сайта.

Параметр По умолчанию Диапазон Значение
Версия Новейшая установленная установленные версии Версия PHP для сайта
Память на запрос, МБ 128 от 32 до 2048 memory_limit
Лимит загрузки, МБ 32 от 1 до 2048 upload_max_filesize и post_max_size
Лимит времени, секунд 30 от 5 до 600 max_execution_time
Воркеры 4 от 1 до 64 Сколько запросов выполняется одновременно
Лимит CPU, % 100 от 10 до 800 100% означает одно ядро, на весь сайт

Панель показывает, сколько всего оперативной памяти может использовать сайт при выбранном числе воркеров и памяти на запрос.

Флажки:

  • Отправлять неизвестные пути в /index.php - красивые URL в WordPress, Laravel, Symfony.
  • Запретить исходящие соединения - сайт не может обращаться к внешним API, отправлять почту по SMTP или что-либо скачивать, а взломанный сайт не может атаковать других.
  • Показывать ошибки в браузере - только для отладки, никогда на рабочем сайте.
  • Выполнять .php из папок с правом записи - см. выше.

Где лежат файлы

Новые сайты (2.0), владелец www-root, домен example.com:

/var/www/www-root/data/www/example.com             папка сайта (для PHP только чтение)
/var/www/www-root/data/tmp                         сессии и загрузки (запись разрешена)
/var/www/www-root/data/logs/example.com-php-error.log   ошибки скриптов сайта
/etc/wardpane/php/<id>/                            php.ini и php-fpm.conf (создаются панелью, не редактировать)

Сайты, созданные до 2.0:

/var/www/example.com/public              корень документов (для PHP только чтение)
/var/www/example.com/data                закрытые файлы, SQLite (запись разрешена)
/var/www/example.com/tmp                 сессии и загрузки (запись разрешена)
/var/www/example.com/logs/php-error.log  ошибки скриптов сайта
/etc/wardpane/php/<id>/                  php.ini и php-fpm.conf (создаются панелью, не редактировать)

Журнал ошибок PHP открывается в панели кнопкой Показать журнал ошибок PHP. Когда он дорастает до 10 МБ, от него остаётся последний 1 МБ.

Что нужно знать

  • Установленные расширения: mysql, sqlite3, curl, gd, mbstring, xml, zip, intl, bcmath, opcache.
  • WardPane 2.0 не устанавливает MySQL и MariaDB. WordPress и другим приложениям, которым нужна такая база, подойдёт внешний сервер баз данных или СУБД, которую вы сами поставили на этот сервер. Базы данных в панели появятся в версии 2.1; см. Базы данных.
  • mail() всегда сообщает «не отправлено». Отправляйте почту по SMTP с помощью SMTP-плагина или библиотеки. Почта на сервере появится в версии 2.5; см. Почта.
  • Общего лимита на длительность запроса нет, поэтому медленная загрузка большого файла не обрывается. Запрос обрывается, если его тело перестало поступать или посетитель перестал читать ответ дольше, чем на max_execution_time плюс 30 секунд; в тот же момент освобождается его место в очереди php-fpm.
  • После первых 10 секунд тело запроса должно поступать в среднем со скоростью 500 байт в секунду (иначе 408), а у одного посетителя (адреса, для IPv6 его /64) может быть не больше 4 запросов к сайту, тело которых ещё передаётся (иначе 429). Без этих ограничений один посетитель мог бы занять медленными загрузками все слоты php-fpm.
  • Файлы сайта должны быть доступны для чтения всем (644 для файлов, 755 для папок). После загрузки по SFTP обычно так и есть.

Применить заново и исправить права

Кнопка Применить заново и исправить права на вкладке Обработчик возвращает PHP права на папки с правом записи и перезапускает php-fpm сайта. Используйте её после того, как загрузили файлы в папку с правом записи (например, uploads) от имени root, или когда PHP не может писать в папку, куда писать должен.

Решение проблем

Если php-fpm не запускается в песочнице strict (панель показывает журнал ошибок), переключитесь на базовую песочницу: задайте PHP_SANDBOX=basic в Настройки > PHP (или в /opt/wardpane/.env), перезапустите панель и сообщите о проблеме. Базовая песочница сохраняет всё, кроме запрета на запуск программ и фильтра системных вызовов.

Сообщения самого php-fpm (а не ваших скриптов) попадают в журнал systemd:

journalctl -u wardpane-php@<id>        # php-fpm одного сайта
journalctl -t wardpane-php-<id>        # то же самое, по тегу
journalctl -u 'wardpane-php@*'         # все PHP-сайты
journalctl -u wardpane-sync            # root-помощник, применяющий настройки

Чтобы узнать <id> сайта, посмотрите содержимое /etc/wardpane/php/: там по одной папке на каждый PHP-сайт, и в её php-fpm.conf указан домен.