Серверы
Безопасная настройка nginx: 10 пунктов, которые стоит проверить до экспертизы
Веб-сервер — первый слой перед приложением. Скрытие версии, default_server, TLS, лимиты, скрытые файлы, ограничение выполнения PHP, rate limiting и логи.
· 11 мин. чтения

Как бы тщательно ни был написан код приложения, каждый запрос сначала проходит через веб-сервер. Настройки nginx по умолчанию рассчитаны на то, чтобы «работало», а не на безопасность: он сообщает свою версию, отвечает на неизвестные домены, отдаёт любой файл из каталога и готов выполнить любой .php. На экспертизе сканеры и специалисты проверяют именно это в первую очередь.
В этой статье — 10 практических пунктов для проверки конфигурации nginx перед экспертизой, каждый с командой проверки и готовой настройкой. Примеры даны для PHP-приложения (Laravel), но большинство подходит и для конфигурации, проксирующей запросы в Node или другой бэкенд.
Почему это важно для экспертизы
Недостатки настроек веб-сервера обычно попадают в отчёт экспертизы как «раскрытие информации», «неверная настройка» или «криптографический недостаток». Из шести находок на экспертизе нашего собственного продукта две напрямую относились к серверному слою: host header injection (часть исправления — добавление default_server в nginx) и открытые в интернет порты. Об этом есть отдельные статьи: host header injection и открытые порты и hardening сервера.
Перед началом посмотрите текущую конфигурацию целиком и после каждого изменения проверяйте синтаксис:
sudo nginx -T | less # полная конфигурация со всеми include
sudo nginx -t && sudo systemctl reload nginx
1. Скрытие версии: server_tokens
По умолчанию nginx показывает свою версию в заголовке Server и на страницах ошибок. Это подсказывает атакующему, какие уязвимости пробовать.
Проверка:
curl -sI https://your-domain.uz | grep -i server
curl -s https://your-domain.uz/nonexistent-page | tail -5
Исправление (в блоке http):
server_tokens off;
После этого останется Server: nginx, но без версии. Уберите и заголовок X-Powered-By: PHP/..., который добавляет само приложение: expose_php = Off в php.ini или fastcgi_hide_header X-Powered-By; в nginx.
2. default_server: отклонение неизвестных доменов
Если запрос приходит на IP-адрес или с чужим доменом, nginx отправляет его в default_server, а если тот не задан — в первый блок server, чаще всего основной сайт. Это открывает путь для host header injection и позволяет сканировать сайт по IP.
Проверка:
curl -skI https://SERVER_IP/
curl -skI https://your-domain.uz/ -H "Host: attacker.example"
Исправление — отдельный «перехватывающий» блок:
server {
listen 80 default_server;
listen 443 ssl default_server;
server_name _;
ssl_reject_handshake on; # nginx 1.19.4+
return 444;
}
3. Настройки TLS
Старые протоколы (SSLv3, TLS 1.0, TLS 1.1) и слабые шифры фиксируются на экспертизе как криптографический недостаток. Современная практика — только TLS 1.2 и 1.3. Вместо ручного составления списка шифров удобно взять профиль «intermediate» из Mozilla SSL Configuration Generator.
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
Добавьте также перенаправление с HTTP на HTTPS и заголовок HSTS — подробно в статье о заголовках безопасности.
Проверка:
openssl s_client -connect your-domain.uz:443 -tls1_1 </dev/null # соединение должно быть отклонено
nmap --script ssl-enum-ciphers -p 443 your-domain.uz
Для более глубокого анализа удобен открытый инструмент testssl.sh.
4. Закрытие скрытых и служебных файлов
.env, .git/, composer.json, package.json, архивы резервных копий и SQL-дампы, оставшиеся в корне сайта, скачиваются как обычные файлы. В .env — пароль базы и ключи приложения, в .git — вся история кода.
Проверка:
for p in .env .git/config .git/HEAD composer.json composer.lock \
package.json backup.zip db.sql .DS_Store; do
printf "%-16s %s\n" "$p" "$(curl -s -o /dev/null -w '%{http_code}' https://your-domain.uz/$p)"
done
Всё должно вернуть 404 или 403.
Первый шаг — правильный root: в Laravel это не корень проекта, а каталог public/. Затем дополнительная защита:
root /var/www/app/public;
# все пути, начинающиеся с точки, кроме .well-known (Let's Encrypt)
location ~ /\.(?!well-known/) {
deny all;
}
location ~* \.(sql|bak|old|orig|swp|zip|tar|gz|7z|log|ini|sh)$ {
deny all;
}
autoindex off;
5. PHP выполняется только там, где нужно
Во многих конфигурациях есть блок location ~ \.php$, то есть выполняется любой .php-файл. Если атакующий через загрузку файлов положит PHP-файл в uploads/, это сразу превращается в выполнение кода. А в Laravel единственная точка входа — index.php.
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# выполняется только index.php
location = /index.php {
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_hide_header X-Powered-By;
}
# остальные .php — 404
location ~ \.php$ {
return 404;
}
Путь к сокету должен соответствовать версии PHP в вашей системе. Для WordPress или приложений с несколькими PHP-точками входа как минимум отключите выполнение в каталогах загрузок:
location ^~ /wp-content/uploads/ {
location ~ \.php$ { deny all; }
}
Другие меры защиты загрузки файлов — в отдельной статье.
6. Лимиты размера и времени запроса
Неограниченный размер запроса и долгие таймауты позволяют занять сервер медленными запросами.
client_max_body_size 10m; # под самую большую загрузку в вашем приложении
client_body_timeout 15s;
client_header_timeout 15s;
send_timeout 15s;
keepalive_timeout 30s;
large_client_header_buffers 4 8k;
Значения зависят от приложения: для API без загрузки файлов client_max_body_size можно сделать намного меньше, а для отдельного location, принимающего большие документы, — больше.
7. Rate limiting
Эндпоинты входа, восстановления пароля, OTP и отправки SMS — главная цель для подбора паролей и злоупотребления платными сервисами. Вместе с ограничением на уровне приложения (например, throttle в Laravel) полезно поставить предел и в nginx.
# в блоке http
limit_req_zone $binary_remote_addr zone=auth:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=perip:10m;
# в блоке server
limit_req_status 429;
limit_conn perip 20;
location = /login {
limit_req zone=auth burst=5 nodelay;
try_files $uri /index.php?$query_string;
}
location /api/ {
limit_req zone=api burst=20 nodelay;
try_files $uri /index.php?$query_string;
}
Если nginx стоит за другим прокси или CDN, $binary_remote_addr будет адресом прокси, и все пользователи попадут под один лимит. В этом случае укажите через set_real_ip_from только адрес доверенного прокси и задайте real_ip_header.
Проверка:
for i in $(seq 1 15); do
curl -s -o /dev/null -w "%{http_code} " -X POST https://your-domain.uz/login
done; echo
Через несколько попыток должен появиться 429.
8. Ограничение HTTP-методов
Методы, которые приложение не использует (TRACE, CONNECT и другие), не должны оставаться открытыми:
if ($request_method !~ ^(GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS)$) {
return 405;
}
Для чисто статического сайта сократите список до GET|HEAD.
9. Заголовки прокси
Если nginx проксирует в Node или другой бэкенд, не передавайте слепо заголовки, присланные клиентом:
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host "";
proxy_hide_header X-Powered-By;
}
А бэкенд должен слушать только 127.0.0.1, а не внешний интерфейс — иначе вся защита в nginx обходится.
10. Логи
Без логов невозможно ни обнаружить инцидент, ни разобрать его. При этом в логи не должны попадать секретные данные.
log_format main '$remote_addr - [$time_local] "$request_method $uri $server_protocol" '
'$status $body_bytes_sent "$http_referer" "$http_user_agent" '
'rt=$request_time';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;
В этом формате вместо $request используется $uri — query string не попадает в логи, потому что в ней иногда бывают токены или персональные данные. Если query нужна, примите это как осознанное решение. Логи должны ротироваться через logrotate, а доступ к ним — быть ограничен.
Одна ловушка: наследование add_header
Если add_header используется во вложенном блоке location, все add_header из внешнего блока server для этого location перестают действовать. Например, если на уровне server заданы HSTS и CSP, а в location /api/ добавлен заголовок CORS, в ответах API HSTS пропадёт. Повторяйте нужные заголовки в каждом location или вынесите их в общий файл и подключайте через include. Обязательно проверяйте результат через curl -sI.
Настройка nginx по умолчанию «работает» — но это не значит «безопасна». Каждый пункт — несколько строк конфигурации, но на экспертизе каждый может стать отдельной находкой.
Чек-лист
server_tokens off,X-Powered-Byскрыт.- Блок
default_serverотклоняет запросы к неизвестным доменам и по IP. - Только TLS 1.2 и 1.3, современные шифры, перенаправление с HTTP на HTTPS, HSTS.
root— каталогpublic/;.env,.git, резервные копии и дампы отдают404/403.autoindex off.- PHP выполняется только для
index.php(или нужных файлов); в каталогах загрузок отключён. client_max_body_sizeи таймауты подогнаны под приложение.limit_reqдля входа, OTP, API; за прокси корректно определяется реальный IP.- Неиспользуемые HTTP-методы —
405. - Заголовки прокси под контролем, бэкенд слушает только
127.0.0.1. - Логи включены, без секретных данных, ротируются.
- В каждом location есть нужные заголовки безопасности.
Итог
nginx — входная дверь вашего приложения. Десять пунктов выше — работа на один день, но они закрывают большую часть «лёгких» находок, которые чаще всего встречаются на экспертизе, и дают дополнительную защиту от ошибок на уровне приложения.
Хотите проверить конфигурацию сервера до экспертизы? Оставьте заявку на сканирование — Claude AI агент быстро проанализирует настройки nginx и сервера, а наш специалист подтвердит находки и поможет закрыть дыры.


