
Плохо настроенные доступы к инфраструктуре могут стать причиной необратимых инцидентов: были случаи, когда буквально несколько обнаруженных сканером устройств BMC позволили скомпрометировать целое облако с 25 тыс. хостов.
Меня зовут Павел Пушкарёв, я разработчик сервиса по аренде выделенных серверов Yandex BareMetal. Мы с коллегами из Yandex Infrastructure создавали его для пользователей Yandex Cloud, опираясь на накопленный в Яндексе опыт работы с железом, и максимально переиспользовали лучшие практики.
Но есть разница между внутренним сервисом в Яндексе и внешним в Облаке: доступ к серверу должны получать совсем другие люди по другим правилам. В BareMetal железо отдаётся целиком, но управлять им приходится через ту же самую IPMI-сеть, куда внутри Яндекса пускают только инженеров дата-центра.
К тому же если к физическому серверу в стойке инженер со специальным доступом может подключиться к management-интерфейсу через физический порт, то для предоставления доступа через KVM вовне нужны дополнительные условия. И в первую очередь речь идёт о безопасности.
В этой статье — история о том, как мы пустили в IPMI внешних людей. Спойлер: почти каждое решение открывало следующую задачу, и половина схемы выросла именно из этого.
KVM, доступы и протоколы
Есть два канонических примера, когда KVM нам особенно пригодится: установка собственной операционной системы и донастройка сервера, если случайно неправильно ввёл сетевые настройки, а значит, управление по сети недоступно. На Хабре уже было достаточно материалов про эволюцию IPMI, но для незнакомых с предысторией этих протоколов оставлю историческую мини-справку под спойлером.
Как мир пришёл к IPMI и почему ещё не перешёл на HTML5
KVM подразумевает использование как минимум двух проводов, при этом один из этих проводов — совсем неудачный: ни HDMI, ни тем более VGA нормально завернуть в сеть не получается без большого количества дополнительного оборудования. Для решения этой проблемы в мире придумали в каждый сервер встраивать BMC — плату, которая позволяет с одной стороны внутри сервера выглядеть как подключённый монитор и клавиатура, но наружу раздавать данные по обычному интерфейсу Ethernet.
Снаружи сервера оно выглядит приблизительно вот так:

IPMI придумали, чтобы управлять сервером через BMC. Этот протокол позволяет единообразно соединяться с серверами различных производителей и управлять ими: можно, например, удалённо включить или выключить сервер или считать температуру процессора. Но вот показать, что сейчас происходит на экране сервера, таким образом нельзя: нет общего стандарта, и каждый производитель может придумать какое-то собственное решение для обеспечения такого доступа.
Раньше производители предоставляли для него приложения типа Java Web Start: браузер скачивает jar-архив с приложением, запускает его, и дальше приложение соединяется с сервером по собственному закрытому протоколу и показывает элементы управления внутри приложения. Минус такого подхода очевиден: не каждый будет рисковать запускать у себя Java-приложение. Кроме того, некоторые старые прошивки BMC используют очень старые версии Java и очень старые версии хешей, например, md4 (да, даже не md5!). Подписанные такими хешами приложения на современных ОС, разумеется, не запустятся.
Современный мир, понимая эту проблему, ушёл в сторону HTML5: сегодня браузеры в состоянии нарисовать картинку на <canvas/> достаточно быстро, все браузеры умеют устанавливать соединения websocket. Это было бы идеальным решением, если бы не то, что протокол, который гоняется по websocket, тоже закрытый и зависит от производителя оборудования.
Какие были особенности у нас на старте разработки:
Внутри Яндекса мы везде используем протокол IPv6. При этом часть оборудования в IPMI-сети умеет работать только по IPv4, поэтому был нужен механизм преобразования одного протокола в другой.
У нашей облачной платформы уже была принятая модель безопасности. И BareMetal как новому сервису также нужно было в неё встроиться, несмотря на его «железную» природу.
Покажу, как мы это учитывали.
IPMI Proxy
Для управления арендуемыми серверами в Yandex BareMetal нам был нужен какой-то способ унифицировать все платформы. Такой способ мы назвали IPMI Proxy.

