Заголовки безопасности и ограничение запросов
Несколько защитных заголовков и грамотный лимит запросов превращают открытый сервер в крепость без единой строчки кода в приложении.
«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. Семантически вернее
429—limit_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) защищает от перебора и наплыва. Дальше — логи, мониторинг и сборка всего в продакшен-конфиг.