АрхитектураОблако и инфраструктураСтарший инженер платформы

Контейнерный процесс имеет UID 0 внутри контейнера, но на узле ему сопоставлен непривилегированный UID. Как...

Контейнерный процесс имеет UID 0 внутри контейнера, но на узле ему сопоставлен непривилегированный UID. Какой механизм обеспечивает такое сопоставление?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Это обеспечивает пространство идентификаторов пользователейuser namespace. Оно позволяет процессу видеть себя пользователем с UID 0 внутри контейнера, while на хосте тот же процесс представлен другим, непривилегированным UID.

Так контейнерный root не становится автоматически root на узле. Однако user namespace не заменяет остальные механизмы защиты: доступ дополнительно ограничивают Linux capabilities, seccomp, контроль устройств, мандатные политики и изоляция пространств имён.

Исторический контекст

Традиционная модель контейнеров использовала изоляцию процессов, сети, монтирований и других ресурсов, но UID внутри контейнера часто напрямую соответствовал UID на хосте. Поэтому процесс с UID 0 внутри контейнера мог быть root и с точки зрения ядра узла, хотя его возможности обычно сокращались набором capabilities и другими ограничениями.

User namespace появился как способ разделить понятия идентичности внутри изолированной среды и на хосте. Это стало особенно важно для запуска контейнеров без привилегированного демона и без необходимости выдавать контейнерному процессу права root на узле.

Постановка проблемы

UID 0 — это не просто текстовое имя пользователя. Ядро использует числовую идентичность процесса при проверке владельца файлов, прав доступа и части привилегированных операций.

Если внутренний UID 0 напрямую соответствует UID 0 на хосте, компрометация приложения может дать атакующему значительно более опасные возможности. Даже при наличии capabilities остаются риски ошибок конфигурации, доступа к чувствительным ресурсам узла и использования уязвимостей ядра.

Обратная крайность тоже создаёт проблемы: если все операции выполняются от непривилегированного UID, приложение может не суметь создать файлы, изменить владельца или выполнить действия, ожидаемые от root внутри контейнера. Поэтому требуется отдельное отображение идентификаторов, а не простое удаление UID 0.

Подробное решение

При создании user namespace ядро поддерживает отображение UID и GID между пространством имён контейнера и пространством имён хоста. Например, UID 0 внутри контейнера может соответствовать UID 100000 на узле. Процесс видит UID 0 в своей среде, а операции ядро проверяет с учётом хостовой идентичности.

Это отображение действует не только для самого процесса, но и при работе с объектами файловой системы. Файл, созданный контейнерным root, может отображаться на узле как принадлежащий непривилегированному UID из выделенного диапазона.

Rootless-контейнеры используют этот подход, чтобы запускать контейнерный runtime без root-доступа на хосте. Если процесс внутри контейнера получает UID 0, это обычно означает root только внутри его user namespace.

User namespace не делает любые действия разрешёнными. Привилегии процесса внутри namespace проверяются вместе с другими ограничениями:

  • Capabilities разделяют традиционные права root на отдельные разрешения; отсутствие capability может запретить операцию даже UID 0.
  • Seccomp ограничивает набор системных вызовов.
  • Linux Security Modules, например SELinux или AppArmor, применяют дополнительные политики доступа.
  • Cgroups ограничивают ресурсы, а правила устройств — доступ к устройствам.
  • Другие пространства имён изолируют процессы, сеть, монтирования и IPC, но не превращают user namespace в полноценную виртуальную машину.

Есть и ограничения. Не всякое программное обеспечение корректно работает с отображёнными UID/GID, особенно при доступе к общим томам и сетевым файловым системам. Некоторые операции требуют возможностей, недоступных rootless-контейнеру или доступных только при определённой конфигурации ядра и runtime.

Важно отличать user namespace от запуска контейнера от обычного UID без UID 0. Во втором случае приложение просто не получает root-права внутри контейнера. В первом случае оно может иметь внутреннюю root-идентичность, но эта идентичность изолирована от хоста.

Ситуация из практики

В платформе необходимо запускать сторонние сборочные задания. Изначально контейнеры работали с UID 0, а каталог рабочего пространства монтировался с узла. После компрометации задания потенциально можно было создавать на общем томе файлы, принадлежащие host-root, а ошибка в конфигурации могла усилить воздействие на узел.

Рассматривались три варианта. Полностью запретить UID 0 внутри контейнера было проще всего, но часть инструментов сборки переставала работать. Запускать каждый job на отдельной виртуальной машине давал более сильную границу изоляции, но увеличивал стоимость и время запуска. Включить user namespaces и rootless runtime было сложнее с точки зрения совместимости томов и сетевых функций, зато это сохраняло внутреннюю модель root без отображения его на root узла.

Выбрали rootless-запуск с отображением UID/GID, ограниченными capabilities, seccomp-профилем и отдельными рабочими томами. Для общих каталогов заранее настроили диапазоны идентификаторов и проверили поведение инструментов, меняющих владельца файлов. В результате задания сохранили требуемую функциональность, а контейнерный UID 0 перестал быть UID 0 на хосте; при этом виртуальные машины оставили для недоверенных сценариев с повышенными требованиями к изоляции.

Что кандидаты часто упускают

  1. Достаточно ли user namespace, чтобы считать контейнер полностью изолированным от хоста?

Нет. User namespace изолирует идентификаторы и часть привилегий, но контейнер всё ещё использует общее ядро хоста. Уязвимость ядра, избыточные capabilities, доступ к сокетам управления runtime, опасные монтирования или слабая политика устройств могут привести к выходу за ожидаемую границу. Для оценки изоляции нужно рассматривать весь профиль безопасности, а для наиболее недоверенных нагрузок — дополнительные границы, например виртуальную машину.

  1. Что произойдёт с файлами на смонтированном томе, если UID внутри контейнера отображается на другой UID хоста?

Права проверяются с учётом отображения идентификаторов. Файл, созданный как UID 0 внутри контейнера, на узле может принадлежать, например, UID 100000 и не выглядеть принадлежащим root.

Это повышает безопасность, но может нарушить совместимость: другой процесс на узле или в соседнем контейнере может не иметь доступа к файлу, а существующие файлы с host-UID могут оказаться недоступны контейнерному процессу. Поэтому для постоянных томов заранее проектируют владельцев, группы, диапазоны UID/GID и правила доступа, а не исправляют права вручную после запуска.

  1. Может ли процесс с UID 0 внутри user namespace менять владельца любых файлов на узле?

Нет. Его возможности ограничены областью отображённых идентификаторов и правами на сам объект. В частности, контейнерный root не получает универсальную возможность назначать произвольные host-UID и не становится владельцем файлов хоста только из-за внутреннего UID 0.

Дополнительные ограничения зависят от типа файловой системы, параметров монтирования, capabilities и политики безопасности. Поэтому проверку нужно проводить на реальном runtime и используемых томах: поведение локальной файловой системы, bind mount и сетевого хранилища может различаться.