Уважаемые коллеги, добрый день!
Меня зовут Дмитрий, я инженер Kubernetes. Одно из самых интересных последних фич в безопасности, которое я очень долго ждал, это реализация User Namespaces в Kubernetes. И сегодня я постараюсь рассказать про эту фичу.
Если очень плохое понимание что такое Namespaces в Linux, или его вообще нет, то советую сначала прочитать про Namespaces, и только потом переходить на эту статью. Например можно почитать эту серию постов
Введение
User namespaces в Linux позволяет изолировать пользователя в контейнере от пользователя на хосте. Когда запускается процесс в контейнере от root (или любого другого пользователя) при включенном User Namespaces, на хосте сам процесс будет отображаться совершенно от другого пользователя. При этом пользователь внутри контейнера будет иметь всё такие же права (например использовать apt/dnf), но не будет иметь привилегий за его пределами
Используя данную фичу, можно уменьшить ущерб при скомпрометированном контейнере, который может нанести хосту или другим pod'ам на этом хосте. Она покрывает довольно много критических уязвимостей, которые просто нельзя эксплуатировать при включённом User Namespaces.
Немного предыстории
Первый issue был описан в 2016 году, но первые движения в сторону User Namespaces появились только в 2021 году. Первая реализация появилась в Kubernetes версии 1.25 и называлась сначала UserNamespacesStatelessPodsSupport, и данное название сохранилось до 1.27 версии включительно, после уже сократилось название до UserNamespacesSupport.
В версии 1.30 уже появилась beta1 версия, и в 1.33 версии появилась beta2 включённая по стандарту. Дальше ждём эту версию в GA, скоро она должна появиться
Зависимости
Из-за того, что данная фича довольно новая, есть очень много зависимостей, из-за которых она может не заработать.
Поддержка idmap
На ноде файловая система для/var/lib/kubelet/pods/(или другая кастомная директория указанная в kubelet) должна поддерживать idmap. Для этого ядро Linux должна быть минимум версии 6.3, т.к. tmpfs начала поддерживать монтирование idmap именно в этой версии. И конечно же операционной системой должна быть Linux, на Windows такое не заработает-
Поддержка User Namespaces в CRI (container runtime interface):
containerd - доступна поддержка с версии 2.0 (или позже)
cri-o - доступна поддержка с версии 1.25 (или позже)
-
Поддержка OCI runtime:
-
Если версия Kubernetes ниже 1.33, нужно включить User Namespaces через feature-gates.
Для api-server добавить флаг при запуске:- --feature-gates=UserNamespacesSupport=trueДля конфигурации kubelet добавить featureGate:
featureGates: UserNamespacesSupport: true -
Также стоит проверить что на системе есть поддержка User Namespaces и есть клонирование непривилегированных namespace:
cat /boot/config-$(uname -r) | grep CONFIG_USER_NS sysctl kernel.unprivileged_userns_cloneВывод:
CONFIG_USER_NS=y kernel.unprivileged_userns_clone = 1
Немного теории
В базовой теории Linux можно встретить что максимальный UID и GID может быть 65535 (2^16 - 1), но это было так до 2.4 Linux.
0 пользователь является root и с 1 до 999 пользователя являются системными и лучше их идентификаторы не занимать, обычные пользователи создаются с 1000 и до 2^16 - 1, а также есть специальный пользователь nobody под UID 65534. Это всё правда, но до версии Linux 2.4.
Раньше все UID были 16-битными, но после версии 2.4 тип данных uid_t и gid_t был изменён на 32-битное целое число и реальное количество пользователей которое сейчас доступно это 4294967295 (2^32 - 1). Но из-за большого количества стороннего ПО и старого легаси, до сих пор не особо используется новый диапазон после 65535
Например, команда useradd без указания определённого идентификатора будет создавать пользователя только с 1000 до 60000 (в большинстве стандартных систем). Эти данные она берёт из файла /etc/login.defs (если что эти данные можно спокойно менять)
Команда для поиска данных по UID:
cat /etc/login.defs | grep -i uid
Вывод:
UID_MIN 1000
UID_MAX 60000
Но всё также можно создавать пользователя с UID выше 2^16 с ручным указанием:
useradd -u 1000000 my_user
Но при создании будем получать warning:
useradd warning: my_user's uid 1000000 outside of the UID_MIN 1000 and UID_MAX 60000 range.
В принципе это будет работать, но для каких-то старых файловых систем или ПО, может оказаться проблематичным
Если что, всё что сказано про UID, также относится к GID. Если хочется чуть больше погрузиться в UID и GID можно прочитать данную статью
Когда запускается pod с включённым User Namespaces, kubelet будет выбирать такие UID и GID чтобы гарантировать что никакие 2 pod'а на одной ноде не используют одно и тоже сопоставление.

