Сначала убедитесь, что это действительно атака
Самая дорогая ошибка во время инцидента — лечить не то, что сломалось. «Сайт тормозит» — симптом, общий и для настоящего флуда, и для деплоя, который выкатил запрос без индекса, и для cron-задачи, начавшей каждый час в одну и ту же минуту выгружать базу, и для краулера, обнаружившего ваш фасетный поиск, и для ссылки, попавшей на главную страницу какого-нибудь крупного ресурса, и для диска, который просто заполнился. С точки зрения браузера всё это выглядит одинаково, а нужные меры при этом взаимоисключающие: вы не хотите ограничивать по частоте настоящих клиентов только из-за того, что миграция забыла про индекс.
Три вопроса позволяют различить их меньше чем за минуту. Объём трафика действительно аномален, или трафик нормальный, а тормозит сам сервер? Флуд проявляется как резкий скачок пакетов или запросов в секунду; неудачный деплой — как обычный объём запросов при рухнувшем времени отклика. Менялось ли что-нибудь на вашей стороне за последний час? Проверьте журнал деплоев раньше, чем firewall, — на небольшой инфраструктуре инциденты, устроенные собственными руками, встречаются куда чаще настоящих атак. Нагрузка размазана по всему сайту или сосредоточена на одном пути? Настоящие флуды обычно бьют без разбора или метят в главную страницу; если дорогой эндпоинт долбят сто клиентов, это ближе к злоупотреблению, чем к DDoS, — и чинится это куда дешевле.
Ответьте на эти вопросы, а затем двигайтесь дальше. Весь оставшийся текст руководства исходит из того, что ответ был таким: объём аномален, на вашей стороне ничего не менялось, и машина тонет.
SP·02Пятиминутный триаж
Есть только два по-настоящему важных режима отказа, и реагировать на них нужно противоположным образом. Либо канал переполнен — пакеты приходят быстрее, чем ваш аплинк или ядро способны их обработать, и сервер теряет трафик ещё до того, как его увидит хоть какое-то ваше ПО, — либо канал в порядке, а исчерпано приложение, потому что корректно сформированные запросы приходят быстрее, чем оно успевает на них отвечать. Спутать их — значит потерять весь час впустую: тюнинг nginx никак не поможет против забитого канала, а покупка дополнительной полосы никак не поможет против флуда запросов.
Отличить одно от другого можно, сравнив две цифры. Смотрите одновременно на счётчики интерфейса и на распределение нагрузки CPU. Если байты rx упёрлись в скорость порта, если счётчики dropped или overrun растут, а время уходит на программные прерывания, а не на ваше приложение, — флуд идёт на уровне L3/L4, и это проблема пропускной способности. Если с полосой всё в порядке, а пул воркеров исчерпан, соединения выстраиваются в очередь, а лог доступа полон правдоподобных на вид запросов, — это уровень L7, и это проблема фильтрации.
Затем классифицируйте случай L3/L4 ещё на шаг точнее, потому что подтипы ведут себя по-разному. SYN-флуд проявляется как десятки тысяч полуоткрытых сокетов в состоянии SYN-RECV; ядро неплохо справляется с этим само, если включены syncookies. UDP-флуд или флуд с усилением проявляется как огромный входящий объём на портах, которые вы даже не слушаете, — отражённые DNS-, NTP-, memcached- и CLDAP-ответы, — и тут не поможет ничего из того, что запущено у вас, потому что ущерб уже нанесён к моменту, когда пакеты доходят до вашей сетевой карты. Фрагментационный флуд или флуд из сырых пакетов проявляется как высокий pps при скромной полосе, что истощает не канал, а CPU. Запишите, с каким именно случаем вы столкнулись, прежде чем лезть в конфиг.
Что нельзя исправить изнутри самой машины
Это та часть, которую пропускает большинство статей, и именно она решает, будет ли ваш час продуктивным. Правило firewall на цели не спасает захлебнувшийся аплинк. Ваш DROP в iptables выполняется на машине в конце канала — пакет уже пересёк транзитную линию, уже израсходовал полосу, за которую вы платите, и уже вытеснил пакет настоящего пользователя. Отбрасывая его локально, вы защищаете приложение от лишней траты циклов CPU — это чего-то стоит, — но вашу полосу это не защищает вообще никак.
Честный потолок для одного сервера — это примерно меньшее из двух чисел: скорость порта, к которому он подключён, и число пакетов в секунду, которое способен разобрать его CPU. Порт на 1 Gbps заполняется на 1 Gbps независимо от того, насколько элегантен ваш набор правил, а скромный флуд мелких пакетов способен занять пару ядер обработкой прерываний задолго до того, как цифра по полосе покажется тревожной. За этой точкой помогает только одно — устройство выше по цепочке, у которого мощности больше, чем у атаки, и которое сбрасывает трафик прежде, чем тот вообще доберётся до вашего канала. Именно это и называется фильтрацией (scrubbing), и именно поэтому каждая машина в нашем парке стоит за 1.5 Tbps постоянно активной защиты, а не за более крупным firewall'ом.
Следствие из этого не менее важно: если у вас нет апстрим-защиты, ответ провайдера на крупную волюметрическую атаку — отправить ваш адрес в null-route, потому что альтернатива — деградация всех остальных клиентов на этом канале. Это не злой умысел, а арифметика, — и получается, что атакующий побеждает не тем, что что-то ломает, а тем, что делает вас слишком дорогим. Заранее выяснить, включена ли защита от атак в ваш тариф или это платная опция, которую вы так и не подключили, — проверка на пять минут, и сделать её стоит сегодня, а не во время инцидента. У нас это включено в каждый тариф, и это единственная схема, которая помогает в три часа ночи.
SP·04L7: флуд, который выглядит как обычный трафик
Флуд на уровне приложения сложнее, потому что каждый отдельный запрос легитимен. Хорошо сделанный HTTP-флуд полностью проходит TCP-рукопожатие, договаривается о TLS, отправляет корректный GET / с правдоподобным user agent'ом и читает ответ. Ни один пакет в нём не деформирован. Убивает вас арифметика: атакующему запрос почти ничего не стоит отправить, а вам он обходится в запрос к базе, рендер шаблона и сотню миллисекунд воркера, который в это время не обслуживает никого другого.
Признаки — в вашем же логе доступа, и они обычно очевидны, если их искать, а не просто смотреть на них. Query-строки, обходящие кэш — тысячи запросов к /?1234567, каждый на уникальный URL, который сводит на нет любой ваш кэш, — самая частая примета из всех. Распределение user agent'ов без длинного хвоста: настоящий трафик — это неопрятная смесь из сотен сборок браузеров, а флуд — это часто три строки, повторённые миллион раз, либо строка, которой не пользуется ни один настоящий пользователь. Поле referrer, одинаковое везде. Запросы, полностью игнорирующие ваши статические ресурсы — настоящий браузер после HTML подтягивает CSS, шрифты и изображения; клиент-флудер запрашивает HTML и уходит. И слишком плоское распределение источников: ботнет, размазанный по десяти тысячам домашних адресов, каждый из которых шлёт по два запроса в секунду, выглядит как популярность — пока вы не заметите, что частота на адрес подозрительно однородна.
Затем есть вариант, которому почти не нужен трафик вообще, — медленная атака. Несколько сотен соединений, которые открываются, шлют по одному заголовку каждые двадцать секунд и никогда не завершаются, займут всех ваших воркеров, пока график полосы остаётся плоским. Лечится это не rate limit'ом — частота запросов здесь мизерная, — а агрессивными таймаутами на заголовки и тело запроса, поэтому они и стоят в первом же блоке конфигурации ниже, а не идут довеском в конце.
SP·05Четыре меры, в порядке эффекта на минуту
Под давлением делайте сначала то, что даёт наибольший эффект. Порядок ниже выбран не произвольно — он идёт примерно по убыванию того, сколько нагрузки каждая мера снимает на минуту вашего внимания.
- Отдавайте что-то дешёвое. Микрокэш на тридцать секунд перед вашим приложением превращает тысячу одинаковых запросов в секунду в одно обращение к origin и 999 чтений из памяти. Это самый мощный рычаг почти против любого HTTP-флуда, стоит он один блок директив, а для анонимного трафика это почти всегда безопасно. Добавьте
proxy_cache_lock, чтобы промах мимо кэша не обрушивал на бэкенд лавину одинаковых запросов (thundering herd). - Ограничивайте число одновременных соединений и сокращайте таймауты.
limit_connна адрес плюс жёсткие таймауты на заголовки, тело и keepalive напрочь убивают медленные атаки и не дают ни одному клиенту в одиночку занять весь ваш пул воркеров. Это мера, которая обходится настоящим пользователям дешевле всего. - Ограничивайте частоту запросов, с запасом на всплеск.
limit_reqс разумным значением burst — мера точная, но настраивается дольше, и именно она даёт ложные срабатывания, если выставлять её в панике, а не по собственным базовым показателям. Прежде чем выбрать число, нужно знать свою нормальную частоту запросов на клиента в секунду — именно поэтому post-mortem в конце этого руководства важнее, чем кажется на первый взгляд. - Блокируйте — точечно и неохотно. Сброс конкретных сетей работает, когда источники сконцентрированы, и бесполезен, когда это не так. Кроме того, это плохо стареет: каждая блокировка, добавленная во время инцидента, — это клиент, которому вы, возможно, будете молча отказывать ещё три месяца спустя. Используйте set с таймаутом, чтобы правила истекали сами.
Обратите внимание, чего в списке нет: ручного бана отдельных IP-адресов, повторных перезапусков веб-сервера и отключения firewall, «чтобы посмотреть, поможет ли». Первое слишком медленно, чтобы иметь значение против распределённого источника, второе выбрасывает все ваши уже прогретые соединения, а третье — это именно то, как инцидент превращается во взлом.
SP·06Не боритесь с этим на машине, где лежат ваши данные
Любая мера из перечисленных выше даёт больше пользы, если работает не на той машине, где лежит ваша база данных. Если ваш edge — это отдельный узел, флуд обрывается на машине, единственная задача которой — обрывать флуды: она кэширует, ограничивает частоту и отбрасывает трафик, имея на руках настоящий адрес клиента, а origin вообще видит только небольшой, уже отфильтрованный остаток — и то через приватный туннель. Если edge падает, вы заменяете его за 15 min и ничего не теряете, потому что на нём и не было ничего. Если падает origin, у вас простой и восстановление из бэкапа.
Это же разделение закрывает и обход, из-за которого большая часть защиты от атак оказывается чисто декоративной. Если у origin всё ещё есть публичный слушатель, атакующий, нашедший его адрес — через пассивный DNS, запись в журнале прозрачности сертификатов, MX-запись или предпросмотр ссылки, — может обойти все настроенные вами меры и ударить прямо по приложению. Это встречается достаточно часто, чтобы считать это состоянием по умолчанию для любого «защищённого» сайта, пока не доказано обратное. Версия, которая держится, — это отдельное руководство: офшорный reverse-proxy, у которого на origin вообще нет ни одного публичного слушателя.
Одно предупреждение насчёт середины инцидента: это архитектура, а не первая помощь. Разворачивать edge, переносить DNS и пересобирать туннель прямо под атакой — это работа на два часа, которую под давлением делают плохо, а одно только изменение DNS не вступит в силу раньше, чем истечёт срок, заданный вашим TTL. Если это у вас уже есть — используйте. Если нет — переживите этот час теми мерами, что есть под рукой, а разворачивайте это в спокойную неделю после инцидента, — именно тогда этого никто и не делает.
SP·07День, когда отражатель — это вы, а не цель
У этого инцидента есть и вторая версия, где ваш сервер — не жертва, а вам об этом никто не сообщает. Открытый резолвер, незащищённый демон NTP, memcached без аутентификации на публичном интерфейсе, SSDP- или CLDAP-сервис внутри контейнера, отвечающий на запросы, — каждый из них отвечает на небольшой запрос с подделанным обратным адресом гораздо более объёмным ответом, нацеленным на кого-то другого. С вашей стороны симптомы перевёрнуты: исходящий трафик высокий, входящий скромный, приложение чувствует себя прекрасно, а первым реальным сигналом становится уведомление о злоупотреблении или отключённый порт.
Проверка занимает минуту и относится к тому же runbook'у, потому что это та же самая команда, которую вы уже запускали во время триажа. ss -tulpn не должна показывать ничего, привязанного к публичному адресу, что вы не помещали туда намеренно, а UDP-сервисы заслуживают особого подозрения, потому что именно они дают усиление. Рекурсивный резолвер обязан быть привязан к localhost или к адресу туннеля; memcached и Redis не должны быть доступны из интернета вообще никогда; а любой контейнер, публикующий порт через -p 0.0.0.0:, только что пробил дыру в firewall, который вы настраивали, — потому что Docker прописывает собственные правила раньше ваших. Вот это каждый раз оказывается сюрпризом.
Та же картина накрывает и исходящие флуды с уже скомпрометированной машины — это ещё одна причина, по которой провайдер вдруг отправляет адрес в null-route. Если график исходящего трафика высокий, а приложение простаивает, перестаньте читать конфиги и начните проверять процессы — это вторжение, а не проблема пропускной способности, и правильный ответ — пересобрать машину из заведомо чистого бэкапа, а не пытаться её отфильтровать.
SP·08Когда всё прекратилось: post-mortem и дежурный комплект
Атаки заканчиваются. Обычно атакующему становится скучно, иногда защита делает атаку бессмысленной, а иногда это была подписка на booter с фиксированным сроком, которая просто истекла. Соблазн в этот момент — оставить всё как есть и лечь спать, и именно так временный rate limit превращается в постоянный, забытый, молчаливый 429 для целой страны девять месяцев спустя. Потратьте двадцать минут, чтобы закрыть цикл, пока всё ещё свежо в памяти.
Стоит подготовить три артефакта. Базовые показатели: ваша нормальная частота запросов в секунду, ваша нормальная частота на клиента, ваша нормальная полоса на пике. Без этих цифр любой лимит, который вы выставите во время следующего инцидента, — просто догадка, и половина таких догадок ошибётся в сторону, вредящую клиентам. Список отката: всё, что вы поменяли, с датой и причиной, — чтобы аварийная конфигурация незаметно не стала постоянной. Runbook длиной в четыре команды, который лежит там, куда вы дотянетесь, когда сайт лежит, — не на самом сервере и не только у вас в голове.
Затем закройте два структурных пробела, потому что именно здесь этот слой встаёт в общую схему. Защита поглощает объём, edge не подпускает флуд к вашим данным, а машина позади них всё равно должна быть как следует защищена и иметь восстанавливаемые бэкапы вне хоста, потому что следующий инцидент вполне может оказаться вовсе не флудом. Сервер, который пережил DDoS, а через месяц потерял диск, никогда не был устойчивым — ему просто повезло дважды.
SP·09Шаг за шагом
-
01
Подтвердите, прежде чем что-то менять
Прежде чем трогать конфиг, получите одну честную картину происходящего на машине. Ищите резкий скачок пакетов или запросов и смотрите, куда уходит время, — приложение, которому не хватает CPU из-за программных прерываний, это совсем другой инцидент, чем приложение, ожидающее ответа от базы данных.
# is the box alive, and where is the time going? uptime # load average against your core count vmstat 1 5 # 'in' and 'cs' high, 'id' near zero = packet work mpstat -P ALL 1 3 # %soft pinned on one core = interrupt saturation # is the pipe full, or just busy? ip -s link show eth0 # rx bytes, and the errors/dropped counters ethtool eth0 | grep -i speed # requests per second from your own log, minute by minute tail -n 20000 /var/log/nginx/access.log \ | awk -F'[][]' '{print $2}' | cut -d: -f2-4 | uniq -c | tail -5Прежде чем делать вывод, что это атака, проверьте собственный журнал деплоев и таблицу cron. Инциденты, вызванные собственными руками, на одном VPS случаются чаще, чем флуды, а выглядят снаружи точно так же.
-
02
Измерьте форму трафика за шестьдесят секунд
Теперь классифицируйте его. Три вопроса: сколько различных источников, какой протокол и состояние, и — если это HTTP — какие пути и какие агенты. Ответы определяют, за какую меру вы возьмётесь, а собрать их можно примерно за минуту.
# top source addresses on the wire right now timeout 20 tcpdump -nn -i eth0 -c 20000 2>/dev/null \ | awk '{print $3}' | rev | cut -d. -f2- | rev \ | sort | uniq -c | sort -rn | head -20 # TCP state census: a wall of SYN-RECV is a SYN flood ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn # layer 7: talkers, paths, agents over the last 50k requests L=/var/log/nginx/access.log tail -n 50000 $L | awk '{print $1}' | sort | uniq -c | sort -rn | head -20 tail -n 50000 $L | awk '{print $7}' | sort | uniq -c | sort -rn | head -20 tail -n 50000 $L | cut -d'"' -f6 | sort | uniq -c | sort -rn | head -10Сверьте результат с приметами: уникальные query-строки на одном пути, список user agent'ов без длинного хвоста, отсутствие запросов к статике, или частота на адрес, подозрительно однородная. Если полоса высокая на портах, которые вы не слушаете, — остановитесь здесь: это волюметрический флуд, и значение имеет только шаг шестой.
-
03
Отдавайте что-то дешёвое и ограничьте соединения
Сначала — то, что даёт наибольший эффект. Микрокэш на тридцать секунд схлопывает флуд одинаковых анонимных запросов в одно обращение к origin, а жёсткие таймауты убивают медленные атаки, которые rate limit просто не видит. Поместите и то, и другое в блок
http, а затем сделайте reload, а не restart, чтобы сохранить прогретые соединения.# /etc/nginx/nginx.conf — http block limit_conn_zone $binary_remote_addr zone=perip:10m; limit_req_zone $binary_remote_addr zone=flood:20m rate=10r/s; client_header_timeout 10s; client_body_timeout 10s; send_timeout 10s; keepalive_timeout 20s; reset_timedout_connection on; proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=hot:64m max_size=2g inactive=10m use_temp_path=off;# the server block — cap concurrency, serve the cached copy limit_conn perip 20; location / { proxy_cache hot; proxy_cache_valid 200 301 302 30s; proxy_cache_lock on; proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504; add_header X-Cache $upstream_cache_status; proxy_pass http://127.0.0.1:8080; }nginx -t && systemctl reload nginx curl -sI https://example.com/ | grep -i x-cache # want: HIT on the second call
-
04
Ограничивайте настоящего клиента, а не собственный прокси
Если перед nginx что-то стоит, каждый запрос приходит с одного и того же адреса, и лимит на адрес будет душить прокси вместо атакующего — или вовсе забанит его и положит сайт как раз тогда, когда вы его защищаете. Доверяйте заголовку с прокинутым адресом, но только от адреса прокси, и никогда — от всего интернета, а уже потом применяйте лимит.
# /etc/nginx/conf.d/realip.conf — the tunnel or edge address only set_real_ip_from 10.66.0.1; real_ip_header X-Forwarded-For; real_ip_recursive off;
# burst absorbs bursty humans; nodelay keeps the page fast for them location / { limit_req zone=flood burst=20 nodelay; limit_req_status 429; } # the expensive paths get a much tighter bucket of their own location ~ ^/(search|login|register|api/) { limit_req zone=flood burst=5; limit_req_status 429; }Затем посмотрите, что вы только что сделали:
tail -f /var/log/nginx/error.log | grep limiting. Если ограничиваемые адреса похожи на ваших клиентов, значит, порог слишком низкий, — поднимите его. Лимит, который блокирует настоящих пользователей, — это простой, который вы устроили себе сами. -
05
Блокируйте точечно и давайте каждой блокировке срок действия
Имеет смысл, только если шаг два показал сконцентрированные источники. Используйте set, а не тысячу правил — поиск в
ipsetвыполняется за константное время, а длинная цепочкаiptablesобходится для каждого пакета и сама превращается в своего рода отказ в обслуживании. Давайте каждой записи таймаут, чтобы сегодняшняя чрезвычайная мера не стала в следующем году молчаливым списком блокировки.ipset create flood hash:net timeout 3600 -exist iptables -I INPUT -m set --match-set flood src -j DROP # feed it from the census: /24s you actually verified, not guesses for n in 203.0.113.0/24 198.51.100.0/24; do ipset add flood $n -exist; done # SYN flood: let the kernel do the part it is good at sysctl -w net.ipv4.tcp_syncookies=1 sysctl -w net.ipv4.tcp_max_syn_backlog=8192 sysctl -w net.core.somaxconn=8192 ipset list flood | head -20 # keep a copy of this for the post-mortem
Не поддавайтесь соблазну заблокировать по гео сразу целую страну, если не можете назвать поимённо клиентов, которых отрезаете. И никогда не используйте
ufw disable, чтобы проверить теорию: машина без firewall под активной атакой — это способ превратить инцидент с полосой пропускания во взлом. -
06
Эскалируйте на уровень, который действительно способен это поглотить
Если счётчики интерфейса говорят, что канал забит, или дропы растут при простаивающем CPU, вы упёрлись в потолок того, что вообще можно сделать на сервере. Подтвердите эти показания, а затем эскалируйте, а не продолжайте тюнинг.
# drops in the stack itself — second column is 'dropped' awk '{print strtonum("0x" $2)}' /proc/net/softnet_stat | paste -sd+ | bc # interface-level loss against link speed ip -s link show eth0 | sed -n '3,6p'При включённой апстрим-фильтрации обычно делать нечего: детектирование работает постоянно, и флуд сбрасывается в сети раньше, чем доходит до вашего порта, — на нашем парке это 1.5 Tbps мощности перед каждым тарифом, так что инцидент зачастую виден постфактум только как график. Если атака — это корректно сформированный HTTP-флуд, а не волюметрический, то это уже случай для L7-защиты, потому что флуд запросов неотличим от пользователей на уровне пакетов и должен оцениваться выше по стеку. Если у вас нет вообще никакой защиты, реалистичные варианты — либо встать за edge, у которого она есть, либо ждать, — и спланировать первое стоит до, а не после следующей атаки.
-
07
Закройте цикл: проверьте, откатите, затем запишите
Проверяйте снаружи машины, а не из shell'а на ней самой. Затем осознанно отмените аварийные меры, оставив те, что были хорошей идеей в любом случае, и зафиксируйте цифры, чтобы следующий инцидент отталкивался от базовых показателей, а не от догадки.
# from somewhere else entirely: is the site healthy for a normal user? curl -s -o /dev/null -w 'code=%{http_code} ttfb=%{time_starttransfer}s\n' \ https://example.com/ # did you leave a limit that is biting real people? grep -c 'limiting requests' /var/log/nginx/error.log # what is still blocked, and when does it expire? ipset list flood | head -30Оставьте кэш, таймауты и syncookies — это постоянные улучшения. Откатите агрессивные rate limit'ы к измеренным базовым показателям с запасом и дайте записям в ipset истечь самим. Затем впишите четыре команды из первого и второго шагов в runbook, хранящийся где-то не на этом сервере, — рядом с вашей нормальной частотой запросов в секунду и нормальной пиковой полосой. Следующая атака станет более коротким инцидентом просто потому, что эти две цифры уже есть.


