Постмортем: 4 мои ошибки во время отражения DDOS атаки (спойлер — выкуп в $250 мы все-таки не заплатили)

в 12:00, , рубрики: CloudFlare, nginx, traefik, администрирование

Мой обеденный кофе прервался. Начали приходить уведомления от мониторинга, что сайт и API не отвечают, а CloudFlare отдаёт 521-ю ошибку на все запросы. Спустя пять минут ко мне в личку пришли пользователи с жалобами на неработающие приложения. А ещё спустя пять позвонил сооснователь проекта и сказал, что от нас требуют $250 за остановку DDOS'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 пока что не используем, потому что нет (точнее не было) регулярных пиков нагрузки и хватает ручного масштабирования.

Всё это в облаке, включая базу данных и кэш.

Схематично проект выглядит вот так:

Постмортем: 4 мои ошибки во время отражения DDOS атаки (спойлер — выкуп в $250 мы все-таки не заплатили) - 1

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 минуты.

Затем мне пересылают следующие сообщения:

Постмортем: 4 мои ошибки во время отражения DDOS атаки (спойлер — выкуп в $250 мы все-таки не заплатили) - 4
Постмортем: 4 мои ошибки во время отражения DDOS атаки (спойлер — выкуп в $250 мы все-таки не заплатили) - 5

Становится понятно: нас решили тактично пошантажировать с формулировкой "помощи в устранении уязвимостей". Вести переговоры мы были не готовы, потому что нет гарантий, что это поможет. Точнее наоборот мотивирует и дальше так делать с другими проектами.

Итак, мы пошли искать, как именно нас ломают и что не выдержатвает. В ходе поиска и устранения проблем, выявили следующие ошибки:

Ошибка 1: я слишком положился на Cloudflare и не настроил rate limit на уровне сервера

Наши сервера стоят за CloudFlare. В том числе из расчёта, что он закроет нас от DDOS атак. Я предполагал, что для защиты от DDOS'a мелко-среднего масштаба этого достаточно.

Уточню: сервера не светят публичные IP адреса (почту и т.д. мы не рассылаем). Весь трафик идёт исключительно через балансировщик нагрузки. Traefik работает через 80'й порт, а CloudFlare отвечает за SSL.

Когда всё упало, в мониторинге не было видно возросшего числа запросов:

График к API аналогичный

График к API аналогичный

Следовательно, я подумал, что засветились сервера и атака идёт по ним в обход CloudFlare. Поэтому первым решением было заблокировать всё через ufw, кроме 22 и 80 порта. Но... не помогло. Через 10-20 секунд после перезапуска сервера запросы всё ещё отваливались.

В этот момент подтянулась статистика CloudFlare (оказалось, она приходит с небольшой задержкой):

Там, где сейчас крестик - график только появился, когда я смотрел в CloudFlare

Там, где сейчас крестик - график только появился, когда я смотрел в CloudFlare

Значит трафик всё-таки идёт через CloudFlare, но он его почему-то не фильтрует. При этом капча и режим "Under attack" были включены в первые минуты атаки.

Логи в Traefik показывали, что нам отправляют уйму запросов и не дожидаются их завершения (если я правильно помню, это называется connections flood):

Лог Traefik'a

Лог Traefik'a

Решение состояло из двух шагов:

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% запросов и блокировка способами выше помогла.

Ошибка 2: SSR запросы ходили в API через домен (а не локальную сеть)

Веб-часть нашей системы делает запросы в два шага:

  • На стороне SSR (server side rendering) берутся общие данные для всех приложений (в основном, закэшированные).

  • На клиентской части берутся данные конкретного пользователя отдельным запросом.

Схематично это выглядит так:

В теории

В теории

Но мы не поставили локальный IP адрес для SSR части. Получилось, что даже серверная часть фронтенда ходила в API через CloudFlare. Это никогда не мешало и не создавало задержек, поэтому и не замечали раньше.

Следовательно, когда мы включили режим защиты от DDOS атаки в CloudFlare, все наши серверные запросы к API отпали. Точнее CloudFlare показывал капчу и запросы не проходили:

На практике

На практике

Как результат: мы отбились от DDOS, но все наши клиентские приложения всё равно не работали. Причём мы, получается, положили их сами. DDOS только проявил проблему с нашей стороны.

Мы поправили все SSR запросы, чтобы они проходили через локальную сеть Docker'a. Но это заняло чуть больше времени, чем должно было бы, потому что возникла следующая проблема...

Ошибка 3: GitLab находился под тем же доменом, что и основная система без white list'a

