Перевод сделан автором сайта goxpert.ru
Краткое содежание перевода
- Изоляция процессов в Linux может быть достигнута с помощью пространств имен.
- Пространства имен позволяют изолировать процессы от других процессов и файловых систем.
- Изоляция может быть достигнута путем создания нового пространства имен для монтирования и замены корневой файловой системы.
- Проект Alpine Linux предоставляет корневые файловые системы, которые могут быть использованы для изоляции команд.
- Поворотный корень позволяет монтировать системные файлы в новом пространстве имен для монтирования, не нарушая изоляцию.
- Реализация изоляции процессов включает создание командного процесса в новом пространстве имен, настройку пространства имен для монтирования и поворот корня.
- Пространства имен PID изолируют идентификаторы процессов в системе, обеспечивая большую изоляцию для запущенной команды.
Глубокое погружение в пространства имен Linux, часть 3
Пространства имен монтирования(Mount namespaces) изолируют ресурсы файловой системы. Это в значительной степени охватывает все, что связано с файлами в системе. Среди инкапсулированных ресурсов есть файл, содержащий список точек монтирования, которые видны процессу, и, как мы намекали в вступительном посте, изоляция может обеспечить такое поведение, что изменение списка (или любого другого файла) в некотором экземпляре пространства имен монтирования M(mount namespace) не влияло на этот список в другом экземпляре (так что только процессы в M наблюдают изменения).
Точки монтирования (Mount Points)
Вам может быть интересно, почему мы просто увеличили кажущийся случайным файл, содержащий список - что в нем такого особенного? Список точек монтирования определяет полное представление процесса о доступных файловых системах в системе, и поскольку мы находимся в стране Linux с мантрой “все - файл”, видимость практически каждого ресурса определяется этим представлением - от реальных файлов и устройств до информации о том, какие другие процессы также запущены в системе. Таким образом, для isolate это огромная победа в области безопасности - иметь возможность точно указывать, о каких частях системы будут в курсе команды, которые мы запускаем. Пространства имен монтирования в сочетании с точками монтирования являются очень мощным инструментом, который позволяет нам достичь этого.
Мы можем видеть точки монтирования, видимые процессу с идентификатором $pid через /proc/$pid/mounts файл - его содержимое одинаково для всех процессов, принадлежащих к тому же пространству имен монтирования , что и $pid:
1 | $ cat /proc/$$/mounts |
Где-то в списке, возвращаемом в моей системе, указано /dev/sda1 устройство, установленное в / (у вас может отличаться). Это дисковое устройство, на котором размещена корневая файловая система, которая содержит все необходимое для запуска системы, поэтому было бы здорово, если бы isolate выполняли команды, не зная о подобных файловых системах.
Давайте начнем с запуска терминала в его собственном пространстве имен mount:
- Строго говоря, нам не нужен доступ суперпользователя для работы с новыми пространствами имен mount, если мы включаем процедуры настройки пользовательского пространства имен, описанные в предыдущем посте. В результате в этом посте мы будем только предполагать, что unshare команды в терминале выполняются от имени суперпользователя. isolate в этом предположении нет необходимости.
1 | # The -m flag creates a new mount namespace. |
Хммм, мы по-прежнему видим тот же список, что и в корневом(root) пространстве имен mount. Особенно после того, как в предыдущем посте было показано, что новое пространство имен пользователя начинается с чистого листа, может показаться, что -m флаг, который мы передали unshare, не возымел никакого эффекта.
Процесс оболочки фактически выполняется в другом пространстве имен mount (мы можем убедиться в этом, сравнив файл с символической ссылкой ls -l /proc/$$/ns/mnt с файлом другой оболочки, запущенным в корневом пространстве имен mount). Причина, по которой мы по-прежнему видим один и тот же список, заключается в том, что всякий раз, когда мы создаем новое пространство имен монтирования (дочернее), в качестве дочернего списка используется копия точек монтирования пространства имен монтирования, где происходило создание (родительское). Теперь любые изменения, которые мы вносим в этот файл (например, монтируя файловую систему), будут невидимы для всех других процессов.
Однако изменение практически любого другого файла на данном этапе повлияет на другие процессы, потому что мы по-прежнему ссылаемся на те же самые файлы (Linux создает копии только специальных файлов, таких как список точек монтирования). Это означает, что в настоящее время у нас минимальная изоляция. Если мы хотим ограничить то, что будет видеть наш командный процесс, мы должны обновить этот список самостоятельно.
Теперь, с одной стороны, поскольку мы пытаемся соблюдать безопасность, мы могли бы просто сказать “К черту” и isolate очистить весь список перед выполнением команды, но это сделает команду бесполезной, поскольку каждая программа, по крайней мере, зависит от ресурсов, таких как файлы операционной системы, которые, в свою очередь, поддерживаются некоторой файловой системой. С другой стороны, мы также могли бы просто выполнить команду как есть, совместно используя с ней те же файловые системы, которые содержат необходимые системные файлы, которые ей требуются, но это, очевидно, противоречит цели этой изоляции, которую мы выполняем.
Самое приятное - предоставить программе ее собственную копию зависимостей и системных файлов, которые ей требуются для запуска, все изолированные, чтобы она могла вносить в них любые изменения, не затрагивая другие программы в системе. В лучшем случае мы заключили бы эти файлы в файловую систему и смонтировали бы ее как корневую файловую систему (в корневом каталоге /) перед выполнением ничего не подозревающей программы. Идея в том, что все, к чему может достичь процесс, должно проходить через корневую файловую систему, и поскольку мы будем точно знать, какие файлы мы туда помещаем для командного процесса, мы будем спокойны, зная, что он должным образом изолирован от остальной системы.
Хорошо, теоретически это звучит неплохо, и для того, чтобы реализовать это, мы сделаем следующее:
- Создайте копию зависимостей и системных файлов, необходимых команде.
- Создайте новое пространство имен mount.
- Замените корневую файловую систему в новом пространстве имен mount на ту, которая состоит из нашей копии системных файлов.
- Выполните программу внутри нового пространства имен mount.
Корневые файловые системы (Root Filesystems)
Вопрос, который возникает уже на шаге, 1 заключается в том, какие системные файлы вообще необходимы команде, которую мы хотим запустить? Мы могли бы порыться в нашей собственной корневой файловой системе и задавать этот вопрос для каждого файла, с которым сталкиваемся, и включать только те, где ответ да, но это звучит болезненно и ненужно. Кроме того, мы даже не знаем, с какой команды isolate будет выполняться для начала.
Если бы только люди сталкивались с такой же проблемой и собирали набор системных файлов, достаточно общих, чтобы прямо из коробки служить базой для большинства существующих программ? К счастью, есть много проектов, которые делают это! Одним из которых является Alpine Linux project (это его основная функция при запуске FROM alpine:xxx в вашем Dockerfile). Alpine предоставляет корневые файловые системы, которые мы можем использовать для наших целей. Если вы следите за ходом событий, вы можете получить копию их минимальной корневой файловой системы (MINI ROOT FILESYSTEM) для x86_64 здесь. Последняя версия на момент написания статьи , которую мы будем использовать в этом посте , - это v3.10.1.
1 | $ wget http://dl-cdn.alpinelinux.org/alpine/v3.10/releases/x86_64/alpine-minirootfs-3.10.1-x86_64.tar.gz |
В rootfs каталоге есть знакомые файлы, такие же, как в нашей корневой файловой системе по адресу /, но проверьте, насколько он минимален - довольно многие из этих каталогов пусты:
1 | $ ls rootfs/{mnt,dev,proc,home,sys} |
Это здорово! мы можем дать команду запустить копию этого, и это возможно sudo rm -rf / нам все равно, больше никого это не побеспокоит.
Pivot root (поворачиваем root)
Учитывая наше новое пространство имен mount и копию системных файлов, мы хотели бы смонтировать эти файлы в корневом каталоге нового пространства имен mount, не выбивая почву у нас из-под ног. В Linux мы познакомились с pivot_root системным вызовом (есть соответствующая команда), который позволяет нам управлять тем, что процессы рассматривают как корневую файловую систему.
Команда pivot_root принимает два аргумента new_root put_old, где new_root это путь к файловой системе, содержащей будущую корневую файловую систему, и put_old это путь к каталогу. Это работает с помощью:
- Монтирование корневой файловой системы вызывающего процесса на put_old.
- Монтируем файловую систему , на которую указывает new_root как на текущую корневую файловую систему в /.
Давайте посмотрим на это в действии. В нашем новом пространстве имен mount мы начинаем с создания файловой системы на основе наших файлов alpine:
1 | $ unshare -m bash |
Далее мы поворачиваем root:
1 | $ cd rootfs |
Наконец, мы размонтируем старую файловую систему из put_old, чтобы вложенная оболочка не могла получить к ней доступ.
1 | $ umount -l put_old |
Благодаря этому мы можем запустить любую команду в нашей оболочке, и они будут выполняться с использованием нашей пользовательской файловой системы alpine root, не подозревая об оркестровке, которая привела к их выполнению. И наши драгоценные файлы в старой файловой системе находятся в безопасности вне пределов их досягаемости.
Реализация (Implementation)
Исходный код этого поста можно найти здесь.
Мы можем повторить то, что только что сделали, в коде, заменив pivot_root команду на соответствующий системный вызов. Сначала мы создаем наш командный процесс в новом пространстве имен mount , добавляя CLONE_NEWNS флаг в clone.
1 | int clone_flags = SIGCHLD | CLONE_NEWUTS | CLONE_NEWUSER | CLONE_NEWNS; |
Далее мы создаем функцию, prepare_mntns которая, учитывая путь к каталогу, содержащему системные файлы (rootfs), устанавливает текущее пространство имен mount , поворачивая корень текущего процесса в rootfs, как мы делали ранее.
1 | static void prepare_mntns(char *rootfs) |
Нам нужно вызвать эту функцию из нашего кода, и это должно быть сделано нашим командным процессом в cmd_exec (поскольку он выполняется в новом пространстве имен mount), прежде чем начнется выполнение фактической команды.
1 | ... |
Давайте попробуем:
1 | $ ./isolate sh |
Этот вывод показывает нечто странное - мы не можем проверить список подключений(монтирования), за который мы так упорно боролись, и ps сообщает нам, что в системе не запущено никаких процессов (даже текущего процесса или ps самого себя?). Более вероятно, что мы что-то сломали при настройке пространства имен mount.
Пространства имен PID (PID Namespaces)
Мы уже упоминали /proc каталог несколько раз в этой серии, и если вы были знакомы с ним, то, вероятно, не удивлены, что ps он оказался пустым, поскольку ранее мы видели, что каталог был пуст в этом пространстве имен mount (когда мы получили его из корневой файловой системы alpine).
/proc Каталог в Linux обычно используется для предоставления доступа к специальной файловой системе (называемой файловой системой proc), которая управляется самим Linux. Linux использует его для предоставления информации обо всех процессах, запущенных в системе, а также другой системной информации, касающейся устройств, прерываний и т.д. Всякий раз, когда мы запускаем команду типа ps, которая обращается к информации о процессах в системе, она обращается к этой файловой системе для получения информации.
Другими словами, нам нужно раскрутить proc файловую систему. К счастью, это в основном включает в себя сообщение Linux о том, что нам нужна файловая система, желательно смонтированная в /proc. Но мы пока не можем этого сделать, поскольку наш командный процесс по-прежнему зависит от той же proc файловой системы, что и isolate и любой другой обычный процесс в системе - чтобы устранить эту зависимость, нам нужно запустить его внутри его собственного PID пространства имен.
Пространство имен PID изолирует идентификаторы процессов в системе. Одним из результатов является то, что процессы, запущенные в разных пространствах имен PID, могут иметь один и тот же идентификатор процесса, не конфликтуя друг с другом. Конечно, мы изолируем это пространство имен, потому что хотим максимально изолировать нашу запущенную команду, но более интересная причина, по которой мы показываем это здесь, заключается в том, что для монтирования proc файловой системы требуются привилегии root, а текущее пространство имен PID принадлежит пространству имен пользователя root, где у нас нет достаточных разрешений (если вы помните из предыдущего поста, root для командного процесса на самом деле нет root). Итак, мы должны работать в пространстве имен PID, принадлежащем пользовательскому пространству имен, которое распознает наш командный процесс как root.
Мы можем создать новое пространство имен PID , передав CLONE_NEWPID в clone:
1 | int clone_flags = SIGCHLD | CLONE_NEWUTS | CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWPID; |
Далее мы добавим функцию prepare_procfs, которая настраивает файловую систему proc путем монтирования ее в текущем пространстве имен mount и pid.
1 | static void prepare_procfs() |
Наконец, мы вызываем функцию непосредственно перед размонтированием put_old в нашей prepare_mntns функции, после того как мы настроили пространство имен mount и изменили его на корневой каталог.
1 | static void prepare_mntns(char *rootfs) |
Мы можем попробовать isolate еще раз:
1 | $ ./isolate sh |
Это выглядит намного лучше! Оболочка рассматривает себя как единственный процесс, запущенный в системе и работающий с PID 1 (поскольку это был первый процесс, запущенный в этом новом пространстве имен PID).
В этом посте были рассмотрены два пространства имен и, как результат, isolate появились две новые функции. В следующем посте мы рассмотрим изоляцию с помощью Network пространств имен. Там нам придется иметь дело с некоторой сложной низкоуровневой сетевой конфигурацией в попытке обеспечить сетевое взаимодействие между процессами в разных сетевых пространствах имен.