Уважаемые коллеги, добрый день!

Меня зовут Дмитрий, я инженер 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, скоро она должна появиться

Зависимости

Из-за того, что данная фича довольно новая, есть очень много зависимостей, из-за которых она может не заработать.

  1. Поддержка idmap
    На ноде файловая система для /var/lib/kubelet/pods/ (или другая кастомная директория указанная в kubelet) должна поддерживать idmap. Для этого ядро Linux должна быть минимум версии 6.3, т.к. tmpfs начала поддерживать монтирование idmap именно в этой версии. И конечно же операционной системой должна быть Linux, на Windows такое не заработает

  2. Поддержка User Namespaces в CRI (container runtime interface):

    • containerd - доступна поддержка с версии 2.0 (или позже)

    • cri-o - доступна поддержка с версии 1.25 (или позже)

  3. Поддержка OCI runtime:

    • runc - поддержка с 1.2 или позже

    • crun - поддержка с 1.9 или позже (рекомендуется с 1.13+)

  4. Если версия Kubernetes ниже 1.33, нужно включить User Namespaces через feature-gates.
    Для api-server добавить флаг при запуске:

    - --feature-gates=UserNamespacesSupport=true

    Для конфигурации kubelet добавить featureGate:

    featureGates:
      UserNamespacesSupport: true
  5. Также стоит проверить что на системе есть поддержка 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'а на одной ноде не используют одно и тоже сопоставление.

Распределение UID's для контейнеров
Распределение UID's для контейнеров

Поля 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

Столбцы в файле:

  1. Стартовый UID для диапазона внутри текущего User Namespace

  2. Реальный UID на хостовой системе

  3. Длина диапазона пользовательских 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 в телеграмме

Комментарии (0)