Поля RunAsUser, runAsGroup, fsGroup и т.д., которые используются в pod.spec всегда относятся к пользователю внутри контейнера. Эти пользователи будут использоваться для монтирования томов (указанных в pod.spec.volumes), и поэтому UID/GID хоста не будет иметь никакого влияния на запись/чтение с томов, которые может монтировать pod. Другими словами, inodes созданные/прочитанные в томах, смонтированных в pod, будут такими же, как если бы pod не использовал User Namespaces.
Поэтому, pod может легко включать и отключать User Namespaces (не влияя на права файлов своего тома), а также может совместно использовать тома с pod'ами без User Namespaces, просто указав соответствующих пользователей внутри контейнера (RunAsUser, RunAsGroup, fsGroup и т.д.). Это относится к любому тому, который может монтировать pod, включая путь к хосту (если pod'у разрешено монтировать тома с путями к хосту).
По стандарту выдаётся на каждый контейнер 65536 UID и GID, и если требуется настроить другое количество UID для каждого pod, то требуется поменять конфигурацию kubelet:
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
userNamespaces:
idsPerPod: 1048576
...
Главное при конфигурации idsPerPod нужно чтобы оно было кратно 65536 и должна быть меньше 2^32.
Указав слишком большое значение это может повлиять на количество pod'ов, например:
Если у вас стоит значение по стандарту 65536, то можно создать 65536 pod'ов на одной ноде -> 65536 * 65536 = 4'294'967'295 (2^32)
Но уже при значении 1048576 можно будет создать только 4096 pod'ов
При работе с User Namespaces также есть некоторые ограничения, например:
Если pod'у поставить capabilities
CAP_SYS_MODULEто возможности загрузки ядра будут невозможныТоже самое с
CAP_SYS_ADMIN, оно не будет действительно за пределами контейнера
Для большой информации можно обратиться к manТакже при использовании User Namespaces запрещается ставить hostNetwork, hostIPC, hostPID параметры в true
Применение
Начиная с версии 1.33 данная фича включена автоматически, поэтому изменять ничего не требуется
Для того чтобы включить User Namespaces у pod'а нужно поле pod.spec.hostUsers перевести в значение false (по стандарту идёт значение true)
Пример:
apiVersion: v1
kind: Pod
metadata:
name: userns
spec:
hostUsers: false
containers:
- name: shell
command: ["sleep", "inf"]
image: ubuntu
После этого можно взять с полки печеньку, получить премию за внедрение безопасности ~и закрыть статью~
После можно зайти в pod и посмотреть что у него внутри
kubectl exec -it userns -- bash
Прожав ps aux в контейнере, можно увидеть, что пользователь в контейнере root
USER PID COMMAND
root 1 sleep inf
Но запустив ps aux | grep sleep | grep -v grep на хосте, можно увидеть что пользователь 3223650+
USER PID COMMAND
3223650+ 123456 sleep inf
Для тестирования поднимем такой же контейнер, но с выключенным User Namespaces:
apiVersion: v1
kind: Pod
metadata:
name: nouserns
spec:
hostUsers: true
containers:
- name: shell
command: ["sleep", "inf"]
image: ubuntu
И выполнив те же манипуляции выше, получаем на хосте запущенный процесс от root:
USER PID COMMAND
root 123456 sleep inf
Также есть специальный файл для UID маппинга. Посмотреть можно командой:
cat /proc/self/uid_map
Столбцы в файле:
Стартовый UID для диапазона внутри текущего User Namespace
Реальный UID на хостовой системе
Длина диапазона пользовательских ID, которая была выдана на контейнер
Если что это также действует на файл /proc/self/gid_map для GID.
Запустив команду на userns контейнере, получаем данный вывод:
0 3223650304 65536
В контейнере где нет User Namespaces:
0 0 4294967295
И на хосте:
0 0 4294967295
Для обычного контейнера и для хоста разницы ноль, что при запуске каких-то привилегированных контейнеров может дать преимущество для дальнейшего побега с него. Но при использовании User Namespaces пользователь который отображается на хосте не будет иметь никаких прав, что гораздо усложняет возможность покинуть контейнер.
Заключение
User namespaces в Kubernetes — это критически важный механизм безопасности, который изолирует пользовательские идентификаторы контейнера от хостовой системы.
Он позволяет процессу быть root внутри контейнера, но на ноде отображение будет совершенно другого пользователя, что позволяет покрыть довольно много различных уязвимостей при побеге из контейнера.
Таким образом, даже если злоумышленник скомпрометирует контейнер, он не получит права root на всей ноде, что повышает безопасность кластера.
Также, время от времени пишу бесполезные статьи про Kubernetes в телеграмме