Перейти к основному содержанию
Блог

Серверы

Безопасная настройка nginx: 10 пунктов, которые стоит проверить до экспертизы

Веб-сервер — первый слой перед приложением. Скрытие версии, default_server, TLS, лимиты, скрытые файлы, ограничение выполнения PHP, rate limiting и логи.

· 11 мин. чтения

Фото: NOIRLab/NSF/AURA/T. Slovinský, CC BY 4.0 · Wikimedia Commons

Как бы тщательно ни был написан код приложения, каждый запрос сначала проходит через веб-сервер. Настройки 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 и сервера, а наш специалист подтвердит находки и поможет закрыть дыры.