
Безопасная настройка Nginx и PHP-FPM: чек-лист для сервера
Безопасная связка Nginx и PHP-FPM: скрыть версии, закрыть выполнение PHP в лишних каталогах, изолировать пулы, включить TLS и лимиты.
Безопасная настройка Nginx и PHP-FPM
Чтобы связка Nginx и PHP-FPM была безопасной, нужно сделать четыре вещи: передавать в PHP только реально существующие скрипты, запускать каждый сайт в отдельном пуле под своим пользователем, отключить опасные функции и раскрытие информации в php.ini, а также закрыть периметр через TLS и лимиты запросов.
В статье собран практический чек-лист: конфигурации, значения директив и порядок внедрения, который можно применить на VPS или выделенном сервере без остановки сайта.
Почему связка Nginx и PHP-FPM нуждается в укреплении
Nginx сам не умеет выполнять PHP. Он принимает запрос, определяет, что это динамический скрипт, и по протоколу FastCGI передаёт его менеджеру процессов PHP-FPM. Именно на этом стыке чаще всего возникают проблемы: веб-сервер передаёт в PHP больше, чем должен, а PHP-процесс получает больше прав, чем ему нужно.
Типичные сценарии атак выглядят так. Злоумышленник загружает через форму файл с расширением изображения, но с PHP-кодом внутри, и добивается его выполнения. Либо через уязвимость в одном плагине получает доступ ко всем сайтам на сервере, потому что все они работают от одного пользователя. Либо читает файлы `.env`, `.git` и резервные копии, которые случайно оказались в корне сайта.
Хорошая новость: все эти сценарии закрываются обычными настройками, которые не требуют дополнительного ПО. Ниже разберём защиту по слоям: от внешнего периметра до прав отдельного PHP-процесса.
TLS и лимиты
HTTPS, актуальные протоколы, limit_req и limit_conn отсекают перебор и грубые нагрузки.
Фильтрация запросов
Скрытые версии, запрет служебных файлов, ограничение методов, try_files для PHP.
Доступ к PHP-FPM
Unix-сокет с правами 0660, отдельный сокет и пул для каждого сайта.
Права процесса
Свой пользователь, open_basedir, disable_functions, запрет вывода ошибок.
Укрепление Nginx: базовые директивы
Начните с настроек, которые не влияют на работу сайта, но сразу сокращают поверхность атаки.
Скройте версию сервера. В блоке `http` добавьте `server_tokens off;`. Сканеры больше не увидят точную версию Nginx в заголовке ответа и на страницах ошибок.
Закройте служебные файлы. Запретите доступ ко всем скрытым файлам и каталогам, кроме `.well-known`, который нужен для выпуска сертификатов: `location ~ /\.(?!well-known) { deny all; }`. Так вы защитите `.git`, `.env`, `.htaccess` и `.htpasswd`.
Ограничьте методы. Большинству сайтов достаточно GET, HEAD и POST. Остальные можно отсекать через `if ($request_method !~ ^(GET|HEAD|POST)$) { return 405; }`.
Добавьте защитные заголовки. `X-Content-Type-Options: nosniff`, `X-Frame-Options: SAMEORIGIN` и `Referrer-Policy: strict-origin-when-cross-origin` задаются директивой `add_header ... always;`. Для сложных сайтов подберите отдельную политику `Content-Security-Policy`, начиная с режима отчётов, чтобы не сломать скрипты.
Ограничьте размер тела запроса. Директива `client_max_body_size` должна соответствовать реальным потребностям: если на сайте нет загрузки больших файлов, хватит 2–10 МБ.
server_tokens off;
location ~ \.php$ {
try_files $uri =404;
include fastcgi_params;
fastcgi_pass unix:/run/php/site.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
location ~* ^/(uploads|storage|cache)/.*\.php$ {
deny all;
}
location ~ /\.(?!well-known) {
deny all;
}Что делает этот блок конфигурации
Главное здесь — директива `try_files $uri =404;` внутри `location` для PHP. Она проверяет, существует ли запрошенный файл, и если нет, сразу возвращает 404, не обращаясь к PHP-FPM. Благодаря этому закрывается целый класс атак, когда в путь подставляют лишние сегменты вроде `/image.jpg/evil.php`.
Вторая важная строка — `fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;`. Она явно задаёт, какой именно скрипт выполнит PHP. В современных конфигурациях это уже есть в `fastcgi.conf`, но при ручной настройке про неё легко забыть.
Отдельный блок для каталогов загрузок страхует от ошибок приложения. Даже если злоумышленник загрузит файл с PHP-кодом, Nginx не передаст его на выполнение. Порядок правил имеет значение: регулярные выражения проверяются сверху вниз, поэтому запрещающий блок должен стоять до общего `location ~ \.php$`, иначе его правило не сработает. Выполните `nginx -t` после каждого изменения.
Частая ошибка: разрешено выполнение PHP в каталоге загрузок
Как делать не надо
- Оставлять в конфигурации общий location ~ \.php$ без try_files и без запрета для uploads
- Включать cgi.fix_pathinfo=1 в php.ini при использовании PHP-FPM без проверки существования файла
- Выдавать каталогу загрузок права 777 «чтобы всё работало»
- Полагаться только на проверку расширения файла в самом приложении
Как правильно
- 01
Добавить try_files $uri =404 в блок обработки PHP и установить cgi.fix_pathinfo=0
- 02
Запретить выполнение PHP в uploads, storage и cache через отдельный location с deny all
- 03
Использовать права 755 для каталогов и 644 для файлов, владелец — пользователь сайта
- 04
Хранить пользовательские загрузки вне корня сайта или отдавать их отдельным статическим location
Изоляция сайтов через отдельные пулы PHP-FPM
По умолчанию PHP-FPM создаёт один пул `www`, который работает от пользователя `www-data`. Если на сервере несколько сайтов, все они выполняются с одними и теми же правами. Уязвимость в одном плагине даёт доступ к файлам, конфигурации и базе данных остальных проектов.
Правильный подход — отдельный пул на каждый сайт. У пула свой системный пользователь, свой сокет и свои ограничения PHP. Взлом одного проекта остаётся локальным: процесс просто не имеет прав на чужие каталоги.
Дополнительный плюс изоляции — предсказуемая нагрузка. Вы задаёте `pm.max_children` отдельно для каждого сайта, и один перегруженный проект не съедает всю память сервера.
Как настроить изолированный пул PHP-FPM
- 01
Создайте системного пользователя без оболочки и домашнего каталога логина, например site, и передайте ему каталог проекта командой chown -R site:site /var/www/site.
- 02
Скопируйте файл пула в /etc/php/8.3/fpm/pool.d/site.conf (путь зависит от версии PHP), замените имя пула на [site] и укажите user = site и group = site.
- 03
Задайте собственный сокет: listen = /run/php/site.sock, а также listen.owner = www-data, listen.group = www-data и listen.mode = 0660, чтобы доступ был только у Nginx.
- 04
Ограничьте процессы: pm = dynamic, pm.max_children подберите по объёму памяти (обычно доступная RAM, делённая на средний размер процесса), pm.max_requests = 500 для перезапуска «разбухших» воркеров.
- 05
Добавьте ограничения на уровне пула: php_admin_value[open_basedir] = /var/www/site:/tmp и php_admin_value[disable_functions] = exec,passthru,shell_exec,system,proc_open,popen.
- 06
Удалите или отключите стандартный пул www, проверьте конфигурацию командой php-fpm8.3 -t, примените через systemctl reload php8.3-fpm и обновите fastcgi_pass в Nginx на новый сокет.
Настройки php.ini, которые стоит проверить
Ограничения в пуле дополняют глобальные настройки PHP. Их значения лучше задавать в отдельном файле в каталоге `conf.d`, а не править основной `php.ini`: так проще обновляться и отслеживать изменения.
Обратите внимание на функции, отключаемые через `disable_functions`. Некоторые CMS и библиотеки используют `exec` или `proc_open` для легитимных задач, например обработки изображений или очередей. Перед отключением проверьте, что приложение без них работает, лучше сначала на тестовой копии.
Ниже приведена таблица основных директив, которые закрывают самые частые проблемы.
| Директива | Рекомендуемое значение | Зачем нужна |
|---|---|---|
| expose_php | Off | Не добавляет версию PHP в заголовки ответа |
| display_errors | Off | Не показывает посетителям пути и ошибки кода |
| log_errors | On | Ошибки пишутся в журнал, а не на экран |
| cgi.fix_pathinfo | 0 | Блокирует выполнение «подменённых» путей |
| allow_url_include | Off | Запрещает подключение удалённых файлов через include |
| session.cookie_httponly | 1 | Скрывает cookie сессии от JavaScript |
| session.cookie_secure | 1 (при HTTPS) | Передаёт cookie сессии только по HTTPS |
| open_basedir | каталог сайта и /tmp | Ограничивает чтение и запись за пределами проекта |
TLS, лимиты запросов и защита от перебора
Периметр — первый слой защиты. Включите HTTPS на всех сайтах, оставьте только TLS 1.2 и TLS 1.3 (`ssl_protocols TLSv1.2 TLSv1.3;`) и настройте автоматическое продление сертификата. Добавьте перенаправление с HTTP на HTTPS и заголовок `Strict-Transport-Security` после того, как убедитесь, что весь сайт корректно работает по HTTPS.
Далее ограничьте частоту запросов. В блоке `http` объявите зону `limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;`, а в `location` страницы входа (например, `/wp-login.php` или `/admin`) подключите её директивой `limit_req zone=login burst=10 nodelay;`. Это не остановит распределённую атаку, но надёжно режет простой перебор паролей с одного адреса. Для общего потока добавьте `limit_conn` и разумные таймауты: `client_body_timeout`, `client_header_timeout` и `send_timeout`.
Не забывайте про файрвол. Наружу должны быть открыты только порты 80, 443 и SSH (лучше по ключам и с ограничением адресов). Порты PHP-FPM, базы данных и кэша не должны быть доступны из интернета.
Что даёт такая конфигурация
- 01
Взлом одного сайта не приводит к компрометации остальных проектов на сервере
- 02
Загруженные пользователями файлы нельзя выполнить как PHP-код
- 03
Сканеры не получают версии ПО и служебные файлы вроде .env и .git
- 04
Перебор паролей и всплески запросов не нагружают PHP-FPM
- 05
Лимиты процессов защищают память сервера от исчерпания
- 06
Ошибки пишутся в журнал, а посетители не видят внутренние пути и детали кода
Логи, мониторинг и обновления
Настроенная защита работает, только если вы видите, что происходит на сервере. Включите отдельные журналы доступа и ошибок для каждого сайта, настройте ротацию через logrotate и периодически просматривайте записи с кодами 403, 404 и 499, а также всплески запросов к `xmlrpc.php`, `wp-login.php` и `/admin`. Для автоматической блокировки адресов, которые много раз подряд ошибаются, подойдёт Fail2ban.
Следите и за внешним видом сайта в поиске. Яндекс Вебмастер и Google Search Console уведомляют, если роботы обнаружили вредоносный код или подозрительные страницы. Эти сообщения стоит подключить к почте: заражение, найденное вовремя, обходится гораздо дешевле.
И наконец, обновления. Подписывайтесь на рассылки по безопасности вашего дистрибутива и включайте автоматические обновления безопасности (например, unattended-upgrades в Debian и Ubuntu). Обновлять PHP нужно до версии, которая ещё получает исправления безопасности: устаревшие ветки остаются без патчей.
Итоги: безопасность Nginx и PHP-FPM — это набор простых правил
- 01
Передавайте в PHP только существующие файлы через try_files и запрещайте выполнение кода в каталогах загрузок
- 02
Запускайте каждый сайт в своём пуле PHP-FPM под отдельным пользователем и с собственным сокетом
- 03
Отключите раскрытие информации и лишние функции в php.ini, задайте open_basedir
- 04
Включите HTTPS, лимиты запросов и закройте лишние порты файрволом
- 05
Ведите логи, обновляйте ПО и проверяйте конфигурацию командами nginx -t и php-fpm -t перед каждым reload
Начните с самых простых шагов: try_files, запрет PHP в uploads и отдельные пулы. Они занимают меньше часа и закрывают основную часть реальных атак. Остальное внедряйте постепенно, проверяя каждое изменение на тестовой копии. Если нужна помощь с аудитом или миграцией на изолированные пулы, обратитесь к специалистам.
Часто задаваемые вопросы
Если оба сервиса работают на одной машине, безопаснее unix-сокет. Доступ к нему регулируется правами файловой системы (listen.owner, listen.group, listen.mode 0660), а наружу он не виден. TCP-порт 9000 нужен только при разнесении на разные серверы, и тогда его закрывают файрволом и пускают только с адреса Nginx.
Она не даёт передать в PHP-FPM несуществующий файл. Без неё запрос вида /uploads/image.jpg/x.php при неудачных настройках может привести к выполнению загруженного пользователем файла как PHP-кода.
Это не заменяет обновления, но убирает лишнюю подсказку для автоматических сканеров. Достаточно server_tokens off в Nginx и expose_php = Off в php.ini. Главное при этом регулярно ставить обновления безопасности.
Создайте для каждого сайта отдельный пул PHP-FPM со своим системным пользователем, своим сокетом и собственным open_basedir. Тогда взлом одного сайта не даст прочитать файлы и конфигурации соседних проектов.
Выполните nginx -t и php-fpm -t (в Debian и Ubuntu команда называется php-fpm8.3 -t, с вашей версией), затем перезагрузите сервисы через reload. Reload применяет настройки без разрыва текущих соединений.
Проведём аудит безопасности вашего Nginx и PHP-FPM
Проверим конфигурацию, права и пулы, а затем дадим список правок по приоритету
Спасибо за заявку!
Мы свяжемся с вами в ближайшее время.
Проверено на проектах с нагрузкой до 1 млн визитов



