Перевод сделан автором сайта goxpert.ru
Краткое содежание перевода
- Пользовательские пространства имен предоставляют возможность изолировать процессы от других процессов и пространств имен.
- Linux предоставляет сопоставление идентификаторов через файловую систему /proc/pid/uid_map и /proc/pid/gid_map.
- Файлы карт содержат различное содержимое в зависимости от процесса чтения.
- Создание пользовательского пространства имен требует доступа суперпользователя, но они также могут владеть другими пространствами имен.
- Пространства имен владельцев важны для определения привилегий процессов при выполнении привилегированных действий над ресурсами, инкапсулированными в пространствах имен.
- Идентификаторы сопоставляются между пространствами имен, и процесс может выполнять операции, требующие проверки прав доступа, путем обхода сопоставлений в дереве пространства имен.
- Управление пользовательскими пространствами имен сопряжено с множеством сложностей, но реализация довольно проста.
Глубокое погружение в пространства имен Linux, часть 2
В предыдущем посте мы окунулись в воды пространства имен и в процессе увидели, насколько просто запустить процесс с изолированным UTS пространством имен. В этом посте мы проливаем свет на User пространство имен.
Пространства имен пользователей(User namespaces) изолируют, среди других ресурсов, связанных с безопасностью, идентификаторы пользователей и групп в системе. В этом посте мы сосредоточимся исключительно на ресурсах user и group ID (UID и GID соответственно), поскольку они играют фундаментальную роль в проведении проверок разрешений и других действий, связанных с безопасностью, во всей системе.
В Linux эти идентификаторы (IDs) представляют собой просто целые числа, которые идентифицируют пользователей и группы в системе, и каждому процессу присваивается несколько из них, чтобы определить, к каким операциям / ресурсам этот процесс может получить доступ, а к каким нет - способность процесса наносить ущерб зависит от разрешений, связанных с присвоенными ему идентификаторами
Пользовательские пространства имен (User Namespaces)
- Мы проиллюстрируем возможности пользовательских пространств имен, используя только идентификаторы пользователей(user IDs). Точно такие же взаимодействия применимы к идентификаторам групп (group IDs), которые будут рассмотрены позже в этом посте.
Пространство имен пользователя имеет свою собственную копию идентификаторов пользователя и группы. Затем изоляция позволяет процессу ассоциироваться с другим набором идентификаторов в зависимости от пространства имен пользователя, к которому он принадлежит в любой данный момент. Например, процесс $pid может выполняться как root (UID 0) в пользовательском пространстве имен P и внезапно продолжить выполнение как proxy (UID 13) после переключения в другое пользовательское пространство имен Q.
Пользовательские пространства имен могут быть вложенными! Это означает, что экземпляр пользовательского пространства имен (родительский) может иметь ноль или более дочерних пространств имен, а каждое дочернее пространство имен, в свою очередь, может иметь свои собственные дочерние пространства имен и так далее … (до ограничения в 32 вложенных уровня). При создании нового пользовательского пространства имен C Linux устанавливает текущее пользовательское пространство имен P процесса, который создает C, в качестве C родительского, и впоследствии это не может быть изменено. В результате все пользовательские пространства имен имеют ровно одного родителя, образуя древовидную структуру пространств имен. И, как и в случае с деревьями, исключение находится вверху, где у нас есть корневое (или начальное, или стандартное) пространство имен - если вы уже не творите какую-то контейнерную магию, это, скорее всего, пользовательское пространство имен, к которому принадлежат все ваши процессы, поскольку при запуске системы это однопользовательское пространство имен.
- В этом посте мы будем использовать командные строки P$ и C$ для обозначения оболочки, которая в данный момент запущена в родительском P и дочернем C пользовательских пространствах имен соответственно.
Сопоставления идентификаторов пользователей (User ID Mappings)
Пространство имен пользователя по сути содержит набор идентификаторов(IDs) и некоторую информацию, связывающую эти идентификаторы с набором идентификаторов других пользовательских пространств имен - этот дуэт определяет полное представление процесса об идентификаторах, доступных в системе. Давайте посмотрим, как это может выглядеть:
1 | P$ whoami |
В другом окне терминала давайте запустим оболочку с помощью unshare (флаг -U создает процесс в новом пользовательском пространстве имен - user namespace):
1 | P$ whoami |
Подождите, кто? Теперь, когда мы находимся во вложенной оболочке в C текущий пользователь становится nobody? Мы могли бы догадаться, что, поскольку C это новое пространство имен пользователя, процесс может иметь другое представление идентификаторов, поэтому мы, возможно, не ожидали, что оно останется iffy, но nobody это неинтересно 😒. С другой стороны, это здорово, потому что мы получили изоляцию, о которой просили. Наш процесс теперь имеет другое (хотя и неполное) представление идентификаторов в системе - в настоящее время он видит всех как nobody и каждую группу как nogroup.
Информация, связывающая UID из одного пространства имен в другое, называется сопоставлением идентификаторов пользователей(User ID Mappings). Он представляет таблицы поиска от идентификаторов в текущем пространстве имен пользователя до идентификаторов в других пространствах имен пользователей, и каждое пространство имен пользователя связано ровно с одним отображением UID (в дополнение к одному отображению GID для идентификаторов групп).
Это отображение нарушено в нашей unshare оболочке. Оказывается, что новые пользовательские пространства имен начинаются с пустого отображения, и в результате в Linux по умолчанию используется ужасный nobody пользователь. Нам нужно исправить это, прежде чем мы сможем выполнять какую-либо полезную работу внутри нашего нового пространства имен. Например, в настоящее время системные вызовы (например, setuid), которые пытаются работать с UID, завершаются неудачей. Но не бойтесь! следуя традиции “все-как-файл”, Linux предоставляет это отображение через /proc файловую систему по адресу /proc/$pid/uid_map (/proc/$pid/gid_map для GID), где $pid - идентификатор процесса. Мы будем называть эти два файла файлами карт.
Файлы карт (Map files)
Файлы Map(Map files) - это особые файлы в системе. насколько особые? ну, такие, которые возвращают разное содержимое всякий раз, когда вы читаете из них, в зависимости от того, из какого процесса вы читаете. Например, файл map /proc/$pid/uid_map возвращает сопоставление из UIDS в пользовательском пространстве имен, к которому $pid принадлежит процесс, с UIDS в пользовательском пространстве имен процесса чтения, и в результате содержимое, возвращаемое процессу, X может отличаться от того, что возвращается процессу, Y даже если они одновременно читают один и тот же файл map.
В частности, процесс, X который считывает файл карты UID, /proc/$pid/uid_map получает набор строк. Каждая строка отображает непрерывный диапазон UIDs в пользовательское пространство C процесса $pid, соответствующего диапазону UID в другом пространстве имен.
Каждая строка имеет формат $fromID $toID $length, где:
- $fromID является начальным UID диапазона для пользовательского пространства имен process $pid
- $length это длина диапазона.
- Перевод $toID зависит от процесса чтения X. Если X принадлежит другому пользовательскому пространству имен U, то $toID это начальный UID диапазона, в U которому $fromID соответствует. В противном случае, $toID это начальный UID диапазона в P, родительском пользовательском пространстве имен C.
Например, если процесс считывает файл /proc/1409/uid_map и среди полученных строк есть 15 22 5, то UIDS с 15 по 19 в пользовательском пространстве имен процесса 1409 сопоставляются с UIDS 22-26 в отдельном пользовательском пространстве имен процесса чтения.
С другой стороны, если процесс считывает из файла /proc/$$/uid_map (или файла сопоставления для любого процесса, который принадлежит тому же пользовательскому пространству имен, что и он) и получает 15 22 5, то UIDS с 15 по 19 в его пользовательском пространстве имен C сопоставляются с UIDS с 22 по 26 в C родительском пользовательском пространстве имен.
Давайте попробуем это:
1 | P$ echo $$ |
Ладно, это было не очень интересно, поскольку это были два крайних случая, но это говорит нам о нескольких вещах:
- Вновь созданное пространство имен пользователя фактически будет содержать пустые файлы map.
- UID 4294967295 не сопоставлен и непригоден для использования даже в root пространстве имен. Linux обрабатывает этот UID специально, чтобы показать, что идентификатора пользователя нет.
Написание файлов UID Map (Writing UID Map files)
Чтобы исправить наше недавно созданное пространство имен пользователя C, нам просто нужно предоставить желаемые сопоставления, записав их в файл map для любого процесса, который принадлежит C (мы не можем обновить этот файл после записи в него). Запись в этот файл говорит Linux о двух вещах:
- Какие UID доступны процессам , принадлежащим этому целевому пользовательскому пространству имен C.
- Какие UID в текущем пользовательском пространстве имен соответствуют UID в C.
Например, если мы из родительского пользовательского пространства имен P запишем следующее в файл карты пользователя для дочернего пространства имен C:
1 | 0 1000 1 |
по сути, мы говорим Linux, что:
- Что касается процессов в C, то единственными UIDS, которые существуют в системе, являются UIDS 0 и 3 - например, системный вызов setuid(9) всегда завершается ошибкой с чем-то вроде недопустимого идентификатора пользователя.
- UIDS 1000 и 0 in P соответствуют UIDS 0 и 3 in C - например, если процесс, запущенный как UID 1000 в P, переключается на C, он увидит, что его UID стал root 0 после переключения.
Пространства имен владельцев и привилегии (Owner Namespaces And Privileges)
В предыдущем посте мы упоминали, что при создании новых пространств имен требовался доступ суперпользователя. В пользовательских пространствах имен этого требования нет. На самом деле, они также уникальны тем, что могут владеть другими пространствами имен.
Всякий раз, когда создается непользовательское пространство имен N, Linux определяет текущее пользовательское пространство имен P процесса, создающего N, владельцем пространства имен N. Если P создается вместе с другими пространствами имен в том же clone системном вызове, Linux гарантирует, что P будет создано первым и назначено владельцем других пространств имен.
Пространства имен владельцев важны, потому что процесс, запрашивающий выполнение привилегированного действия над ресурсом, инкапсулированным непользовательским пространством имен, будет проверять свои привилегии UID на соответствие пользовательскому пространству имен владельца, а не корневому пользовательскому пространству имен. Например, скажем, P является родительским пользовательским пространством имен дочерних C, а P и C владеют собственными сетевыми пространствами имен M и N соответственно, то процесс может не иметь привилегий на создание сетевых устройств, инкапсулированных в M, но может быть в состоянии сделать это для N.
Значение пространств имен владельцев для нас заключается в том, что мы можем отказаться от sudo требования при выполнении команд с помощью unshare or isolate , если мы также запрашиваем создание пользовательского пространства имен - например, unshare -u bash потребует sudo, но unshare -Uu bash - не будет тебовать sudo:
1 | # UID 1000 is a non-privileged user in the root user namespace P. |
- К сожалению, мы повторно применим требование к суперпользователю в следующем посте, поскольку isolate требуются права root в корневом пространстве имен для правильной настройки пространств имен Mount и Network. Но мы обязательно удалим привилегии перед выполнением командного процесса, чтобы убедиться, что у команды нет ненужных разрешений.
Как разрешаются идентификаторы (How IDs are resolved)
Мы только что видели, как процесс, запущенный от имени обычного пользователя, 1000 внезапно переключился на root 😮. Не волнуйтесь, никакого повышения привилегий не было. Помните, что это всего лишь сопоставление идентификаторов - хотя наш процесс думает, что он есть root в системе, Linux знает, что root в его случае означает обычный UID 1000 (благодаря нашему сопоставлению), поэтому, хотя пространства имен, принадлежащие его новому пользовательскому пространству имен (например, сетевому пространству имен в C), признают его права в качестве root, другие (например, сетевое пространство имен в P) не делают этого, поэтому процесс не может сделать ничего, чего не смог бы пользователь 1000.
Всякий раз, когда процесс во вложенном пространстве имен пользователя выполняет операцию, требующую проверки разрешений, например, создает файл, его UID в этом пространстве имен сравнивается с эквивалентным идентификатором пользователя в root пространстве имен пользователя путем обхода сопоставлений в дереве пространств имен вплоть до корневого. Выполняется обратное направление, когда он, например, считывает идентификаторы пользователей, как мы делали бы с ls -l my_file - UID владельца my_file сопоставляется из root пользовательского пространства имен в текущее пространство имен, и окончательный сопоставленный идентификатор (или nobody если сопоставление отсутствовало где-то в дереве) предоставляется процессу чтения.
Идентификаторы групп (Group IDs)
Несмотря на то, что мы попали в C как root, мы по-прежнему ассоциируемся с ужасной nogroup в качестве идентификатора(ID) нашей группы. Нам просто нужно сделать то же самое для соответствующего /proc/$pid/gid_map. Прежде чем мы сможем это сделать, нам нужно отключить setgroups системный вызов (в этом нет необходимости, если у вашего пользователя уже есть CAP_SETGID возможность в P, но мы не будем предполагать этого, поскольку это обычно сопровождается правами суперпользователя), написав “deny” в proc/$pid/setgroups файл:
1 | # Where 13294 is the pid for the unshared process |
Реализация (Implementation)
Исходный код этого поста можно найти здесь.
Как вы можете видеть, управление пользовательскими пространствами имен сопряжено с множеством сложностей, но реализация довольно проста. Все, что нам нужно сделать, это записать кучу строк в файл - основная задача заключалась в том, чтобы знать, что и куда записывать. Без лишних слов, вот наши цели, которых нам нужно достичь:
- Клонируйте командный процесс в его собственном пользовательском пространстве имен.
- Запись в файлы UID и GID map командного процесса.
- Отбросьте все права суперпользователя перед выполнением команды.
1 достигается простым добавлением CLONE_NEWUSER флага к нашему clone системному вызову.
1 | int clone_flags = SIGCHLD | CLONE_NEWUTS | CLONE_NEWUSER; |
Для 2 мы добавляем функцию prepare_user_ns, которая консервативно предоставляет доступ к одному обычному пользователю 1000 как root.
1 | static void prepare_userns(int pid) |
И вызовите его из основного процесса в родительском пользовательском пространстве имен, прямо перед тем, как мы подадим сигнал командному процессу.
1 |
|
Для шага 3 мы обновляем cmd_exec функцию, чтобы убедиться, что команда выполняется от имени обычного непривилегированного пользователя, 1000 который мы указали в сопоставлении (помните, что пользователь root 0 в пространстве имен user командного процесса - user 1000):
1 | ... |
И все! isolate теперь запускает процесс в изолированном пользовательском пространстве имен.
1 | $ ./isolate sh |
В этом посте было довольно много подробностей о том, как User работают пространства имен, но, в конце концов, настройка экземпляра прошла относительно безболезненно. В следующем посте мы рассмотрим возможность isolate запуска команды в ее собственном Mount пространстве имен (раскрывая тайну, стоящую за Dockerfile FROM инструкцией). Здесь нам потребуется немного больше помощи Linux, чтобы правильно настроить экземпляр.