Заголовки безопасности и ограничение запросов

Несколько защитных заголовков и грамотный лимит запросов превращают открытый сервер в крепость без единой строчки кода в приложении.
«Rate limiting — это дверной доводчик: пускает поток ровно, не давая толпе вынести дверь разом.»

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

Защитные заголовки

add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options SAMEORIGIN always;
add_header Referrer-Policy strict-origin-when-cross-origin always;

server_tokens off;     # скрыть версию Nginx в заголовках и ошибках

X-Content-Type-Options nosniff запрещает браузеру «угадывать» тип файла. X-Frame-Options SAMEORIGIN мешает встроить твой сайт в чужой iframe (защита от кликджекинга). server_tokens off убирает версию Nginx из ответов — не дарим сканерам подсказку.

Ограничение частоты запросов

http {
    # зона памяти на 10 МБ, ключ — IP клиента, лимит 10 запросов/сек
    limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;

    server {
        location /login {
            limit_req zone=mylimit burst=20 nodelay;
            limit_req_status 429;
            proxy_pass http://127.0.0.1:8000;
        }
    }
}

limit_req_zone объявляет «ведро» в общей памяти: ключ (IP), размер зоны и базовую скорость 10r/s. limit_req применяет лимит к location. burst=20 разрешает короткий всплеск (очередь до 20 запросов), nodelay пропускает их сразу, а не растягивает во времени. Превышение — ответ 429.

Алгоритм leaky bucket

   Запросы льются неравномерно
        | | | | | |
        v v v v v v
       [   ВЕДРО    ]  <- burst: вместимость очереди
        |   |   |
        v (вытекает 10/сек, ровно)
       на бэкенд
   Переполнилось -> лишние запросы получают 429

Смоделируем rate-limiter на Python

RATE = 2.0        # запросов в секунду (вытекает из ведра)
CAPACITY = 4      # burst: вместимость ведра
water = 0.0       # текущий уровень
last = 0.0

# (время_запроса)
requests = [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 2.0]
for t in requests:
    leaked = (t - last) * RATE      # сколько вытекло с прошлого раза
    water = max(0.0, water - leaked)
    last = t
    if water + 1 <= CAPACITY:
        water += 1
        print(f"t={t:.1f}: пропущен (в ведре {water:.1f})")
    else:
        print(f"t={t:.1f}: ОТКЛОНЁН 429 (ведро полно)")

Попробуй сам ▶ Первые запросы проходят, заполняя ведро; когда оно полно — приходит 429; а к t=2.0 ведро успело «протечь» и снова принимает запрос. Это и есть логика limit_req в Nginx.

Как работает под капотом

Nginx хранит для каждого ключа (IP) уровень «воды» в общей памяти зоны. Вода вытекает с постоянной скоростью rate; каждый запрос добавляет каплю. Без burst любой запрос сверх скорости сразу отвергается. burst добавляет очередь: всплеск помещается в неё. С nodelay запросы из очереди обрабатываются немедленно (но слот в очереди освобождается по графику); без него — равномерно растягиваются во времени. Для двухстадийного лимита есть параметр delay.

Частые ошибки

  • Слишком жёсткий лимит без burst. Легитимная пачка запросов (страница тянет 20 ассетов) словит 429. Дай разумный burst.
  • add_header без always. Заголовки безопасности пропадут на ответах с ошибками.
  • Маленькая зона памяти. При множестве уникальных IP зона переполнится; 10m хранит порядка 160 тыс. адресов.
  • Лимит по умолчанию отдаёт 503. Семантически вернее 429limit_req_status 429;.

Best practices

  • Жёсткий лимит — на чувствительные эндпоинты (/login, /api); мягкий — на остальное.
  • burst + nodelay — рабочий шаблон: всплески проходят, поток ограничен.
  • Все add_header безопасности — с always; server_tokens off везде.
  • Возвращай 429, а не дефолтный 503.

Многослойная защита и тюнинг лимитов

Заголовки и rate limiting — это разные слои обороны, и сила в их сочетании. Защитные заголовки борются с атаками на стороне браузера пользователя: X-Frame-Options мешает кликджекингу, X-Content-Type-Options — подмене типа контента, а более продвинутый Content-Security-Policy (тема для отдельного изучения) резко ограничивает, откуда страница может грузить скрипты, что бьёт по XSS. Эти заголовки ничего не стоят в производительности и навешиваются одной строкой — грех ими не воспользоваться. Главное — не забывать always, иначе они пропадут на ответах с ошибками, где порой и опаснее всего.

Rate limiting же защищает уже сам сервер и бэкенд от наплыва: перебора паролей на /login, скрейпинга, примитивных DDoS. Тонкость — в подборе чисел. Слишком мягкий лимит бесполезен, слишком жёсткий бьёт по живым пользователям: современная страница тянет десятки ассетов почти одновременно, и без разумного burst легитимный браузер словит 429. Поэтому рабочая стратегия — разные зоны для разных маршрутов: строгий лимит на чувствительные точки (логин, регистрация, тяжёлые API), мягкий или вовсе никакого на статику. Полезно начать с консервативных значений, понаблюдать за реальным трафиком в логах (сколько 429 ловят настоящие пользователи) и подкрутить. Помни и про память зоны: 10m хранит порядка 160 тысяч уникальных ключей-IP, и при большом числе клиентов зону увеличивают. Грамотно настроенный лимит незаметен для людей и болезнен для ботов — именно этого мы и добиваемся.

Итоги

Заголовки (X-Content-Type-Options, X-Frame-Options, Referrer-Policy) и server_tokens off закрывают целый класс рисков бесплатно. Rate limiting по алгоритму leaky bucket (limit_req_zone + limit_req с burst/nodelay) защищает от перебора и наплыва. Дальше — логи, мониторинг и сборка всего в продакшен-конфиг.

Проверьте себя
1. Что делает параметр burst в limit_req?
AУвеличивает скорость вдвое
BРазрешает короткий всплеск запросов сверх базовой скорости, помещая их в очередь вместо мгновенного отказа
CОтключает лимит
DВключает сжатие
2. Зачем нужна директива server_tokens off?
AУскоряет отдачу
BСкрывает версию Nginx в заголовках и страницах ошибок, не давая сканерам лишней подсказки
CВключает HTTP/2
DОграничивает размер запроса