Зачем аудитить блокировки IP в Nginx и с чего начать
Аудит блокировок IP в Nginx отвечает на один вопрос: какие правила могут отдать клиенту 403 и не попал ли под них тот, кто вам нужен. Правила разбросаны по nginx.conf, каталогам conf.d/ и sites-enabled/, а иногда лежат в отдельных файлах с зонами. Одни вы задали руками и они работают постоянно, другие включаются только при превышении порога под нагрузкой. Начинайте с вывода итоговой конфигурации, а не с чтения основного файла: именно include-файлы чаще всего скрывают лишний deny all.
Практическая цель аудита: собрать полный список запретов, сопоставить его с реальными клиентами и убрать те, что бьют по своим. Проверять правки до перезагрузки обязательно, иначе конфиг с несуществующей зоной или опечаткой в адресе оставит сервер без рабочего правила.
Чем статические блокировки отличаются от динамических
Разница определяет всю методику поиска. Статические правила лежат в конфиге текстом и действуют всегда, динамические создаются зонами в памяти и срабатывают только по порогу. Диагностику 403 начинайте со статических: их проще найти и они дают предсказуемый код ответа.
| Тип | Директивы | Контекст | Когда срабатывает | Код ответа |
|---|---|---|---|---|
| Статический запрет | deny, allow | http, server, location, limit_except | Пока правило есть в конфиге | 403 |
| Статическая карта адресов | geo | http | По значению переменной для адреса клиента | Любой из вашего правила, обычно 403 через return |
| Ограничение скорости | limit_req_zone, limit_req | Зона в http, правило в http, server, location | При превышении rate и burst | 503 по умолчанию, а limit_req_status меняет его на 429 или другой |
| Ограничение соединений | limit_conn_zone, limit_conn | Зона в http, правило в http, server, location | При превышении числа одновременных соединений | 503, а limit_conn_status меняет код |
Минимальные примеры выглядят так. Статическая блокировка: deny 192.0.2.1; в блоке server или location. Динамическое ограничение: сначала зона limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s; в блоке http, затем правило limit_req zone=one burst=5 nodelay; в location.
Опасная настройка, которую находим при аудите чаще всего: limit_req_status 403. С ней динамическое ограничение начинает выглядеть как статическая блокировка, и вы будете искать deny там, где его нет. Проверьте эту директиву отдельно.
Как вывести полный конфиг через nginx -T
Команда nginx -T выводит полную действующую конфигурацию в stdout, включая все обработанные include-директивы, и попутно проверяет синтаксис. Команда nginx -t только проверяет синтаксис конфигурационного файла и пытается открыть указанные в нём файлы, но ничего не печатает, поэтому для аудита одного теста недостаточно. Это поведение описано в разборе проверки конфигурации Nginx и в man-странице nginx(8).
Сохраните вывод в файл, чтобы искать по нему и не гонять Nginx повторно:
nginx -T > /tmp/nginx-full.conf
Дальше работайте с файлом: он содержит склейку nginx.conf, conf.d/*.conf, sites-enabled/* и всех остальных include. Маска include /etc/nginx/conf.d/*.conf; разворачивается полностью, поэтому правило из забытого файла вы увидите в общем дампе.
В контейнере команда выполняется через exec:
docker exec nginx nginx -T
docker compose exec nginx nginx -T
В Kubernetes, если Nginx развёрнут как Deployment:
kubectl exec -it deploy/nginx -- nginx -T > /tmp/nginx-full.conf
Перед любыми правками сделайте копию каталога конфигов:
cp -a /etc/nginx /etc/nginx.bak.20260923
Как найти все статические блокировки: deny и geo
Ищите сразу по дампу, а не по каталогам, так вы гарантированно получите порядок, в котором Nginx видит правила. Проверяйте не только deny, но и allow: в директивах доступа порядок задаёт результат, и один allow перед deny all полностью меняет логику блока.
Базовые команды по полному конфигу:
grep -n 'deny\|allow' /tmp/nginx-full.conf
grep -n 'geo ' /tmp/nginx-full.conf
Поиск deny и allow в include-файлах
Иногда дампа нет под рукой, тогда ищите по файловой системе:
grep -rn 'deny\|allow' /etc/nginx/conf.d/ /etc/nginx/sites-enabled/
Директивы доступа работают в http, server, location и limit_except. Nginx проверяет правила последовательно в порядке их появления, и результат решает первое совпадение. Типовой шаблон защиты админки:
location /admin {
allow 10.0.0.0/8;
deny all;
}
Здесь первая строка пропускает внутреннюю сеть, вторая закрывает всех остальных. Перестановка этих двух строк заблокирует вообще всех, включая администраторов, и это самая частая ошибка в правилах доступа. Больше примеров ограничений по IPv4 и IPv6 с проверкой через curl собрано в материале про права доступа к данным в Nginx и контроль доступа.
Блокировки через geo выглядят иначе: директива объявляет карту адресов, а решение принимает переменная. Карта описывается только в контексте http:
geo $blocked {
default 0;
192.0.2.0/24 1;
}
Внутри server или location правило превращается в ответ: if ($blocked) { return 403; }
Как найти конкретный IP в правилах блокировки
Проверка конкретного адреса сводится к grep по дампу:
nginx -T | grep -n '192.0.2.1'
nginx -T | grep -n '192.0.2.0/24'
Для карт geo ищите по имени переменной, а не по адресу, потому что подсети в них могут быть заданы диапазонами и списками. Если адрес не найден ни в deny, ни в geo, проверьте, не приходит ли 403 из другого места: от return, от прав на файл или от бэкенда.
Как найти динамические ограничения: limit_req и limit_conn
Динамические ограничения ищутся в два прохода: сначала зоны, потом правила, которые их используют. Правило без объявленной зоны не работает, и Nginx может не запуститься с ошибкой unknown limit_req zone, а вот зона без правила просто висит в памяти без эффекта.
Поиск по дампу:
nginx -T | grep -n 'limit_req\|limit_conn'
Где искать зоны limit_req_zone и limit_conn_zone
Зоны объявляются в контексте http и часто вынесены в отдельный файл вроде /etc/nginx/conf.d/limits.conf. Примеры:
limit_req_zone $binary_remote_addr zone=req_limit:10m rate=5r/s;
limit_conn_zone $binary_remote_addr zone=addr:10m;
Ключ зоны определяет, по чему считается лимит. С $binary_remote_addr ограничение считается на адрес клиента, с $server_name на виртуальный хост, с $http_x_api_key на ключ. По данным блога NGINX, состояние примерно 16 000 IP-адресов занимает 1 МБ, то есть зона на 10 МБ рассчитана примерно на 160 000 адресов. Это ориентир из практического материала, а не норма из официальной документации модуля ngx_http_limit_req_module, поэтому при оценке запаса памяти отталкивайтесь от фактического числа уникальных клиентов.
Правила размещаются в server или location:
limit_req zone=req_limit burst=10 nodelay;
limit_conn addr 10;
Параметр burst задаёт очередь запросов сверх лимита, nodelay отдаёт их сразу без задержки. Без nodelay лишние запросы обслуживаются с паузами, и это выглядит для клиента как медленный сервер, а не как блокировка.
Как отличить rate limit от статической блокировки
Различие видно по коду ответа и по сообщению в error.log. Для ограничения скорости подтверждено поведение по умолчанию: NGINX отвечает кодом 503 клиенту, превысившему лимит, и пишет в лог строку вида limiting requests, excess: 1.000 by zone "mylimit" с указанием зоны и величины превышения — это зафиксировано в материале NGINX о rate limiting. Для deny ожидаемый код 403 и запись access forbidden by rule, для limit_conn — запись limiting connections; эти два случая в доступных источниках не подтверждены, поэтому проверяйте их по фактическому логу вашей установки, а не считайте гарантированными.
Смотрите журнал ошибок в реальном времени:
tail -f /var/log/nginx/error.log | grep -i 'limiting'
Если код 403 встречается вместе с настройкой limit_req_status 403, динамический лимит маскируется под статический запрет. Тогда ищите строки limiting в error.log: они есть, даже когда клиент видит 403. Обратный случай тоже возможен: deny может быть перекрыт error_page, и клиент увидит нестандартную страницу.
Как понять причину 403: пошаговая диагностика
403 в Nginx появляется минимум по пяти причинам: deny, geo или if с return, ограничение с limit_req_status 403, нехватка прав на файл, ответ апстрима, а также внешний WAF или фильтр. Порядок разбора всегда одинаков: сначала логи, потом правила, потом права и бэкенд.
Что искать в error.log и access.log
В error.log ключевая строка про блокировку содержит текст access forbidden by rule. Пример записи:
2026/09/23 10:00:00 [error] 1234#0: *1 access forbidden by rule, client: 192.0.2.1, server: example.com, request: "GET /admin HTTP/1.1"
Если вместо этого вы видите open() вроде failed (13: Permission denied), причина в правах на файл или каталог, а не в блокировке IP. Проверьте каталог раздачи:
ls -la /var/www/html
Срез по коду 403 удобно смотреть в access.log:
grep ' 403 ' /var/log/nginx/access.log | tail -20
Сопоставьте адрес и время с моментом, когда клиент сообщил о проблеме. Готовые связки grep и awk для поиска ошибок и подозрительной активности разобраны в статье про анализ логов Nginx и Apache.
Если в access.log запроса нет вообще, он не дошёл до Nginx: смотрите балансировщик, firewall или CDN. Полезно поднять детализацию логирования на время проверки:
error_log /var/log/nginx/error.log debug;
Как проверить, что IP блокируется на уровне Nginx, а не upstream
Сравнить источник ответа помогает переменная $upstream_status в формате лога. Логика такая: если 403 отдаёт бэкенд, $upstream_status тоже показывает 403; если запрос не ушёл на апстрим, значение $upstream_status остаётся пустым или равным дефису. Это рабочая эвристика, а не гарантия из документации: в доступных источниках поведение $upstream_status при deny не подтверждено, поэтому сверяйте его с записью access forbidden by rule в error.log.
Фрагмент log_format для такой проверки:
log_format diag '$remote_addr $status $upstream_status "$request"';
Дальше выборка по нужному адресу:
grep '192.0.2.1' /var/log/nginx/access.log | tail -20
Признак блокировки на стороне Nginx: в error.log есть access forbidden by rule, а $upstream_status пустой. Признак проблемы на бэкенде: $upstream_status равен 403, а строки access forbidden by rule в логах нет. Общий алгоритм разбора кодов 403, 404 и 500 с проверкой прав, PHP-FPM и виртуальных хостов собран в статье про диагностику ошибок веб-сервера.
Чек-лист аудита блокировок IP в Nginx
Прогоняйте этот список целиком, чтобы не пропустить правила из include-файлов и динамические лимиты.
- Сделайте копию конфигов: cp -a /etc/nginx /etc/nginx.bak.20260923
- Выведите полный конфиг: nginx -T > /tmp/nginx-full.conf
- Найдите статические запреты: grep -n 'deny\|allow\|geo' /tmp/nginx-full.conf
- Найдите динамические лимиты: grep -n 'limit_req\|limit_conn' /tmp/nginx-full.conf
- Проверьте коды подмены: grep -n 'limit_req_status\|limit_conn_status' /tmp/nginx-full.conf
- Проверьте 403 в access.log: grep ' 403 ' /var/log/nginx/access.log | tail -20
- Проверьте error.log на access forbidden by rule и на Permission denied
- Протестируйте конкретный адрес через curl и сопоставьте время с логами
- Убедитесь, что конфиг валиден: nginx -t
- Перезагрузите без разрыва соединений: nginx -s reload
Отдельно ведите список адресов, которые блокируете намеренно: мониторинг, офисная сеть, VPN администраторов. Через месяц никто не вспомнит, откуда взялся deny в sites-enabled, и дежурный потратит час на поиск. Для постоянного контроля доступа и следа изменений настройте журналирование и правила автоматической блокировки, как описано в гайде по мониторингу и аудиту безопасности Nginx.
Как проверить результат аудита на практике
Убедиться, что адрес действительно блокируется, быстрее всего через curl. Запрос без заголовков проверяет реальный путь клиента:
curl -I https://example.com
curl -I https://example.com | head -1
Подстановка адреса через X-Forwarded-For сработает только тогда, когда в Nginx настроены real_ip_header и set_real_ip_from. Модуль ngx_http_realip_module меняет адрес и необязательный порт клиента на переданные в указанном поле заголовка: set_real_ip_from задаёт доверенные адреса, которые передают верный адрес, а real_ip_header задаёт поле заголовка, значение которого используется для замены адреса клиента — это описано в документации модуля ngx_http_realip_module. Без этой настройки ограничения считаются по адресу, с которого пришёл TCP-запрос, то есть по $remote_addr. Проверка с чужого IP через заголовок без модуля real_ip даст неверный результат:
curl -I -H 'X-Forwarded-For: 192.0.2.1' https://example.com
Если блокировка нужна только в одном location, тестируйте именно этот путь: корень сайта может отдавать 200, а закрытая админка 403. Сопоставьте ответ со строкой в access.log, время запроса должно совпадать с точностью до секунды.
Не блокируйте себя во время правок: перед reload держите открытой вторую SSH-сессию, добавьте свой адрес в allow, проверьте конфиг через nginx -t и только потом выполняйте nginx -s reload. Правило, которое валидно синтаксически, но закрывает весь трафик, откатывается из созданной ранее копии конфигов.