29 октября закончился отбор на Кубок CTF России, который проводила наша команда, C4T BuT S4D. У нас довольно большой опыт проведения подобных ивентов, но в этот раз мы попробовали много новых технологий и плагинов для CTFd (и даже новое облако), а также построили сложную инфраструктуру, готовясь к возможным DDoS атакам, подобным тем, которые произошли в прошлом году на этом соревновании.
Построением и настройкой инфраструктуры и CTFd занимался я, @pomo_mondreganto. По неудачному стечению обстоятельств отбор пересекся с моими перелетами в отпуск (которые к тому же задержали), поэтому тимлид @jnovikov страховал и поддерживал инфраструктуру во время соревнования.
Вся инфраструктура была развернута в облаке EdgeCenter, которое было бесплатно предоставлено с солидными лимитами для соревнования. В целом впечателения от их использования крайне положительные, хоть проблема в одной из их фич и привела к написанию данного постмортема (но об этом позже). Опыт использования этого облака у нас был очень небольшой — мы проводили на нем только отбор на BRICS+ CTF 2023 месяцем ранее.
Так как в соревновании участвовали школьники, студенты и смешанные команды в разных зачетах, одним из требований к борде было разделить зачеты любым способом, чтобы иметь возможность визуализировать топ-10 команд в каждом зачете, проходящие на финал. Изначально для этого мы планировали использовать этот плагин, но возникло сразу несколько проблем:
Следующим плагином был CTFd-Whale. Мы сразу пошли по пути advanced настройки, всё заработало практически сразу и почти без допиливаний. Понадобилось только добавить в код плагина возможность логиниться не в стандартный Docker registry, а в Yandex Container Registry, который мы использовали для приватного хостинга докер образов с тасками (цены у них демократичны, и работают они очень хорошо).
Схема инфраструктуры Кубка CTF России 2023
Схема инфраструктуры Кубка CTF России 2023
На диаграмме показана немного упрощенная схема инфраструктуры. Каждый прямоугольник — отдельная виртуальная машина. Цветами на схеме обозначена условная “опасность” хоста. Зеленый — хост под Cloudflare, защищен. Красный — открытый хост, самая большая вероятность атаки. Фиолетовый — адрес хоста можно узнать, но атаковать сложно, так как хост за файрволом. Синий — хост, с которого начался данный постмортем, об этом ниже.
CTFd — самое простое, здесь запущен только CTFd в Docker с помощью стандартного compose. Домен проксируется через cloudflare.com для защиты от DDoS, а в конфиге nginx прописан whitelist адресов, которые могут делать запросы на CTFd (здесь можно почитать, как это делается). В whitelist также добавлены публичные адреса VPN нашей команды, чтобы иметь возможность заливать в таски файлы, превышающие ограничения бесплатного тарифа Cloudflare (100MB). В Cloudflare включен flexible SSL и редиректы HTTP → HTTPS, чтобы CTFd всегда работал по HTTPS, но при этом на самой машине не нужно было создавать и настраивать сертификаты. Все порты, кроме 22 (ssh) и 80 (nginx) закрыты файрволом облака.
Swarm master + frpc — это не очень тяжелая машинка, которая участвует в кластере swarm, в котором поднимаются таски плагином whale, описанным выше, в качестве мастера, то есть только с неё можно деплоить в кластер новые сервисы. Также на ней запущен клиент frp (aka frpc), который подключен к overlay сети swarm и может проксировать запросы в любой таск. На этой машине открыт порт 2376 с сокетом Docker, но он защищен файрволом облака (разрешающим запросы на него только с адреса CTFd) и сертификатами (+rep авторам Whale за поддержку доступа к Docker с сертификатами из коробки).
frps-proxy — ещё одна относительно “легкая” машинка, на которой запущен только frps — сервер frp, который перенаправляет запросы на frpc (описан в предыдущем пункте). Запросы бывают по HTTP (e.g. web, ppc) и по сырому TCP (e.g. pwn). Для HTTP настроен Cloudflare точно так же, как для CTFd — с проксированием всех доменов и HTTPS. Для этого в Cloudflare создана A-запись *, то есть любой поддомен, кроме явно прописанных, будет вести на frps. Точно так же есть nginx, который разрешает только запросы с адресов Cloudflare. Проблема возникает с тасками, работающими по TCP — для них надо открыть на машинке большой диапазон портов (10000-15000 в нашем случае), и она становится уязвима к DDoS. В файрволе frps-proxy к диапазону портов TCP-тасков разрешен доступ только с адреса pwn-proxy.
pwn-proxy — легкая машинка, на которой запущен multiproxy, простой прокси диапазона TCP-портов на Go, который я написал специально для кубка. Этот хост выступает как точка отказа и снижает последствия DDoS на TCP-порты (при атаке на pwn-proxy HTTP-таски продолжат работать). У облака EdgeCenter есть услуга чистого TCP трафика ”защита инфраструктуры”, но перестраховаться не помешает. Eсли упадет frps-proxy, будут недоступны сразу все per-team таски, если упадет pwn-proxy, будут недоступны только половина per-team тасков.
Swarm workers — группа машин (5 штук, 80 ядер суммарно), на которой запускаются per-team таски. Такая схема позволяет быстро добавлять машинки в кластер запуском всего одной команды на воркере (docker swarm join) и одной команды на мастере (повесить метку linux-1 на нового воркера, чтобы показать плагину Whale, что на эту машину можно шедулить таски). В файрволе этих хостов прописаны все порты, которые могут понадобиться докеру (обратите внимание, что это порты для воркеров, для мастера их нужно больше):

Бросается в глаза странность в источниках портов для докера — прописан весь диапазон адресов облака. Правильнее было бы либо явно перечислить все адреса воркеров и мастера, либо объединить их в приватную сеть (EdgeCenter дает такую возможность) и указать её. К сожалению, первое мне делать было лень (и вероятность DDoS из подсети EdgeCenter ничтожно мала), а второе довольно странным образом ломает сеть на машинках и сам докер (при подключении публичной и приватной сети с файрволами через некоторое время публичная сеть на машинке умирает и не реанимируется).
slons worker (под конец их было 2) и client-side webs — машины, на которых запущены не per-team таски (таких тасков было всего 3). Все они под Cloudflare, как описано выше. Так как Cloudflare не умеет проксировать запросы на нестандартные порты, дополнительно на хостах мы поднимаем реверс-прокси Caddy с примерно следующим конфигом: