В предыдущей части мы разобрали, чем являются пространства имен, и какую роль они играют в современных системах, после чего познакомились с двумя их видами: PID и NET. Во второй и заключительной части материала мы изучим пространства имен USER, MNT, UTS, IPC и CGROUP, а в завершении объединим полученные знания, создав полностью изолированную среду для процесса.

USER namespace


Все процессы в мире Linux кому-то принадлежат. Существуют привилегированные и непривилегированные процессы, что определяется их пользовательским ID (UID). В зависимости от этого UID процессы получают разные привилегии в ОС. Пользовательское пространство имен – это функционал ядра, позволяющий выполнять виртуализацию этого атрибута для каждого процесса. В документации Linux это пространство имен определяется так:

«Пользовательские пространства имен изолируют связанные с безопасностью идентификаторы и атрибуты. В частности, ID пользователей, групповые ID, корневой каталог, ключи и возможности. Пользователь процесса и групповые ID внутри и вне user namespace* могут отличаться. Если конкретно, то процесс может иметь обычный непривилегированный UID вне этого пространства имен и в то же время иметь UID=0 внутри него».

*Прим. пер.: С целью облегчить чтение по ходу статьи выражения «пространство имен» и «namespace» будут использоваться попеременно.

По сути, это означает, что процесс имеет полные привилегии для операций внутри его текущего пользовательского пространства имен, но вне него является непривилегированным.

Начиная с Linux 3.8 (и в отличие от флагов, используемых для создания других типов пространств имен), в некоторых дистрибутивах Linux для создания user namespace привилегий не требуется. Попробуем!

cryptonite@cryptonite:~ $uname -a
Linux qb 5.11.0-25-generic #27~20.04.1-Ubuntu SMP Tue Jul 13 17:41:23 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

# корневое user namespace
cryptonite@cryptonite:~ $id
uid=1000(cryptonite) gid=1000(cryptonite) groups=1000(cryptonite) ...
cryptonite@cryptonite:~ $unshare -U /bin/bash
nobody@cryptonite:~$ id
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)

В новом user namespace наш процесс принадлежит пользователю nobody с UID=65334, который в системе отсутствует. Хорошо, но откуда он берется, и как ОС обрабатывает его, когда дело касается общесистемных операций (изменение файлов, взаимодействие с программами)? В документации говорится, что это предопределено в файле:

«Если UID не имеет отображения внутри пространства имен, тогда системные вызовы, возвращающие пользовательские ID, возвращают значение, определенное в файле /proc/sys/kernel/overflowuid. В стандартной системе по умолчанию этим значением является 65534. Изначально, user namespace не имеет отображения UID, поэтому все UID внутри него отображаются в это значение».

На вторую же часть вопроса ответ такой – для ситуаций, когда процессу нужно выполнять общесистемные операции, существует динамическое отображение UID.

#внутри user namespace создаем файл
nobody@cryptonite:~$ touch hello
nobody@cryptonite:~ $ls -l hello
-rw-rw-r-- 1 nobody nogroup 0 Jun 29 17:06 hello

Здесь мы видим, что созданный файл принадлежит пользователю nobody, который по факту в текущей системе не существует. Давайте проверим владение этим файлом с позиции процесса в корневом user namespace.

cryptonite@cryptonite:~ $ls -al hello
-rw-rw-r-- 1 cryptonite cryptonite 0 Jun 29 17:09 hello
# под каким UID выполняется процесс, создавший этот файл?
cryptonite@cryptonite:~ $ps l | grep /bin/bash
F S   UID     PID    PPID  C PRI  NI ADDR SZ WCHAN       TTY         TIME CMD
0  1000   18609    3135  20   0  19648  5268 poll_s S+   pts/0      0:00 /bin/bash

Как видно из фрагмента выше, процесс оболочки в корневом user namespace видит, что процесс внутри user namespace имеет такой же UID. Это также объясняет, почему файл, созданный nobody и видимый как принадлежащий nobody в новом user namespace, на деле принадлежит пользователю с ID=1000.

Как уже говорилось, пользовательские пространства имен могут быть и вложенными – процесс может иметь родительское пространство имен (за исключением процессов в корневом пространстве имен) и нуль или более дочерних пространств имен. Теперь посмотрим, как процесс видит файловую систему, в которой владение содержимым определяется в корневом user namespace.

cryptonite@cryptonite:~ $unshare -U /bin/bash
nobody@cryptonite:/$ ls -al
drwxr-xr-x  20 nobody nogroup  4096 Jun 12 17:25 .
drwxr-xr-x  20 nobody nogroup  4096 Jun 12 17:25 ..
lrwxrwxrwx   1 nobody nogroup     7 Jun 12 17:21 bin -> usr/bin
drwxr-xr-x   5 nobody nogroup  4096 Jun 25 10:23 boot
...

nobody@cryptonite:~$ id
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)
nobody@cryptonite:~$ touch /heloo.txt
touch: cannot touch '/heloo.txt': Permission denied

Странно – содержимое корневого каталога, видимое из нового user namespace, принадлежит пользователю, владеющему процессом внутри этого namespace, но содержимое данного каталога этот пользователь изменять не может? В новом user namespace пользователь (корневой), владеющий этими файлами, не переотображается, а значит не существует. Именно поэтому процесс видит nobody и nogroup.

Но, когда он пробует изменить содержимое каталога, то делает это с помощью своего UID в корневом user namespace, который отличается от UID файлов. Ладно, но как в таком случае процесс может взаимодействовать с файловой системой? Придется использовать отображение.

Отображение UID и GID


Некоторые процессы должны запускаться под действующим UID=0, чтобы предоставить свои службы и иметь возможность взаимодействовать с файловой системой ОС. Одним из типичных случаев при использовании пользовательских пространств имен является определение отображений. Выполняется оно при помощи файлов /proc/‹PID
›/uid_map
и /proc/‹PID›/gid_map в следующем формате:

ID-внутри-ns ID-вне-ns длина

ID-внутри-ns (соответственно ID-вне-ns) определяет начальную точку отображения UID внутри user namespace (соответственно вне этого пространства имен), а длина определяет количество последующих отображений UID (соответственно GID). Эти отображения применяются, когда процесс внутри user namespace пытается манипулировать ресурсами системы, принадлежащими user namespace.

Некоторые важные правила из документации Linux:

«Если два процесса находятся в одном пространстве имен, тогда ID-вне-ns интерпретируется как UID (GID) в родительском user namespace идентификатора (PID) процесса. Здесь типичным случаем является процесс, производящий запись в собственный файл отображения (/proc/self/uid_map или /proc/self/gid_map)».

Если два процесса находятся в разных пространствах имен, то ID-вне-ns интерпретируется как UID (GID) в user namespace процесса, открывающего /proc/PID/uid_map (/proc/PID/gid_map). Тогда записывающий процесс определяет отображение относительно его собственного user namespace.

Хорошо, проверим все это:

# добавление отображения для процесса оболочки с PID=18609 в user namespace
cryptonite@cryptonite:~ $echo "0 1000 65335" | sudo tee /proc/18609/uid_map
0 1000 65335
cryptonite@cryptonite:~ $echo "0 1000 65335" | sudo tee /proc/18609/gid_map
0 1000 65335

# возвращаемся в оболочку в user namespace
nobody@cryptonite:~$ id
uid=0(root) gid=0(root) groups=0(root)
# создаем файл
nobody@cryptonite:~$ touch hello

# возвращаемся в корневое пространство имен
cryptonite@cryptonite:~ $ls -l hello
-rw-rw-r-- 1 cryptonite cryptonite 0 Jun 29 17:50 hello

Процесс внутри пользовательского пространства имен считает, что его действительный UID является корневым, но в вышестоящем (корневом) пространстве имен его UID такой же, как у создавшего его процесса (zsh с UID=1000). Вот иллюстрация для вышеприведенного фрагмента кода:



В более общем виде процесс переотображения показан здесь:



Также важно отметить, что в созданном user namespace процесс имеет действительный UID=0, и все возможности устанавливаются а разрешенном наборе.

Посмотрим, как выглядит файловая система для процесса, находящегося внутри переотображенного user namespace.

# в корневом user namespace
cryptonite@cryptonite:~ $ls -l
total 0
-rw-rw-r-- 1 cryptonite cryptonite 0 Jul  2 11:07 hello
# в дочернем user namespace
nobody@cryptonite:~/test$ ls -l
-rw-rw-r-- 1 root root 0 Jul  2 11:07 hello

Здесь мы видим, что переотображенный пользователь воспринимает владение файлами относительно текущего отображения user namespace. Поскольку файл принадлежит UID=1000, который отображен в UID=0 (корень), этот процесс видит файл как принадлежащий корневому пользователю в текущем пространстве имен.

Инспектирование текущих отображений процесса


Файлы /proc/‹PID
›/uid_map
и /proc/‹PID›/gid_map также можно использовать для инспектирования отображения заданного процесса. Это инспектирование происходит относительно пользовательского пространства имен, в котором процесс находится.
Здесь есть два правила:

  • Если процесс внутри одного namespaceинспектирует отображение другого процесса в том же namespace, то он увидит отображение на родительское user namespace.
  • Если процесс инспектирует отображение процесса из другого namespace, то он увидит отображение относительно его собственного отображения.

Все это немного запутывает, так что посмотрим наглядно:

# в оболочке номер 1
cryptonite@cryptonite:~ $unshare -U /bin/bash
nobody@cryptonite:~$

# в оболочке номер 2 определяем переотображение пользователя
cryptonite@cryptonite:~ $echo "0 1000 65335" | sudo tee  /proc/6638/uid_map
0 1000 65335

# восприятие изнутри переотображенного namespace
nobody@cryptonite:~ $cat /proc/self/uid_map
         0       1000      65335
nobody@cryptonite:~ $id
uid=0(root) gid=65534(nogroup) groups=65534(nogroup)

# теперь в 3-й оболочек создаем новое user namespace
cryptonite@cryptonite:~ $unshare -U /bin/sh
$

# во 2-й оболочке определяем новое отображение оболочки sh
cryptonite@cryptonite:~ $echo "200 1000 65335" | sudo tee /proc/7061/uid_map

# возвращаемся в оболочку sh
# восприятие собственного переотображения
$ cat /proc/self/uid_map
       200       1000      65335
# восприятие переотображения оболочки bash
$ cat /proc/6638/uid_map
         0        200      65335

Хорошо, мы видим, что переотображенный процесс оболочки sh воспринимает переотображение пользователя на основе его собственного user namespace. Результаты можно интерпретировать так: процесс UID=0 в user namespace для процесса 6638 соответствует UID=200 в текущем namespace. Все это относительно. Выдержка из документации:

«Если процесс, открывающий файл, находится в том же user namespace, что и PID этого процесса, тогда ID-вне-ns определяется относительно родительского user namespace. Если процесс, открывающий файл, находится в другом user namespace, тогда ID-вне-ns определяется относительно user namespace этого открывающего файл процесса».

Существуют кое-какие важные правила по определению переотображения внутри файла:

«Определение переотображения – это одноразовая операция для каждого пространства имен: мы можем выполнить только одну операцию записи (которая может содержать несколько разделенных пустыми строками записей) в файл uid_map или ровно один из процессов в user namespace. Более того, сейчас в файл можно записать не более пяти строк».

«Файл /proc/PID/uid_map принадлежит UID, который создал пространство имен, и делать запись в него может только этот пользователь (или привилегированный). При этом должны быть выполнены следующие требования:
  • записывающий процесс должен иметь возможность CAP_SETUID (CAP_SETGID для gid_map) в user namespace своего PID;
  • независимо от возможностей, записывающий процесс должен находиться либо в user namespace PID процесса, либо в ближайшем родительском user namespace идентификатора процесса».

User namespace и возможности


Как я уже говорил, первый процесс в новом user namespace имеет полный набор возможностей внутри этого user namespace.

Выдержка из документации:

«Когда создается user namespace, первый процесс в нем получает полный набор возможностей для данного namespace. Это позволяет ему выполнять любые необходимые инициализации до создания других процессов в этом namespace. Причем несмотря на то, что новый процесс имеет весь набор возможностей в новом user namespace, в родительском namespace он их не имеет совсем. Это верно независимо от полномочий и возможностей процесса, вызывающего clone(). В частности, даже если корень задействует clone(CLONE_NEWUSER), результирующий дочерний процесс не будет иметь возможностей в родительском namespace».

Так что, даже возможности процесса инициализируются и интерпретируются относительно user namespace. Рассмотрим все это на примере, используя небольшую программу, порождающую новый процесс внутри user namespace и показывающую его возможности с позиции родительского user namespace и дочернего namespace.

cryptonite@cryptonite:~ $ ./main
Capabilities of child process viewed from parent namespace =
Capabilities of child process viewed from child namespace =ep
Namespaced process eUID = 65534;  eGID = 65534;

Как видно, процесс внутри нового пространства имен имеет полный набор разрешенных и активных возможностей в user namespace несмотря на то, что программа была запущена от непривилегированного пользователя. Однако в родительском user namespace возможностей у него никаких нет. Причина в том, что каждый раз, когда создается user namespace, первый процесс получает все возможности, чтобы смочь полноценно инициализировать среду пространства имен до создания в нем любых других процессов. Это сыграет важную роль в последнем разделе статьи, где мы займемся совмещением почти всех пространств имен с целью изоляции процесса.

MNT namespace


Пространства имен mount (MNT) позволяют создавать деревья файловых систем под отдельные процессы, тем самым создавая представления корневой файловой системы. Linux поддерживает структуру данных для всех различных файловых систем, смонтированных в системе. Эта структура является индивидуальной для каждого процесса, а также пространства имен. В нее входит информация о том, какие разделы дисков смонтированы, где они смонтированы и тип монтирования (RO/RW).

cryptonite@cryptonite:~ $cat /proc/$$/mounts
sysfs /sys sysfs rw,nosuid,nodev,noexec,relatime 0 0
proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0
...

Пространства имен в Linux дают возможность копировать эту структуру данных и передавать копию разным процессам. Таким образом, эти процессы могут изменять данную структуру (монтировать и размонтировать), не влияя на точки монтирования друг друга. Предоставляя разные копии структуры файловой системы, ядро изолирует список точек монтирования, видимых процессу в пространстве имен.

Определение mount namespace также позволяет процессу изменять свой корень, то есть проявлять поведение, аналогичное системному вызову chroot. Разница здесь в том, что chroot привязан к текущей структуре файловой системы, и все изменения (монтирование, размонтирование) в среде с измененным корнем влияют на всю файловую систему. В случае с пространствами имен такое невозможно, поскольку вся структура виртуализирована, тем самым обеспечивая полную изоляцию исходной файловой системы для монтирования/размонтирования.

# находим ID пространства имен текущего процесса оболочки
cryptonite@cryptonite:~ $readlink /proc/$$/ns/mnt
mnt:[4026531840]

Схема устройства mount namespace:


Схема оформлена Махмудом Ридваном из Toptal

Попробуем!

# создаем новое namespace и входим в него
cryptonite@cryptonite:~ $sudo unshare -m /bin/bash
root@cryptonite:/home/cryptonite# mount -t tmpfs tmpfs /mnt
root@cryptonite:/home/cryptonite# touch  /mnt/hello
# во втором терминале <=> в отдельном mount namespace
cryptonite@cryptonite:~ $ls -al /mnt
total 8
drwxr-xr-x  2 root root 4096 Feb  9 19:47 .
drwxr-xr-x 20 root root 4096 Jun  8 23:24 .
# изменения не отразили другое mount namespace

Здесь мы видим, что процессы в изолированном mount namespace могут создавать под собой различные точки монтирования и файлы, не отражая родительское mount namespace.

Общие поддеревья


Монтирование и размонтирование каталогов отражает файловую систему ОС. В классической ОС Linux эта система представлена в виде дерева. Как видно из схемы выше, создание различных пространств имен технически приводит к созданию виртуальных структур деревьев для каждого процесса. Так вот, эти структуры могут быть общими.

Из документации Linux:

«Ключевое преимущество общих поддеревьев заключается в возможности автоматического, управляемого распространения событий монтирования и размонтирования между пространствами имен. Это означает, к примеру, что монтирование оптического диска в одном mount namespace может запустить монтирование этого диска во всех пространствах имен».


Основной составляющей поддеревьев являются теги для каждой точки монтирования, сообщающие, что произойдет при добавлении/удалении точек монтирования, присутствующих в разных mount namespace. Добавление/удаление точек монтирования запускает событие, которое распространяется в так называемых одноранговых группах. Одноранговая группа – это набор vfsmounts (точек монтирования виртуальной файловой системы), которые распространяют события монтирования и размонтирования между собой.

Точки монтирования бывают четырех видов:

  • MS_SHARED: разделяет события монтирования/размонтирования с другими точками монтирования, входящими в ее «одноранговую группу». Когда под этой точкой монтирования добавляется/удаляется другая точка, это изменение распространяется на всю группу, в результате чего монтирование/размонтирование также происходит под каждой из одноранговых точек. Распространение также происходит и в обратном направлении, в следствии чего события монтирования/размонтирования в однорангововых точках монтирования распространяются на эту точку.
  • MS_PRIVATE: противоположна общей точке монтирования, то есть не распространяет события на одноранговых соседей и не получает аналогичные события от них.
  • MS_SLAVE: находится между общим видом и закрытым. Подчиненная точка монтирования имеет мастера – общую группу пиров, чьи члены распространяют события монтирования/размонтирования на подчиненную точку. Однако такая точка, в свою очередь, на мастер-группу пиров события не распространяет.
  • MS_UNBINDABLE – не получает и не передает никакие события распространения и роль привязки монтирования выполнять не может.

Состояние точки монтирования является индивидуальным для каждой такой точки. То есть, если у вас есть, например, / и boot, то вам нужно отдельно применить нужное состояние для каждой точки монтирования. Предлагаю немного поэкспериментировать.

# создаем точку монтирования и описываем ее как общую для пиров (дочерних пространств # имен)
cryptonite@cryptonite:~ $mkdir X
cryptonite@cryptonite:~ $sudo mount --make-shared -t tmpfs tmpfs X/
# входим в новое mount namespace
cryptonite@cryptonite:~ $sudo unshare --mount /bin/bash
root@cryptonite:/home/cryptonite#
# теперь создадим еще одну точку монтирования в родительском mount namespace
cryptonite@cryptonite:~ $mkdir Y; mount --make-shared -t tmpfs tmpfs Y/
# возвращаемся в оболочку внутри mount namespace
root@cryptonite:/home/cryptonite/X# mount | grep Y
tmpfs в /home/cryptonite/X/Y type tmpfs (rw,relatime)
# так, мы видим, что между этими двумя пространствами имен происходит  
# распространение.
# Точка монтирования была помечена как shared => изменения отражаются во всех членах 
# одноранговой группы.

# возвращаемся в корневое mount namespace
cryptonite@cryptonite:~ $mkdir Z; sudo mount --make-private -t tmpfs tmpfs Z
cryptonite@cryptonite:~ $mount | grep Z
tmpfs on /home/cryptonite/test/Z type tmpfs (rw,relatime)

# возвращаемся в дочернее mount namespace
root@cryptonite:/home/cryptonite/test# mount | grep Z
# ничего --> точка монтирования, помеченная как private, из родительского пространства # имен в дочернем mount namespace отсутствует

Информацию о разных видах монтирования в пространстве имен текущего процесса можно найти в /proc/self/mountinfo.

╭─cryptonite@cryptonite ~
╰─$ cat /proc/self/mountinfo | head -n 3
24 31 0:22 / /sys rw,nosuid,nodev,noexec,relatime shared:7 - sysfs sysfs rw
25 31 0:23 / /proc rw,nosuid,nodev,noexec,relatime shared:15 - proc proc rw
26 31 0:5 / /dev rw,nosuid,noexec,relatime shared:2 - devtmpfs udev rw,size=8031108k,nr_inodes=2007777,mode=755

UTS namespace


Пространство имен UTS изолирует имя хоста системы для определенного процесса.
Большая часть взаимодействия с хостом выполняется через IP-адрес и номер порта. Однако для человеческого восприятия все сильно упрощается, когда у процесса есть хоть какое-то имя. К примеру, выполнять поиск по файлам журналов гораздо проще, когда определено имя хоста. Не в последнюю очередь это связано с тем, что в динамической среде IP могут изменяться.

По аналогии можно сравнить имя хоста с названием многоэтажного жилого здания. Сказать таксисту, что мы живем в City Palace, часто будет эффективнее, чем сообщать фактический адрес. Наличие нескольких имен хостов на одном физическом хосте существенно помогает в обширных контейнеризованных средах.

Итак, пространство имен UTS предоставляет разделение имен хостов среди процессов. Таким образом, становится проще взаимодействовать со службами в закрытой сети и инспектировать их логи на хосте.

cryptonite@cryptonite:~ $sudo unshare -u /bin/bash
root@cryptonite:/home/cryptonite# hostname
cryptonite
root@cryptonite:/home/cryptonite# hostname test
root@cryptonite:/home/cryptonite# hostname
test

# тем временем во втором терминале
cryptonite@cryptonite:~ $hostname
cryptonite

IPC namespace


Пространство имен IPC предоставляет изоляцию для механизмов взаимодействия процессов, таких как семафоры, очереди сообщений, разделяемая память и т.д. Обычно, когда процесс ответвляется, он наследует все IPC, открытые его родителем. Процессы внутри IPC namespace не могут видеть или взаимодействовать с ресурсами IPC вышестоящего пространства имен. Вот краткий пример, где используются разделяемые сегменты памяти:

cryptonite@cryptonite:~ $ipcs -m

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status
0x00000000 32769      cryptonite 600        4194304    2          dest
0x00000000 6          cryptonite 600        524288     2          dest
0x00000000 13         cryptonite 600        393216     2          dest
0x00000000 16         cryptonite 600        524288     2          dest
0x00000000 32785      cryptonite 600        32768      2          dest

cryptonite@cryptonite:~ $sudo unshare -i /bin/bash
root@cryptonite:/home/cryptonite#
------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status

CGROUP namespace


Cgroup (контрольная группа) — это технология, контролирующая потребляемый процессом объем аппаратных ресурсов (RAM, HDD, блок ввода-вывода).

По умолчанию CGroup создаются в виртуальной файловой системе /sys/fs/cgroup. Создание другого CGroup namespace, по сути, перемещает корневой каталог CGroup. Если CGroup размещалось, например, в /sys/fs/cgroup/mycgroup, то новое CGroup namespace могло использовать его в качестве корневого каталога. Хост может видеть /sys/fs/cgroup/mycgroup/{group1,group2,group3}, но создание нового CGroup namespace будет означать, что новое namespace увидит только {group1,group2,group3}.

Итак, пространства имен CGroup виртуализируют другие виртуальные файловые системы в виде PID namespace. Но в чем вообще цель предоставления изоляции для данной системы? Выдержка из мануала:

«Это предотвращает утечку информации при том, что в противном случае пути каталогов cgroup вне контейнера будут видимы для процессов в контейнере. Подобные утечки могут, к примеру, раскрыть контейнеризованным приложениям информацию о структуре контейнера».

В традиционной иерархии CGroup существует возможность того, что вложенное CGroup сможет получить доступ к своему предку. Это означает, что процесс в /sys/fs/cgroup/mycgroup/group1 потенциально может считывать и/или управлять чем-угодно, вложенным в mycgroup.

Объединение изученного


Теперь мы используем пройденный материал, чтобы создать для процесса полностью изолированную среду. Сделаем мы это шаг за шагом, используя обертку unshare. Доказательство концепции этой статьи написано на Си с использованием системного вызова clone() и находится здесь.

# Сначала скачиваем минимальный образ файловой системы,
# который будет находиться в новой корневой файловой системе изолированного процесса
cryptonite@cryptonite:~ $wget http://dl-cdn.alpinelinux.org/alpine/v3.10/releases/x86_64/alpine-minirootfs-3.10.1-x86_64.tar.gz
cryptonite@cryptonite:~ $mkdir rootfs_alpine; tar -xzf alpine-minirootfs-3.10.1-x86_64.tar.gz -C rootfs_alpine
cryptonite@cryptonite:~ $ls rootfs_alpine
bin  etc   hostfs_root  media  opt   root  sbin  sys  usr
dev  home  lib          mnt    proc  run   srv   tmp  var