На каждое пользовательское соединение мы поднимаем отдельный контейнер Docker. А внутри контейнера мы поднимаем то, что может быть необходимо для доступа к серверу:
систему XWindow, обеспечивающую графический интерфейс;
сервер VNC, который будет собирать экран с XWindow и отдавать по сети единообразно;
Java с заниженными параметрами безопасности, чтобы можно было запускать приложения JavaWS;
браузер Firefox, который может отображать HTML5-вёрстку;
кроме того, на рабочем столе может быть дополнительное приложение Java для монтирования образов по проприетарному протоколу.
Это ещё не полноценная операционная система в Докере — мы оставляем только те приложения, которые нужны для работы с конкретной платформой: так проще и клиентам, и нам.
Как видно из списка запущенных компонентов, IPMI Proxy преобразует закрытый протокол производителя в одинаковый для всех платформ протокол vnc, который затем отображается в консоли Yandex Cloud. Картинку для пользователей рисует novnc.
При такой архитектуре может потребоваться достаточно большое количество ресурсов на установленные соединения, так что необходимо средство масштабирования. Поэтому мы запускаем IPMI Proxy в самом Yandex Cloud, чтобы была возможность увеличивать количество ресурсов за балансером и за счёт этого держать пользовательскую нагрузку.
IPMI Router
Для преобразования IPv4 в IPv6 есть готовые механизмы. Мы остановились на jool — это модуль ядра Linux, который позволяет ловить пакеты IPv6 с одной стороны и отправлять аналогичные пакеты IPv4 с другой (а также делать эту операцию в обратном направлении). Серверы, на которых стоит Linux с jool, мы назвали IPMI Router:

Понятно, что размеры адресов двух протоколов совсем разные, поэтому мы приняли простое правило для jool: каждому адресу IPv4 мы сопоставляем адрес с каким-то известным префиксом, но последние байты в этих адресах совпадают. Например, если у bmc адрес 10.1.2.3, а у IPMI Router префикс 2a02:6bf:fff0:7010::/64, то финальный адрес этого BMC снаружи IPMI Router будет 2a02:6bf:fff0:7010::0a01:0203.
Хорошо, но как вообще BMC знает, что у неё адрес 10.1.2.3? Для этого мы используем DHCP-сервер, который также запущен на IPMI Router.
У этой схемы всё хорошо, кроме одного нюанса: IPMI Router — это физический маршрутизатор, а значит, он может сломаться, и, в отличие от виртуальных сущностей, его восстановление может занять длительное время. Значит, нам нужен какой-то резервный маршрутизатор, который подхватит работу в случае проблем. Здесь мы тоже не стали изобретать велосипед, а воспользовались готовым протоколом VRRP (virtual router redundancy protocol), который позволяет одному маршрутизатору работать, а второму — следить за живостью первого, чтобы подхватить работу в случае неполадок.
Тут снова есть проблема. Стандартный сервер DHCP выдаёт адреса из какого-то диапазона, и при этом локально запоминает своё состояние. Соответственно, если IPMI Router выйдет из строя, это состояние потеряется, и часть клиентов не будет иметь связности до перезапроса адресов. Мы придумали в этом месте использовать DHCP без состояния: адрес IPv4, который мы раздаём устройству, высчитывается из последних трёх байт его mac-адреса, дописывая 10 в начале. При такой схеме нам не нужно передавать состояние DHCP между серверами, что сильно упрощает их работу.
Давайте посмотрим на примере, как мы получаем адреса.

Для сервера с mac-адресом 00:25:90:9D:2D:89 мы получаем адреса 10.157.45.137 и, соответственно, 2a02:6bf:fff0:7010::0a9d:2d89.
Схема выглядит красиво, но она допускает пересечение адресов. Действительно: если про полные mac-адреса есть условная гарантия отсутствия пересечения, то про последние три байта никто эту гарантию не даёт.
Тут приходит на помощь то, что последние три байта уникальны в рамках одного производителя. А разного вида оборудование мы можем размещать в разных модулях дата-центра — именно в этих пределах у нас развёрнуты сети IPv4. Если пересечения адресов будут между разными модулями — уже не страшно, потому что у BMC снаружи будут видны отличающиеся из-за разных префиксов адреса IPv6.
Безопасность
К этому моменту у нас получилась такая картинка архитектуры сервиса:

В этой схеме есть много мест, где могут быть проблемы с безопасностью. Начнём с BMC — той самой точки, через которую в истории из начала статьи и раскрутили компрометацию всего облака.
Предположим, что опытный атакующий смог захватить управление над BMC целиком, при этом его цель — атаковать соседние серверы. Очевидно, что они находятся в том же сегменте сети, поэтому нам необходимо изолировать BMC так, чтобы они могли общаться с сетью выше через IPMI Router, но не могли общаться между собой.

Начнём с простого — фильтрация на стороне свитчей. Подключим каждый BMC к коммутатору, на котором прописаны ACL, позволяющие прокидывать фреймы только в сторону mac-адресов IPMI Router. Тогда фрейм в сторону другого BMC просто не полетит.
Но что будет, если атакующий, например, поменяет на своём BMC mac-адрес на адрес соседа? В этом случае он сможет отправить фрейм до IPMI Router, захватить чужую запись в forwarding database коммутатора и перехватить чужой трафик. Для предотвращения такой атаки сделаем авторизацию по протоколу 802.1X на коммутаторе по mac-адресу: тогда фреймы с чужого mac-адреса не пролетят в коммутатор. Чтобы протокол работал, важно знать правильное сопоставление mac-адресов BMC и портов.
Казалось бы, это уже всё. Но есть ещё одна возможная атака — замена адреса IPv4 (с верным mac-адресом): коммутатор правильно отработает все L2-заголовки, а дальше мы сможем отправить пакет в сторону IPMI Router и отравить его таблицу ARP. В этом месте мы написали небольшое приложение eBPF, которое сравнивает в каждом влетающем пакете соответствие mac-адреса и адреса IPv4. Если они не совпадают, то такой пакет отбрасывается.
Фуф, с атаками на BMC разобрались. Перейдём к атакам на клиента — а как мы помним из исторической справки, там и код старый, и хеш-суммы небезопасные.

Выглядит страшно, но по сути нам нужно только ограничить сетевой доступ из контейнера так, чтобы до конкретной BMC в одних случаях можно было достучаться, а в других случаях — нет. Это проще всего сделать снаружи контейнера через ip6tables: так атакующий не сможет поменять правила и будет иметь возможность работать только в рамках своего сервера.
Помимо KVM, пользователь может также примонтировать через BMC собственный образ ISO, который лежит на S3. В этом месте вместо того, чтобы дать доступ по сети в S3, мы решили, что будем монтировать образ снаружи от контейнера и прокидывать его как volume: так мы оставляем доступы внутри контейнера минимальными, не жертвуя функциональностью.
Осталось понять, как мы определяем, кого пускать в KVM, а кого — нет. Для этого и используется упомянутая в начале модель Yandex Cloud с доступами и правами, выданными через консоль. Когда пользователь открывает вкладку KVM, консоль делает первоначальную проверку наличия прав. Если права есть, то открывается iframe с novnc и создаётся контейнер в IPMI Proxy. Дальше сервис каждые 10 секунд проверяет, что права на управление сервером всё ещё есть у клиента — так долго висящие открытые соединения не приведут к уязвимости.
Что в итоге
Собрав всё вместе, мы получили довольно простую схему. IPMI Proxy сводит закрытые протоколы разных вендоров к одному VNC, а novnc рисует картинку прямо в консоли Yandex Cloud. Собственный ISO пользователь берёт из S3 и монтирует без сетевого доступа в хранилище: образ подключается снаружи контейнера и прокидывается внутрь как volume.
Так мы унифицировали доступ клиентов в консоль: KVM для управления серверами и монтирования своих образов ISO — в одном интерфейсе, независимо от железа под ним. Схему несложно поддерживать и масштабировать, а вынесенное наружу монтирование заодно закрывает часть векторов атак.
Удалённый доступ к железу — отдельная большая история, которая параллельно развивается как в дата-центрах, так и в потребительском сегменте. В комментариях я с удовольствием обсужу всё, что касается удаленного доступа к железу.

