Kubernetes состоит из множества компонентов, и иногда их комбинирование определенным образом приводит к неожиданным результатам. В этой статье я покажу, как pod, запущенный с привилегиями root'а и примонтированной директорией
/var/log
узла, может раскрыть содержимое всей файловой системы хоста пользователю с доступом к его логам. Мы также обсудим варианты решения этой проблемы.Как Kubernetes видит логи
Задумывались ли вы над тем, как
kubectl logs <pod_name>
извлекает логи из pod'а? Кто отвечает за сбор логов из контейнеров? И как они попадают на ваш компьютер?Следующая схема иллюстрирует процесс:
Kubelet создает структуру внутри директории
/var/log
на хосте, представляющую pod'ы на узле. В директории для нашего pod'а есть файл 0.log
(1), но на самом деле это симлинк на лог контейнера, лежащий в /var/lib/docker/containers
. Это все с точки зрения хоста.Kubelet открывает endpoint
/logs/
(2), который просто работает с файловым HTTP-сервером в директории (3), делая логи доступными для запросов, поступающих от API-сервера.Теперь представьте, что мы развернули pod с
hostPath
, примонтированным в /var/log
. У такого pod'а будет доступ ко всем лог-файлам на хосте. Хотя уже это само по себе является потенциальной проблемой, мы можем сделать следующий логический шаг. Что, если заменить 0.log
на симлинк к… скажем, /etc/shadow
?¦
+-- var
¦ +-- logs
¦ ¦ +-- pods
¦ ¦ ¦ +-- default_mypod_e7869b14-abca-11e8-9888-42010a8e020e
¦ ¦ ¦ ¦ +-- mypod
¦ ¦ ¦ ¦ ¦ +-- 0.log -> /etc/shadow
¦ ¦ ¦ ¦ ¦ ¦
Теперь, пытаясь загрузить логи с помощью
kubectl logs
на клиентской машине, получим:$ kubectl logs mypod
failed to get parse function: unsupported log format: "root:*:18033:0:99999:7:::\n"
Kubelet переходит по ссылке и читает содержимое файла, на который она указывает (им может быть любой файл на узле).
Поскольку ожидался JSON, kubectl вылетел после первой строки, однако мы можем легко прочитать конкретные строки файла
shadow
, запуская команду с флагом –-tail=-<line_number>
.Это поразительно. Поскольку kubelet переходит по симлинку, можно воспользоваться его root-правами для чтения любого файла на узле, просто создавая символическую ссылку внутри pod'а.
Побег из pod'а
Пойдем еще дальше. Мы знаем, что при запуске pod'а в Kubernetes в него устанавливается токен ServiceAccount. Таким образом, если service account разрешает доступ к логам, мы можем напрямую получить доступ к kubelet'у и root-привилегии на узле.
Я написал proof of concept (POC), демонстрирующий данный вектор атаки:
- развертывание pod'а с точкой монтирования
/var/log
; - создание символической ссылки на корневую директорию хоста;
- чтение закрытого ключа ssh пользователя на хосте.
В следующем видео показаны две особые команды, выполняющиеся внутри pod'а:
-
lsh == ls
(на файловой системе хоста); -
cath == cat
(на файловой системе хоста).
Прим. перев.: К сожалению, на хабре так и не починили вставку контента с asciinema, хотя мы уже обращались по данной проблеме, поэтому вынуждены «вставлять» видео простой ссылкой выше.
Все файлы, задействованные в этом POC, можно найти в соответствующем репозитории GitHub. Там же лежит еще один POC-скрипт, который автоматически собирает закрытые ключи и токены ServiceAccount с файловой системы хоста.
Монтирование директорий может быть опасным
Итак, это уязвимость или просто плохая практика?
Развертывание pod'а с открытым для записи
hostPath
в /var/log
встречается редко (кроме того, есть другие способы злоупотребить монтированием секретных директорий хоста в pod). Но даже если вы знали, что монтирование /var/log
— сомнительная практика, вы скорее всего не ожидали, что она позволит с такой легкостью завладеть узлом.Перед публикацией мы обратились в команду Kubernetes по безопасности, чтобы узнать, считают ли они это уязвимостью. Они пришли к выводу, что это всего лишь печальное последствие монтирования закрытой директории хоста с правами записи: риски, связанные с этим, хорошо задокументированы. Впрочем, этой уязвимостью довольно легко воспользоваться. В мире есть множество проектов, которые пользуются данным монтированием. Если вы используете один из этих проектов, помните, что ваш deployment будет уязвим перед таким способом захвата хоста.
Этот метод был протестирован на Kubernetes 1.15 и 1.13, но скорее всего затрагивает и другие версии.
Устранение
Такой «побег» возможен только в том случае, если pod работает под root'ом. Вообще этого следует избегать. Aqua CSP позволяет с минимумом усилий задать политику, предотвращающую работу контейнеров под root'ом или выдающую разрешения только определенной группе образов, для которой действительно требуется root.
Другой способ — просто не развертывать pod'ы с
hostPath
с правами записи в /var/log
. Этот подход не задается по умолчанию и не является обычной практикой, поэтому необходимо сознательно его определять (впрочем, возможность по-прежнему остается). Но как проверить?Мы добавили новый скрипт (hunter) в kube-hunter — наш легковесный Open Source-инструмент для тестирования Kubernetes, — проверяющий кластер на предмет существования pod'ов с такими опасными точками монтирования. (Прим. перев.: Kube-hunter присутствовал в недавнем обзоре утилит для безопасности K8s, что мы публиковали в своем блоге.)
Пользователи Aqua могут защититься от данного риска, используя runtime-политику для запрета монтирования определенных томов:
Прим. перев.: Частично такую проблему можно решить с помощью Pod Security Policies, а именно —
AllowedHostPaths
. Однако это тоже не защита от симлинков. Наконец, как нам подсказывают в комментариях, можно просто ограничить запуск под root'ом, снова руководствуясь PSP.Итог
Kubernetes — сложная система с массой тонкостей в настройке безопасности, которые не всегда очевидны для рядового и даже опытного пользователя. В этой статье я показал, как при определенных обстоятельствах невинное ведение логов может привести к потенциальной уязвимости. В большинстве случаев это невозможно, однако Kubernetes предлагает пользователям большую свободу действий, которые могут отразиться на безопасности. Важно помнить об этом и внедрять соответствующие средства контроля, позволяющие предотвратить подобные ошибки.
P.S. от переводчика
Читайте также в нашем блоге:
- «В 19% популярнейших Docker-образов нет пароля для root»;
- «33+ инструмента для безопасности Kubernetes»;
- «Введение в сетевые политики Kubernetes для специалистов по безопасности»;
- «Docker и Kubernetes в требовательных к безопасности окружениях»;
- «9 лучших практик по обеспечению безопасности в Kubernetes»;
- «11 способов (не) стать жертвой взлома в Kubernetes».
Комментарии (12)
gecube
12.09.2019 12:52Проблема только лишь в том, что не нужно разрешать HostPath.
Выпиливаем HostPath — и уязвимость исчезает.
Действительно же — контейнеру нефиг что-либо делать на файловой системе хоста.
А вот как при этом организовать локальный сторидж верно — вопрос, который остаётся за кадром.
VolCh
12.09.2019 13:21Сейчас нет возможности проверить: а классический chroot имеет подобную багофичу?
SlavniyTeo
12.09.2019 14:00+2Здесь дело не в chroot, а в том что мы одну и ту же символьную ссылку маунтим два раза: внутри контейнера мы переписываем ссылку на
/etc/shadow
, а читаем её уже с помощьюkubectl logs
. Выхода за пределы chroot jail не происходит.
Прочитать
/etc/shadow
хоста изнутри пода не получится.
Все дело в том, что не нужно позволять кому-попало маунтить что-угодно с хостов.
Нормальный подход в данной ситуации:
- с помощью PodSecurityPolicy запрещаем запуск подов под рутом;
- с помощью PodSecurityPolicy запрещаем все volume кроме ConfigMap, Secret, PersistentVolumeClaim + какие вам необходимы, но не HostPath/Local;
- с помощью RBAC запрещаем пользователю создавать PersistentVolume, по требованию создаем для него.
Если уж заговорили про безопасность, то добавить к перечисленному еще и ResourceQuota и NetworkPolicy.
celebrate
13.09.2019 11:21Если я правильно понял, само по себе монтирование /var/log через hostPath даже с правом записи не несет в себе описанной уязвимости (так работает, например, стандартный fluentd). Нужно запустить под именно привилегированным, а это достаточно редко нужно.
SlavniyTeo
13.09.2019 13:35+1Если Вы имеете в виду
priveleged
, то этого не нужно. Нужно, чтобы пользователь внутри контейнера был root. А это происходит достаточно часто.
evg_krsk
И ни слова о PSP. Странно.
Wimbo Автор
Да, вы правы.
Частично такую проблему можно решить с помощью PSP: kubernetes.io/docs/concepts/policy/pod-security-policy/#volumes-and-file-systems, а именно AllowedHostPaths. Однако это тоже не защита от симлинков. Мы добавим данный комментарий в статью.
Спасибо.
evg_krsk
Вообще, я имел ввиду защиту от запуска под рутом :-)
gecube
Опеншифт опять со своим секурити впереди планеты всей.
Теоретически какой-нибудь селинукс и мапирование пользователей внутри контейнера помогло бы.
Обидно, что мейнстрим это куб поверх убунты, а не шифт