Хорошо, теперь у нас есть каталог, чье содержимое совпадает с классическим корневым каталогом Linux. Важно отметить порядок обертывания пространства имен, поскольку некоторые операции (например, создание пространств имен PID, UTC, IPC) требуют наличия расширенных привилегий в текущем пространстве имен. Приведенный ниже алгоритм иллюстрирует необходимый порядок действий:

  1. Создать USER namespace
  2. Переотобразить UID процесса в новое USER namespace
  3. Создать UTC namespace
  4. Изменить имя хоста в новом UTC namespace
  5. Создать IPC/CGROUP namespaces
  6. Создать NET namespace
  7. Создать и настроить пару veth
  8. Создать PID namespace
  9. Создать MNT namespace
  10. Изменить корень процесса
  11. Смонтировать /proc в новом корне
  12. Размонтировать старый корень
  13. Поместить процесс в его новый корневой каталог

Поробуем!

# создаем привилегированное user namespace, чтобы иметь возможность 
# создавать пространства имен, требующие права администратора
cryptonite@cryptonite:~ $unshare -U --kill-child  /bin/bash
nobody@cryptonite:~$ id
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)
# пробуем создать новые пространства имен
nobody@cryptonite:~$ unshare -iu /bin/bash
unshare: unshare failed: Operation not permitted
nobody@cryptonite:~$ id
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)

Пользователь в user namespace не может создавать новые пространства имен, поскольку не обладает eUID=0. Чтобы это исправить, нам нужно дать ему рут-права с помощью переотображения. Как уже говорилось, это переотображение должно происходить из процесса в вышестоящем user namespace. Однако из соображений безопасности мы выполним переотображение в непривилегированного пользователя вне этого namespace (eUID=0 -> eUID!=0).

# в другом терминале; получаем PID процесса в пространстве имен.
# Затем переотображаем корневого пользователя внутри этого пространства имен в 
# непривилегированного.
# В вышестоящем пространстве имен:
cryptonite@cryptonite:~ $ps aux | grep /bin/bash
crypton+   26739  0.2  0.0  19516  5040 pts/0    S+   18:04   0:00 /bin/bash
cryptonite@cryptonite:~ $echo "0 1000 65335" | sudo tee /proc/26739/uid_map
0 1000 65335
cryptonite@cryptonite:~ $echo "0 1000 65335" | sudo tee /proc/26739/gid_map
0 1000 65335

# возвращаемся к процессу в user namespace
nobody@cryptonite:~/test$ id
uid=0(root) gid=0(root) groups=0(root),65534(nogroup)

Теперь можно переходить к созданию пространств имен IPC и UTS.

#создаем пространства имен IPC и UTC
nobody@cryptonite:~$ unshare --ipc --uts --kill-child /bin/bash
root@cryptonite:~/test# ipcs

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status

------ Semaphore Arrays --------
key        semid      owner      perms      nsems
root@cryptonite:~# hostname isolated
root@cryptonite:~# hostname
isolated

Далее с помощью виртуальных интерфейсов создадим net namespace.

# возвращаемся в первый терминал,
# чтобы создать пару виртуальных интерфейсов
cryptonite@cryptonite:~ $sudo ip link add veth0 type veth peer name ceth0
# активируем одну из виртуальных сторон и присваиваем ей IP-адрес
cryptonite@cryptonite:~ $sudo ip link set veth0 up
cryptonite@cryptonite:~ $sudo ip addr add 172.12.0.11/24 dev veth0

# создаем net namespace
root@cryptonite:~# unshare --net --kill-child /bin/bash
root@isolated:~# sleep 30

# находим PID изолированного процесса
cryptonite@cryptonite:~ $ps -fC sleep
UID          PID    PPID  C STIME TTY          TIME CMD
crypton+   78625   78467  0 11:44 pts/0    00:00:00 sleep 30
# помещаем другую сторону в net namespace этого процесса
cryptonite@cryptonite:~ $sudo ip link set ceth0 netns /proc/78467/ns/net