Во время исправления проблем нам нужно было деплоить обновленные конфигурации Traefik'a и фронта. Но наш GitLab находился на поддомене основного домена, куда шла атака. CloudFlare начал показывать капчу всему, что обращается к GitLab. И под эту капчу попали GitLab runner'ы, которые собирают проект.

Для тех, кто не в курсе: GitLab runner — это сервис, который запускается отдельно от GitLab и подключается к нему, чтобы брать задачи на сборку в работу. Получается, мы пушим код в GitLab, раннер делает запрос в GitLab и берёт задачу в работу.

В итоге все наши билды встали (пример двух коммитов):

Постмортем: 4 мои ошибки во время отражения DDOS атаки (спойлер — выкуп в $250 мы все-таки не заплатили) - 12

Дело было в спешке и тут я не сразу сообразил, что нужно просто добавить сервер GitLab в белый список IPшников. Поэтому последующие 30 минут мы дружно деплоили новые конфиги и фронт вручную... Напомню: ситуация стрессовая, параллельно решаем сразу несколько проблем и успокаеваем пользователей.

P.S. Со временем про белые списки вспомнил один из разработчиков, мы поправили и всё заработало в штатном режиме.

Ошибка 4: сервера не находились в приватной сети

Как подсказал мне по итогу мой знакомый СТО Иван Томилов, все сервера по-хорошему нужно прятать в приватную подсеть. Чтобы доступ к ним имел только балансировщик нагрузки, GitLab, мониторинг и сервер-бастион, через который мы подключаемся к серверам.

Тогда отпала бы проблема с определением причины: засветили ли мы сервера или был каким-то образом пробит CloudFlare. То есть сейчас архитектура такая:

Постмортем: 4 мои ошибки во время отражения DDOS атаки (спойлер — выкуп в $250 мы все-таки не заплатили) - 13

А должно быть так:

Постмортем: 4 мои ошибки во время отражения DDOS атаки (спойлер — выкуп в $250 мы все-таки не заплатили) - 14

Собственно, созданием приватной сети я и займусь на этой неделе. Пока что это не стало причиной проблем, но рано или поздно станет.

Заключение

По итогу, наша команда и лично я получили крайне наглядный опыт защиты проекта от DDOS атаки. Нас смогли вполне заслуженно положить в первую очередь из-за моих ошибок. Которые теперь я знаю, командой мы их исправили и держим в уме на будущее.

После устранения ошибок мы пообщались с DDOS'ером в формате "мы всё подчинили, сломай ещё, а если выйдет — будем общаться дальше". Мы справились:

Несмотря на негатив для нас, на пользу это действительно пошло

Несмотря на негатив для нас, на пользу это действительно пошло

После восстановления график запросов в этот день выглядел вот так:

Постмортем: 4 мои ошибки во время отражения DDOS атаки (спойлер — выкуп в $250 мы все-таки не заплатили) - 16

Важные уроки, которые были вынесены из ситуации:

  • Нельзя полагаться на CloudFlare на 100%.

  • У CloudFlare есть правила для ручной настройки rate limit'a для тех, кто прошёл проверку на бота. Их нужно включать.

  • Всегда нужно настраивать rate limit на уровне сервера.

  • GitLab, мониторинг и другие сервисы нужно добавлять в белый список CloudFlare.

  • SSR запросы нужно отправлять через локальную сеть (если есть такая возможность). Так быстрее, не будет проблем в случае сбоев в сети или при появлении капчи CloudFlare.

  • Всю инфраструктуру нужно собирать в приватную сеть, чтобы исключить доступ к серверам в обход защиты (и не пытаться угадать, по ним ли идёт атака).

Планы на ближайшую неделю:

  • Закрыть все сервера приватной сетью с ограниченным доступом для CloudFlare, мониторинга и GitLab.

  • Взять консультацию и провести аудит основных инфраструктурных уязвимостей.

  • Вместе с СЕО извиниться, объяснить пользователям подробности ситуации и какие шаги предприняли, чтобы избежать такой же проблемы в будущем.

Для тех, кто разбирается в защите от таких ситуаций и видит, что чего-то нам не хватает — буду рад советам и замечаниям в комментариях или личке. Но напомню, что ресурсов у нас сильно меньше, чем у большой корпорации.

Если статья вам понравилась или оказалось полезной, поставьте, пожалуйста, лайк. Это мотивирует писать объемные статье и рассказывать конкретику из своего опыта.

Ну и, как полагается, у меня есть Telegram-канал, в котором я рассказываю про разработку, развитие SaaS-сервисов и управление IT проектами. В том числе о проблемах, которые возникают. Там же я выкладываю ссылки на новые статьи на Habr'e.

Автор: RostislavDugin

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js