Постмортем: 4 мои ошибки во время отражения DDOS атаки (спойлер — выкуп в $250 мы все-таки не запла…
- четверг, 27 февраля 2025 г. в 00:00:14
Мой обеденный кофе прервался. Начали приходить уведомления от мониторинга, что сайт и API не отвечают, а CloudFlare отдаёт 521-ю ошибку на все запросы. Спустя пять минут ко мне в личку пришли пользователи с жалобами на неработающие приложения. А ещё спустя пять позвонил сооснователь проекта и сказал, что от нас требуют $250 за остановку DDOS'a.
Ниже расскажу, как мы командой решали проблему, какие ошибки допустил я и чем всё закончилось.
Содержание:
Ошибка 1: слишком положился на Cloudflare и не настроил rate limit на уровне сервера
Ошибка 2: SSR запросы ходили в API через домен (а не локальную сеть)
Ошибка 3: GitLab находился на том же домене, что и основная система без white list'a
Я отвечаю за разработку No Code платформы для создания Telegram Mini App'ов. У нас есть frontend на NextJS, backend на Java и Go. В качестве reverse proxy мы используем Traefik, а для балансировки нагрузки, SSL и DDOS защиты — CloudFlare. Всё это мониторится Prometheus'ом и выводится в Grafana.
У нас стоит несколько серверов за защитой от CloudFlare. На каждом сервере своя копия frontend, backend и Traefik'a. Запущено несколько серверов для исключения единой точки отказа и для запаса в х10-х15 по вертикальному масштабированию.
Kubernetes пока что не используем, потому что нет (точнее не было) регулярных пиков нагрузки и хватает ручного масштабирования.
Всё это в облаке, включая базу данных и кэш.
Схематично проект выглядит вот так:
API обслуживает:
личный кабинет (конструктор);
приложения клиентов (~95% нагрузки);
веб-хуки от Telegram'a;
веб-хуки от платёжных систем;
Из других важных моментов для понимания контекста:
Проекту ~7 месяцев.
Средний MAU (monthly active users) системы ~125 000 человек.
Постоянная команда разработки 5 человек (middle-senior), включая меня.
За CI \ CD и администрирование отвечаю я. DevOps'а у нас нет, потому что инфраструктурных задач очень мало (да и мы все-таки стартап на грани самоокупаемости).
Моя специализация — это full-stack разработка (и управление разработкой). С администрированием серверов дружу на уровне стандартного разработчика.
Для ориентира: настрою фаерволл по гайдам, заведу self hosted GitLab и CI \ CD, напишу bash-скрипты для сбора метрик, напечатаю конфиг Nginx'a по памяти. Но вот настраивать права доступа или точечно фильтровать трафик я буду долго.
Во время атаки решение проблемы искали всей командой. При этом были в довольно сильном стрессе и от самой атаки, и от града сообщений пользователей (как в публичном чате, так и в личках).
С DDOS'ом никто в нашей команде раньше не сталкивался.
Теперь, думаю, есть примерное понимание, какая у нас архитектура и контекст проекта. Если интересно, другие детали о развитии этого проекта и в целом о разработке я пишу в своем Telegram-канале.
Собственно, всё то, что описано в начале статьи началось с уведомлений об отказе API и недоступности страниц фронта:
Я сразу побежал в Grafana и увидел такую картину:
Обратите внимание: графики загрузки CPU и RAM с пробелами. В этот момент мониторинг прерывается, потому что сервера отказывают (причём сразу все). И я, разумеется, сразу пытался их перезагрузить (синие стрелки), что помогало на ~1–2 минуты.
Затем мне пересылают следующие сообщения:
Становится понятно: нас решили тактично пошантажировать с формулировкой «помощи в устранении уязвимостей». Вести переговоры мы были не готовы, потому что нет гарантий, что это поможет. Точнее наоборот мотивирует и дальше так делать с другими проектами.
Итак, мы пошли искать, как именно нас ломают и что не выдержатвает. В ходе поиска и устранения проблем, выявили следующие ошибки:
Наши сервера стоят за CloudFlare. В том числе из расчёта, что он закроет нас от DDOS атак. Я предполагал, что для защиты от DDOS'a мелко-среднего масштаба этого достаточно.
Уточню: сервера не светят публичные IP адреса (почту и т.д. мы не рассылаем). Весь трафик идёт исключительно через балансировщик нагрузки. Traefik работает через 80'й порт, а CloudFlare отвечает за SSL.
Когда всё упало, в мониторинге не было видно возросшего числа запросов:
Следовательно, я подумал, что засветились сервера и атака идёт по ним в обход CloudFlare. Поэтому первым решением было заблокировать всё через ufw
, кроме 22
и 80
порта. Но... не помогло. Через 10-20 секунд после перезапуска сервера запросы всё ещё отваливались.
В этот момент подтянулась статистика CloudFlare (оказалось, она приходит с небольшой задержкой):
Значит трафик всё-таки идёт через CloudFlare, но он его почему-то не фильтрует. При этом капча и режим «Under attack» были включены в первые минуты атаки.
Логи в Traefik показывали, что нам отправляют уйму запросов и не дожидаются их завершения (если я правильно помню, это называется connections flood):
Решение состояло из двух шагов:
1) Ограничить количество запросов в минуту кастомным правилом в CloudFlare даже для тех, кто прошел проверку на бота.
Оказалось, у CloudFlare есть отдельная настройка для ограничения requests per minute. Поправили её и ограничили до 100 запросов в минуту с одного IP. Этим правилом оказалось заблокировано ~2 млн запросов.
2) Ограничить количество запросов и параллельных подключений для каждого Traefik'a.
Каждый Traefik работает на своём сервере независимо от других. На случай, если CloudFlare все-таки пропустил спам-запросы, нужно ограничить их количество на стороне нашего прокси.
Мы выставили следующие лимиты:
максимум 10 запросов в секунду с одного IP;
максимум 10 параллельных подключений с одного IP (на случай, если идут долгие удерживающие запросы);
Это помогло. CPU и RAM какое-то время поборолись... и нагрузка спала. Дальше мы вышли на контракт с DDOS'ером и он уже не смог положить нас.
Правда поиск этих двух пунктов, настройка конфигов и попытки разобраться, что не так заняли ~2 часа. Все-таки делали мы это под некоторым стрессом и под градом сообщений от пользователей. Это заняло много времени.
Уже после всего DDOS'ер сказал, что его способ подразумевал обход CloudFlare. Насколько я понял, так реально делать. Но очень сложно делать массово. Следовательно, было ограниченное количество ботов, которые делают 90% запросов и блокировка способами выше помогла.
Веб-часть нашей системы делает запросы в два шага:
На стороне SSR (server side rendering) берутся общие данные для всех приложений (в основном, закэшированные).
На клиентской части берутся данные конкретного пользователя отдельным запросом.
Схематично это выглядит так:
Но мы не поставили локальный IP адрес для SSR части. Получилось, что даже серверная часть фронтенда ходила в API через CloudFlare. Это никогда не мешало и не создавало задержек, поэтому и не замечали раньше.
Следовательно, когда мы включили режим защиты от DDOS атаки в CloudFlare, все наши серверные запросы к API отпали. Точнее CloudFlare показывал капчу и запросы не проходили:
Как результат: мы отбились от DDOS, но все наши клиентские приложения всё равно не работали. Причём мы, получается, положили их сами. DDOS только проявил проблему с нашей стороны.
Мы поправили все SSR запросы, чтобы они проходили через локальную сеть Docker'a. Но это заняло чуть больше времени, чем должно было бы, потому что возникла следующая проблема...
Во время исправления проблем нам нужно было деплоить обновленные конфигурации Traefik'a и фронта. Но наш GitLab находился на поддомене основного домена, куда шла атака. CloudFlare начал показывать капчу всему, что обращается к GitLab. И под эту капчу попали GitLab runner'ы, которые собирают проект.
Для тех, кто не в курсе: GitLab runner — это сервис, который запускается отдельно от GitLab и подключается к нему, чтобы брать задачи на сборку в работу. Получается, мы пушим код в GitLab, раннер делает запрос в GitLab и берёт задачу в работу.
В итоге все наши билды встали (пример двух коммитов):
Дело было в спешке и тут я не сразу сообразил, что нужно просто добавить сервер GitLab в белый список IPшников. Поэтому последующие 30 минут мы дружно деплоили новые конфиги и фронт вручную... Напомню: ситуация стрессовая, параллельно решаем сразу несколько проблем и успокаеваем пользователей.
P.S. Со временем про белые списки вспомнил один из разработчиков, мы поправили и всё заработало в штатном режиме.
Как подсказал мне по итогу мой знакомый СТО Иван Томилов, все сервера по-хорошему нужно прятать в приватную подсеть. Чтобы доступ к ним имел только балансировщик нагрузки, GitLab, мониторинг и сервер-бастион, через который мы подключаемся к серверам.
Тогда отпала бы проблема с определением причины: засветили ли мы сервера или был каким-то образом пробит CloudFlare. То есть сейчас архитектура такая:
А должно быть так:
Собственно, созданием приватной сети я и займусь на этой неделе. Пока что это не стало причиной проблем, но рано или поздно станет.
По итогу, наша команда и лично я получили крайне наглядный опыт защиты проекта от DDOS атаки. Нас смогли вполне заслуженно положить в первую очередь из-за моих ошибок. Которые теперь я знаю, командой мы их исправили и держим в уме на будущее.
После устранения ошибок мы пообщались с DDOS'ером в формате "мы всё подчинили, сломай ещё, а если выйдет — будем общаться дальше". Мы справились:
После восстановления график запросов в этот день выглядел вот так:
Важные уроки, которые были вынесены из ситуации:
Нельзя полагаться на CloudFlare на 100%.
У CloudFlare есть правила для ручной настройки rate limit'a для тех, кто прошёл проверку на бота. Их нужно включать.
Всегда нужно настраивать rate limit на уровне сервера.
GitLab, мониторинг и другие сервисы нужно добавлять в белый список CloudFlare.
SSR запросы нужно отправлять через локальную сеть (если есть такая возможность). Так быстрее, не будет проблем в случае сбоев в сети или при появлении капчи CloudFlare.
Всю инфраструктуру нужно собирать в приватную сеть, чтобы исключить доступ к серверам в обход защиты (и не пытаться угадать, по ним ли идёт атака).
Планы на ближайшую неделю:
Закрыть все сервера приватной сетью с ограниченным доступом для CloudFlare, мониторинга и GitLab.
Взять консультацию и провести аудит основных инфраструктурных уязвимостей.
Вместе с СЕО извиниться, объяснить пользователям подробности ситуации и какие шаги предприняли, чтобы избежать такой же проблемы в будущем.
Для тех, кто разбирается в защите от таких ситуаций и видит, что чего-то нам не хватает — буду рад советам и замечаниям в комментариях или личке. Но напомню, что ресурсов у нас сильно меньше, чем у большой корпорации.
Если статья вам понравилась или оказалось полезной, поставьте, пожалуйста, лайк. Это мотивирует писать объемные статье и рассказывать конкретику из своего опыта.
Ну и, как полагается, у меня есть Telegram-канал, в котором я рассказываю про разработку, развитие SaaS-сервисов и управление IT проектами. В том числе о проблемах, которые возникают. Там же я выкладываю ссылки на новые статьи на Habr'e.