# Вернемся к процессу в пространстве имен,
# активируем вторую сторону veth и также присваиваем ей IP-адрес
root@isolated:~# ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
9: ceth0@if10: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 76:8d:bb:61:1b:f5 brd ff:ff:ff:ff:ff:ff link-netnsid 0
root@isolated:~# ip link set lo up
root@isolated:~# ip link set ceth0 up
root@isolated:~# ip addr add 172.12.0.12/24 dev ceth0

# проверяем соединение
root@isolated:~# ping -c 1 172.12.0.11
PING 172.12.0.11 (172.12.0.11) 56(84) bytes of data.
64 bytes from 172.12.0.11: icmp_seq=1 ttl=64 time=0.079 ms
...

Теперь создадим отдельные пространства имен PID и MNT, а затем отделим корневую файловую систему.

// создаем новые пространства имен PID и MNT
root@cryptonite:~/test# unshare --pid --mount --fork --kill-child /bin/sh
#
# mount | grep ext4
/dev/mapper/vgcryponite-lvcryptoniteroot on / type ext4 (rw,relatime,errors=remount-ro)
// у нас есть копия точки монтирования rootfs в mount namespace,
// делаем корневую файловую систему alpine монтируемой
# mount --bind rootfs_alpine rootfs_alpine
# cd rootfs_alpine
# pwd
/home/cryptonite/test/rootfs_alpine
# mkdir hostfs_root
# ls
bin  dev  etc  home  hostfs_root  lib  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

// изменяем точку монтирования текущей корневой файловой системы
# pivot_root . hostfs_root
# ls hostfs_root
bin         dev         lib         libx32      mnt         root        snap        tmp
boot        etc         lib32       lost+found  opt         run         srv         usr
cdrom       home        lib64       media       proc        sbin        sys         var
// на данном этапе у нас есть связывающая точка монтирования файловой системы хоста в новом mount namespace 
# touch hostfs_root/home/cryptonite/test/hello
cryptonite@cryptonite:~ $ls hello -l
-rw-rw-r-- 1 cryptonite cryptonite 0 Jul  6 13:45 hello

# ps
PID   USER     TIME  COMMAND
// так, вывода нет – дело в том, что новую procfs мы ищем в пустом каталоге proc образа alpine. 
// Давайте смонтируем относительную procfs в каталог /proc файловой системы alpine
# mount -t proc proc /proc
# ps
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/sh
   12 root      0:00 ps

Теперь нам нужно полностью изолировать процесс внутри текущего каталога alpine.

// размонтируем старую корневую систему в процессе из mount namespace
# umount -l hostfs_root
# mount | wc -l
2
// у нас три точки монтирования – проверим верхнее mount namespace
cryptonite@cryptonite:~ $mount | wc -l
59
# cd /
# pwd
/
# whoami
root
# ps
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/sh
   20 root      0:00 ps
# ipcs

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status

------ Semaphore Arrays --------
key        semid      owner      perms      nsems

# hostname
isolated
# ls hostfs_root/
// пустой каталог
# rmdir hostfs_root
# ls
bin    etc    lib    mnt    proc   run    srv    tmp    var
dev    home   media  opt    root   sbin   sys    usr

Мы изолировали процесс, используя почти все пространства имен. Иными словами, мы создали очень примитивный контейнер. Однако в нем нет ограничения на использование ресурсов, которое устанавливается с помощью контрольных групп.

Заключение


Пространства имен – это очень мощный принцип ядра Linux, обеспечивающий изоляцию системных ресурсов. Это один из основных принципов, стоящих за созданием контейнеров, таких как известные Docker или LXC. Он обеспечивает ортогональность – то есть, все возможности, предоставляемые пространствами имен, могут использоваться независимо. Тем не менее пространства имен не устанавливают ограничения на использование процессом аппаратных ресурсов, это делается с помощью контрольных групп. Совмещение пространств имен, контрольных групп и возможностей позволяет нам создавать полностью изолированную среду.


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