Стандартная установка Nginx раскрывает версию сервера и не включает защитные HTTP-заголовки. Hardening занимает 20 минут и закрывает целый класс уязвимостей.
nginx.conf — базовые настройки
http {
server_tokens off; # Скрыть версию
client_max_body_size 10m;
client_body_timeout 12;
client_header_timeout 12;
keepalive_timeout 15;
large_client_header_buffers 2 1k;
}
Защитные HTTP-заголовки
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
Rate Limiting
http {
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
}
server {
location /api/ { limit_req zone=api burst=20 nodelay; }
location /wp-login.php { limit_req zone=login burst=3; }
}
Запрет скрытых и опасных файлов
location ~ /\. { deny all; access_log off; log_not_found off; }
location ~* \.(bak|conf|sql|log|sh|ini)$ { deny all; }
SSL/TLS конфигурация
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305';
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
Ограничение HTTP-методов
location / {
limit_except GET HEAD POST { deny all; }
}
Content-Security-Policy-Report-Only перед боевым применением.
Разбор типичных ошибок после hardening
nginx: [emerg] invalid number of arguments in "add_header" directive
nginx: [warn] conflicting server name "example.com" on 0.0.0.0:443
403 Forbidden после включения limit_except
nginx: [emerg] "ssl_ciphers" directive is duplicate
| Сообщение | Причина | Решение |
|---|---|---|
| invalid number of arguments in "add_header" | В значении заголовка пропущены кавычки, и Nginx разбил строку на несколько аргументов по пробелам | Оборачивать значение заголовка в двойные кавычки целиком, включая директивы вроде max-age=31536000 |
| conflicting server name на 443 | Один и тот же server_name объявлен в двух server-блоках, слушающих один порт | Проверить nginx -T | grep -A2 server_name, оставить объявление только в одном файле |
| 403 Forbidden после limit_except | Метод запроса (часто PUT или PATCH от API-клиента) не входит в список разрешённых | Добавить нужный метод в limit_except GET HEAD POST только там, где он реально используется |
| "ssl_ciphers" directive is duplicate | Директива SSL задана и в http {}, и повторно в конкретном server {} | Оставить директиву в одном месте — обычно на уровне http, если сертификаты и требования одинаковы для всех сайтов |
Сравнение уровней hardening
| Мера | Риск, который закрывает | Сложность внедрения |
|---|---|---|
| server_tokens off и защитные заголовки | Утечка версии сервера, clickjacking, MIME-sniffing | Низкая, безопасно для любого сайта |
| Rate Limiting на логин и API | Брутфорс паролей, перебор эндпоинтов | Средняя, нужно подобрать лимиты под реальный трафик |
| Запрет скрытых и служебных файлов | Утечка .env, .git, бэкапов .sql через прямой URL | Низкая, но легко забыть новое расширение при добавлении сервисов |
| Content-Security-Policy | XSS через инжект стороннего JS | Высокая, требует инвентаризации всех внешних скриптов сайта |
Когда этот hardening недостаточен
- Атака идёт через уязвимость в самом приложении (SQL-инъекция, небезопасная десериализация) — заголовки Nginx её не закрывают, нужен патч кода или WAF на уровне приложения
- Нужна защита от распределённого перебора с тысяч IP — rate limiting по одному адресу здесь бессилен, требуется анализ поведения на уровне CDN/прокси перед сервером
- Сайт использует сторонние встраиваемые виджеты (чаты, аналитика, платёжные формы) — строгий CSP без Report-Only периода почти гарантированно сломает часть из них при первом включении
- Нужна защита данных при передаче между внутренними сервисами за пределами HTTP — hardening из этой статьи касается только публичного веб-трафика, а не внутренней сети
Как проверить, что защита реально работает
# Синтаксис конфигурации до применения на боевом сервере
sudo nginx -t
# Заголовки ответа сервера
curl -sI https://yourdomain.com/ | grep -Ei 'x-frame|x-content|strict-transport|referrer'
# Версия сервера не палится в заголовке и в служебных страницах
curl -sI https://yourdomain.com/ | grep -i server
- Выполните
nginx -tперед каждымreload— синтаксическая ошибка в проде роняет весь сайт, а не только применяемую директиву - Запросите заголовки через
curl -Iи убедитесь, что X-Frame-Options, X-Content-Type-Options и Strict-Transport-Security реально присутствуют в ответе - Проверьте, что заголовок
Serverне выдаёт версию Nginx — подтверждение, чтоserver_tokens offприменился - Прогоните сайт через внешний сервис проверки заголовков (например, тот, что упомянут в совете выше) и добейтесь оценки не ниже A, начиная с CSP в режиме Report-Only
Отдельный разбор Rate Limiting и лимитов подключений — в статье Fail2ban для SSH и Nginx, а базовая настройка брандмауэра перед веб-сервером — в материале UFW на VPS.
Частые вопросы
Что делать, если после Rate Limiting на /api/ начали массово падать легитимные запросы фронтенда?
Проверьте, не стоит ли лимит ниже реального количества AJAX-запросов одной страницы — зона rate=10r/s с burst=20 может быть тесной для SPA, которая шлёт десятки запросов разом при загрузке. Поднимайте burst или добавляйте nodelay только после анализа логов, а не вслепую, иначе рискуете просто ослабить защиту без пользы.
Почему страница с CSP в режиме Report-Only всё ещё ломается у части посетителей сайта?
Report-Only только логирует нарушения политики, но не блокирует загрузку ресурсов, поэтому сама по себе она ничего сломать не может. Поломка почти всегда связана не с CSP, а с другим заголовком или кешем браузера либо CDN, который отдаёт старую версию страницы. Сначала очистите кеш и сверьте заголовки через curl, прежде чем менять директивы CSP.
Нужно ли повторять все заголовки безопасности для каждого виртуального хоста вручную?
Нет, директивы add_header и ssl_ciphers логичнее задать один раз в общем конфиге и подключать через include во все server-блоки. Повторение одних и тех же строк в каждом файле хоста быстро приводит к рассинхронизации: один сайт обновили, про остальные забыли, и уровень защиты расползается без единого места правки.
Как узнать, что ограничение HTTP-методов не сломало API, который использует PUT или DELETE?
Проверьте логи на 403 сразу после применения limit_except и повторите реальные запросы клиента через curl с нужным методом. Если API на других путях легитимно использует PUT, PATCH или DELETE, ограничение стоит задавать точечно для конкретных location, а не глобально для всего сайта — иначе любое отклонение от GET, HEAD, POST будет отклонено сервером.