SSRF атаки: как защитить бэкенд от server-side request forgery
Дефолтный nginx обрабатывает тысячи запросов в секунду. Но когда трафик растёт до десятков тысяч RPS, дефолты начинают мешать: соединения к upstream пересоздаются на каждый запрос, буферы слишком маленькие, сжатие отключено. Разберём ключевые параметры, которые превращают nginx из «работает» в «летает».
worker_processes и worker_connections
Два параметра, определяющие потолок concurrency:
# /etc/nginx/nginx.conf
worker_processes auto; # = количество CPU ядер
worker_rlimit_nofile 65535; # лимит открытых файлов на worker
events {
worker_connections 16384; # макс. соединений на один worker
multi_accept on; # принимать все новые соединения разом
use epoll; # Linux: epoll эффективнее select/poll
}
Формула максимальной concurrency: worker_processes × worker_connections. При 4 ядрах и 16384 connections — до 65536 одновременных соединений.
Важно: worker_rlimit_nofile должен быть больше worker_connections, потому что каждое проксированное соединение — это 2 файловых дескриптора (клиент + upstream). Также проверьте системный лимит:
# Проверить текущий лимит
ulimit -n
# Установить в /etc/security/limits.conf
nginx soft nofile 65535
nginx hard nofile 65535
Буферы: proxy_buffer_size, client_body_buffer_size
Nginx буферизирует ответы от upstream в памяти. Если буфер мал — данные сбрасываются на диск, что убивает latency:
http {
# Буфер для заголовков ответа upstream
proxy_buffer_size 16k;
# Буферы для тела ответа (количество × размер)
proxy_buffers 8 32k;
proxy_busy_buffers_size 64k;
# Буфер для тела запроса клиента
client_body_buffer_size 16k;
client_max_body_size 10m;
# Буферы для больших заголовков запроса (cookies, JWT)
large_client_header_buffers 4 16k;
}
Правила подбора:
- API с JSON-ответами до 32KB — дефолты достаточны
- Ответы 100KB+ (отчёты, списки) — увеличьте
proxy_buffersдо16 64k - Большие cookies/JWT — поднимите
large_client_header_buffers - Загрузка файлов —
client_body_buffer_size= типичный размер файла
Keepalive к upstream
По умолчанию nginx открывает новое TCP-соединение к backend на каждый запрос. При 10k RPS это 10k TCP handshake в секунду — бессмысленная нагрузка:
upstream backend {
server 10.0.1.10:8080;
server 10.0.1.11:8080;
keepalive 64; # пул постоянных соединений
keepalive_requests 1000; # запросов на одно соединение
keepalive_timeout 60s; # таймаут простоя
}
server {
location /api/ {
proxy_pass http://backend;
proxy_http_version 1.1; # обязательно для keepalive
proxy_set_header Connection ""; # убрать "close"
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
}
}
keepalive 64 — это не максимум соединений, а размер пула idle-соединений на каждый worker. При 4 workers и keepalive 64 — до 256 постоянных соединений к backend. Подбирайте под реальный RPS: слишком мало — соединения всё равно пересоздаются, слишком много — держите лишние сокеты.
Gzip vs Brotli: настройка сжатия
Сжатие уменьшает трафик на 60-80% для текстовых ответов:
http {
# Gzip (встроен)
gzip on;
gzip_comp_level 4; # баланс CPU/сжатие (1-9)
gzip_min_length 256; # не жать мелкие ответы
gzip_vary on;
gzip_proxied any;
gzip_types
text/plain
text/css
text/javascript
application/json
application/javascript
application/xml
image/svg+xml;
# Brotli (модуль ngx_brotli, ставится отдельно)
brotli on;
brotli_comp_level 4;
brotli_types text/plain text/css application/json
application/javascript text/javascript
image/svg+xml;
}
Brotli даёт на 15-25% лучшее сжатие при том же CPU, но поддерживается только через HTTPS. Стратегия: brotli для браузеров (Accept-Encoding: br), gzip как fallback.
gzip_comp_level 4 — оптимум. Уровни 6-9 дают +2-3% сжатия при двойном расходе CPU.
Кэширование: proxy_cache
Кэш на nginx снимает нагрузку с backend для повторяющихся запросов:
http {
proxy_cache_path /var/cache/nginx
levels=1:2
keys_zone=app_cache:32m # 32MB под ключи (~250k записей)
max_size=1g # макс. размер на диске
inactive=10m # удалять после 10 мин без обращений
use_temp_path=off;
server {
location /api/catalog {
proxy_pass http://backend;
proxy_cache app_cache;
proxy_cache_key "$request_method|$uri|$args";
proxy_cache_valid 200 5m;
proxy_cache_valid 404 1m;
# Bypass кэша по заголовку
proxy_cache_bypass $http_x_no_cache;
# Отдавать stale при ошибках backend
proxy_cache_use_stale error timeout updating
http_500 http_502 http_503;
add_header X-Cache-Status $upstream_cache_status;
}
}
}
Заголовок X-Cache-Status показывает HIT/MISS/EXPIRED — незаменим для отладки. proxy_cache_use_stale — страховка: если backend упал, отдаём устаревший кэш вместо 502.
Мониторинг: stub_status, логи latency, open_file_cache
Без метрик оптимизация — гадание. Минимальный набор:
# stub_status — базовые счётчики
server {
listen 127.0.0.1:8080;
location /nginx_status {
stub_status;
allow 127.0.0.1;
deny all;
}
}
# Логи с upstream latency
log_format perf '$remote_addr $request_method $uri '
'status=$status rt=$request_time '
'uct=$upstream_connect_time '
'urt=$upstream_response_time '
'cs=$upstream_cache_status';
access_log /var/log/nginx/perf.log perf;
# Кэш метаданных файлов (статика)
open_file_cache max=10000 inactive=60s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
$upstream_response_time — время ответа backend. Если оно растёт — проблема не в nginx. $request_time — полное время обработки запроса. Разница между ними — overhead самого nginx (обычно <1ms).
open_file_cache кэширует дескрипторы и метаданные файлов. Для серверов со статикой (тысячи файлов) это убирает лишние stat() syscalls.
Итого: keepalive к upstream, правильные буферы, сжатие и кэш — четыре рычага, которые дают 3-5x прирост throughput без замены железа. Начните с stub_status и логов latency, найдите узкое место, крутите один параметр за раз.