Речь не про дорогой WAF и не про машинное обучение. Речь про baseline - тот минимум на уровне прокси, который закрывает 80 процентов глупых дыр десятком директив и не стоит ни рубля. nginx стоит на входе у всех твоих запросов, и именно тут дешевле всего навесить broneжилет: security-заголовки, скрытие версии, ограничение методов и тел, запрет служебных файлов. Дальше - механика каждого пункта, грабли и рабочий конфиг, который можно положить в globals/security.conf по образцу эталонного gateway.
nginx безопасность: модель угроз на прокси и почему заголовки решают
Сразу честно: nginx не чинит дыры в твоём бэкенде. Если PHP-код уязвим к SQL-инъекции, никакой security-заголовок его не спасёт. Заголовки работают в браузере жертвы - это инструкции, которые говорят браузеру "не делай вот так". Поэтому модель простая: ты на прокси добавляешь к каждому ответу набор HTTP-заголовков, браузер их читает и ужесточает свою политику. Атака, которая раньше проходила (загрузка твоей страницы во фрейм, выполнение инлайн-скрипта, утечка Referer на чужой домен), теперь блокируется самим браузером.
Ключевой технический нюанс в nginx - директива add_header капризная. Она наследуется в дочерний контекст (server -> location) ТОЛЬКО если в дочернем контексте нет ни одного своего add_header. Стоит тебе добавить хоть один add_header внутри location - все унаследованные из server исчезают. Это классическая грабля: навесил заголовки глобально, потом дописал в одном location add_header Cache-Control, и весь security-набор для этого location пропал. Поэтому в эталоне все заголовки идут с флагом always (чтобы слались и на ответах 4xx/5xx, а не только на 2xx/3xx), и важно держать их в одном месте либо аккуратно дублировать. Запомни: always добавляет заголовок к ответам с любым кодом, без него - только к успешным.

nginx security headers: полный набор и что каждый делает
Разберём набор из эталонного global_security построчно - что, зачем и где грабли.
X-Content-Type-Options: nosniff - запрещает браузеру угадывать MIME-тип. Без него браузер может решить, что твой .txt с пользовательским контентом на самом деле HTML, и выполнить его как страницу. Дешёвая защита от целого класса XSS через загруженные файлы. Ставь всегда, побочек нет.
X-Frame-Options: SAMEORIGIN - защита от clickjacking. Запрещает встраивать твой сайт во фрейм на чужом домене. Технически устаревает в пользу CSP frame-ancestors, но старые браузеры понимают только X-Frame-Options, поэтому шлём оба. SAMEORIGIN разрешает фреймить только со своего origin.
Referrer-Policy: no-referrer - управляет тем, что уйдёт в заголовке Referer при переходе по ссылке. no-referrer - максимально жёстко, вообще ничего не отправляем. Если ломается аналитика - ослабь до strict-origin-when-cross-origin (отдаёт только origin при переходе на другой домен, полный URL - внутри своего).
Permissions-Policy: geolocation=(), microphone=(), camera=() - отключает мощные браузерные API для твоего сайта и встроенных в него фреймов. Пустые скобки = запрещено всем. Если сайту не нужна камера и микрофон - вырубай, чтобы внедрённый через XSS скрипт не смог их дёрнуть.
X-Download-Options: noopen и X-Permitted-Cross-Domain-Policies: none - легаси для старого IE и Flash/PDF-плагинов соответственно. Вреда от них ноль, в эталоне они есть для полноты, но в 2026 это уже почти археология. Знай что это, не трать на них нервы.
А теперь про две главные.
nginx hsts и Content-Security-Policy: два тяжеловеса
Strict-Transport-Security (HSTS) - заставляет браузер ходить к тебе ТОЛЬКО по HTTPS, даже если пользователь набрал http:// или кликнул по старой ссылке. После первого визита браузер запоминает политику на max-age секунд и сам апгрейдит все запросы до HTTPS, не давая шанса на downgrade-атаку (man-in-the-middle, который перехватывает первый незашифрованный запрос).
Код: Выделить всё
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
Content-Security-Policy (CSP) - это главный заголовок безопасности 2026 года и реальная замена устаревшему X-XSS-Protection. CSP - белый список источников, откуда браузеру разрешено грузить скрипты, стили, картинки, шрифты, фреймы. Если злоумышленник через XSS внедрил <script>укради-cookie</script>, но твоя CSP говорит "инлайн-скрипты запрещены, скрипты только с self" - браузер просто не выполнит инъекцию.
Код: Выделить всё
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; object-src 'none'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'" always;
ВАЖНО про X-XSS-Protection. Эталонный global_security его всё ещё шлёт - и это сознательное легаси. В 2026 OWASP прямо рекомендует ЛИБО не отправлять этот заголовок вовсе, ЛИБО слать X-XSS-Protection: 0. Старый XSS-аудитор браузеров был выпилен из Chrome и Edge ещё годы назад, а в ряде случаев сам создавал уязвимости (атакующий мог использовать фильтр, чтобы отключать легитимные куски страницы). Вывод на 2026: полагайся на CSP, а X-XSS-Protection либо выкинь, либо ставь в 0. Не копируй старые гайды, где он стоит в "1; mode=block" - это вредный совет из 2015 года.
nginx hide version: скрытие версии и заголовка Server
server_tokens off - первое, что делает любой адекватный admin. По умолчанию nginx светит полную версию в заголовке Server и на страницах ошибок. Атакующему это подарок: увидел nginx/1.30.0 - сразу знает, какие CVE применимы (а на 2026 актуальны CVE-2026-42945 в rewrite-модуле, CVE-2026-1642 и другие - см. урок по CVE).
Код: Выделить всё
http {
server_tokens off;
}
Код: Выделить всё
# требует модуль headers_more (есть в OpenResty/Angie из коробки)
more_set_headers "Server: cyberlake-gw";
more_clear_headers "Server";
Ограничения: методы, тело запроса и max_headers против флуда
max_headers - новая директива nginx 1.29.8 (вошла в stable 1.30). До неё ничто не мешало клиенту прислать тысячи мелких заголовков в одном запросе и сожрать память воркера - классический header-flooding DoS. Теперь есть лимит на число строк заголовков, по умолчанию 1000, превышение - сразу 400 Bad Request. Директива приехала из freenginx Дунаева. Если твой апстрим точно не ждёт сотни заголовков - ужесточи:
Код: Выделить всё
http {
max_headers 100;
}
Код: Выделить всё
location /api/ {
limit_except GET POST {
deny all;
}
proxy_pass http://backend_upstream;
}
Код: Выделить всё
if ($request_method !~ ^(GET|HEAD|POST)$) {
return 405;
}
client_max_body_size - сколько максимум байт примем в теле запроса. По умолчанию 1m. Если у тебя API, который не грузит файлы, ставь жёстко - так ты режешь попытки залить гигабайтный мусор и забить диск/память:
Код: Выделить всё
http {
client_max_body_size 10m; # глобально скромно
}
location /upload/ {
client_max_body_size 100m; # точечно поднять там где реально нужно
}
Запрет служебных файлов и query-фильтры: deny .git, .env и пределы фильтрации
Огромный пласт утечек - это банально открытые служебные файлы. Кто-то задеплоил через git pull и оставил .git/ в webroot - и вот уже example.com/.git/config, а через него вся история коммитов с паролями в старых версиях. Или .env с ключами от базы. Эталонный app_redirects_security это закрывает regex-ом:
Код: Выделить всё
# скрытые dotfiles, кроме .well-known (нужен для ACME и пр.)
location ~ /\.(?!well-known) {
deny all;
access_log off;
log_not_found off;
}
# .git/.svn/.hg, .env, бэкапы и дампы
location ~* \.(git|svn|hg|env|bak|old|orig|sql|zip|tar|gz)$ {
deny all;
}
location ~* /(\.git|\.env|composer\.(json|lock)|\.htaccess) {
deny all;
}
Простые SQLi/XSS-фильтры по query_string. Эталон фильтрует подозрительные строки запроса - грубо режет очевидный мусор:
Код: Выделить всё
location / {
if ($query_string ~* "union.*select|concat.*\(|GLOBALS|_REQUEST|<script") {
return 403;
}
if ($query_string ~* "(\.\./|%2e%2e)") { # path traversal
return 403;
}
# ...
}
Мини-лаба: собрать security.conf и проверить заголовки руками
Повтори по шагам на тестовом nginx (1.30+ или OpenResty/Angie):
- Создай файл globals/security.conf и положи в него: server_tokens off; max_headers 100; client_max_body_size 10m; и весь набор add_header с флагом always (X-Content-Type-Options nosniff, X-Frame-Options SAMEORIGIN, Referrer-Policy no-referrer, Permissions-Policy, CSP с default-src 'self').
- Подключи include globals/security.conf; внутри http {} в основном конфиге.
- Проверь синтаксис: nginx -t. Получил "syntax is ok, test is successful" - перезагружай: nginx -s reload.
- Дёрни заголовки: curl -sI https://localhost/ и убедись, что в ответе есть твои заголовки, а Server не светит версию.
- Проверь always: curl -sI https://localhost/несуществует - на 404 заголовки должны быть на месте (если флага always нет - они пропадут, в этом весь смысл).
- Проверь deny: curl -s -o /dev/null -w "%{http_code}\n" https://localhost/.git/config - должен вернуть 403.
- Проверь метод: curl -s -o /dev/null -w "%{http_code}\n" -X DELETE https://localhost/api/ - должен вернуть 405 (или 403 для limit_except).
- Проверь max_headers: сгенерь запрос с 200 заголовками (например циклом в bash, добавляя -H "X-N: 1") - получишь 400.
- Для финальной оценки прогони домен через securityheaders.com или Mozilla Observatory - цель A или A+.
Код: Выделить всё
# globals/security.conf -- подключать в http {}
server_tokens off;
max_headers 100;
client_max_body_size 10m;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "no-referrer" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Content-Security-Policy "default-src 'self'; object-src 'none'; frame-ancestors 'self'; base-uri 'self'" always;
add_header X-XSS-Protection "0" always; # 2026: legacy, держим в 0 (или вовсе убрать)
# HSTS -- ТОЛЬКО на server с listen 443 ssl, не здесь глобально!
# add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
- Почему security-заголовки из server-контекста пропадают, если в location добавить хоть один свой add_header, и как этого избежать?
- Зачем флаг always у add_header и на каких ответах заголовок появится без него?
- Почему HSTS нельзя ставить на HTTP-сервере и чем опасна связка includeSubDomains + preload при кривых поддоменах?
- Почему query-фильтры SQLi/XSS на nginx - это не WAF, и какие два способа их обойти ты можешь назвать?
Baseline-безопасность на прокси - это десяток директив, которые ставятся раз и работают на каждом запросе. Скрыл версию (server_tokens off, при наличии headers_more - перепиши Server), навесил security-заголовки с always (CSP - главный, X-Frame-Options и nosniff обязательны, HSTS осознанно на 443, X-XSS-Protection в 2026 - в 0 или в мусорку), ограничил методы и тело (limit_except, client_max_body_size), прикрыл флуд (max_headers), закрыл .git/.env/бэкапы regex-ом с исключением для .well-known. Грубые query-фильтры - бордюр, а не стена: настоящая защита от инъекций живёт на бэкенде и в WAF. Сделай это сегодня - и завтрашний массовый скан пройдёт мимо.