Ограничение доступа к Nginx только с российских IP-адресов

⚠️ Дисклеймер

Материал предоставлен “как есть” и отражает практический опыт эксплуатации в конкретных инфраструктурах.

Авторы не гарантируют:

  • корректность решений в вашей среде;
  • совместимость с вашей конфигурацией;
  • отсутствие побочных эффектов.

Применение любых инструкций выполняется на ваш страх и риск.

Перед внедрением изменений рекомендуется:

  • тестирование на изолированном контуре;
  • проверка зависимостей и текущей конфигурации;
  • наличие актуальных резервных копий (backup).

GeoIP-фильтрация в Nginx через geo + map

Назначение

Данный подход реализует белый список (whitelist) IP-адресов на уровне Nginx, используя директивы geo и map, вместо большого количества allow/deny.

Это более эффективная и масштабируемая схема для production-систем.

Принцип работы

  1. Загружается список российских IP-диапазонов (CIDR)
  2. В директиве geo формируется переменная $geo_ru, где:
    • 1 — IP относится к РФ
    • 0 — всё остальное
  3. Через map формируется логика доступа
  4. В server или location происходит проверка

Преимущества по сравнению с allow/deny

  • O(1) lookup вместо последовательного прохода правил
  • меньше нагрузка на worker’ы
  • лучше масштабируется на десятки тысяч CIDR
  • чище конфигурация

Источник данных

Используется IPdeny:

https://www.ipdeny.com/ipblocks/data/aggregated/ru-aggregated.zone

Генерация geo-конфига

Пример формируемого файла:

geo $geo_ru {
    default 0;
 
    5.8.0.0/13 1;
    31.13.144.0/20 1;
    ...
}

Скрипт обновления

#!/usr/bin/env bash
set -euo pipefail
 
URL="https://www.ipdeny.com/ipblocks/data/aggregated/ru-aggregated.zone"
TARGET="/etc/nginx/geo/ru-geo.conf"
 
TMP=$(mktemp)
 
curl -fsSL "$URL" -o "$TMP"
 
echo "geo \$geo_ru {" > "$TARGET"
echo "    default 0;" >> "$TARGET"
 
awk '{ print "    " $0 " 1;" }' "$TMP" >> "$TARGET"
 
echo "}" >> "$TARGET"
 
rm -f "$TMP"
 
nginx -t && systemctl reload nginx

Использование через map

map $geo_ru $block_non_ru {
    1 0;
    0 1;
}

Применение

Весь сайт

server {
    include /etc/nginx/geo/ru-geo.conf;
 
    if ($block_non_ru) {
        return 403;
    }
}

Только админка

location /admin/ {
    if ($block_non_ru) {
        return 403;
    }
 
    proxy_pass http://127.0.0.1:8080;
}

Важно про статику

Если блокировать всё без исключений, сломаются CSS/JS.

Решение:

location ~* \.(css|js|png|jpg|jpeg|gif|svg|ico|woff2?)$ {
    root /var/www/static;
}

И применять блокировку только к backend:

location / {
    if ($block_non_ru) {
        return 403;
    }
 
    proxy_pass http://127.0.0.1:8080;
}

Страница отказа

error_page 403 /access-denied/;
 
location = /access-denied/ {
    root /var/www/site;
}

Пример HTML:

<!DOCTYPE html>
<html>
<head>
    <title>Access denied</title>
</head>
<body>
    <h1>Доступ ограничен</h1>
    <p>Сервис доступен только из России.</p>
</body>
</html>

Cron

17 4 * * * root /usr/local/sbin/update-nginx-ru-geo.sh

Риски

  • GeoIP не идеален
  • VPN ломает модель
  • требуется мониторинг

Вывод

geo + map — более правильный подход для production по сравнению с allow/deny, так как обеспечивает быстрый lookup, лучше масштабируется и снижает нагрузку на worker-процессы Nginx.

Однако оптимальнее выносить фильтрацию на более ранний уровень:

  • iptables / nftables — позволяют отбрасывать нежелательный трафик ещё на уровне ядра, до попадания в Nginx. Это снижает:
    • нагрузку на CPU веб-сервера;
    • количество соединений, обрабатываемых Nginx;
    • влияние атак типа сканирования и brute-force.
  • WAF (Web Application Firewall) — добавляет уровень контекстной фильтрации:
    • анализирует поведение, а не только IP;
    • может блокировать атаки уровня L7 (SQLi, XSS и т.д.);
    • даёт централизованное управление политиками безопасности.

Кратко:

  • geo + map — хороший баланс простоты и производительности на уровне приложения
  • iptables / nftables — правильное решение для базовой фильтрации на уровне сети
  • WAF — необходим для защиты от сложных атак

Оптимальная схема — комбинация всех трёх уровней, где каждый решает свою задачу и разгружает следующий.