Тревога разделения: руководство по изоляции вашей системы с помощью пространств имен Linux
Опубликовано: 2022-03-11С появлением таких инструментов, как Docker, Linux Containers и других, стало очень легко изолировать процессы Linux в их собственных небольших системных средах. Это позволяет запускать целый ряд приложений на одной реальной Linux-машине и гарантировать, что никакие два из них не будут мешать друг другу, не прибегая к использованию виртуальных машин. Эти инструменты стали огромным подспорьем для поставщиков PaaS. Но что именно происходит под капотом?
Эти инструменты основаны на ряде функций и компонентов ядра Linux. Некоторые из этих функций были введены относительно недавно, в то время как другие требуют исправления самого ядра. Но один из ключевых компонентов, использующий пространства имен Linux, был особенностью Linux с момента выпуска версии 2.6.24 в 2008 году.
Любой, кто знаком с chroot
, уже имеет общее представление о том, что могут делать пространства имен Linux и как вообще использовать пространство имен. Точно так же, как chroot
позволяет процессам видеть любой произвольный каталог в качестве корня системы (независимо от остальных процессов), пространства имен Linux также позволяют независимо изменять другие аспекты операционной системы. Это включает в себя дерево процессов, сетевые интерфейсы, точки подключения, ресурсы межпроцессного взаимодействия и многое другое.
Зачем использовать пространства имен для изоляции процессов?
На однопользовательском компьютере вполне может быть единая системная среда. Но на сервере, где вы хотите запустить несколько служб, для безопасности и стабильности важно, чтобы службы были максимально изолированы друг от друга. Представьте себе сервер, на котором работает несколько служб, одна из которых скомпрометирована злоумышленником. В таком случае злоумышленник может использовать эту службу и проложить себе путь к другим службам и даже может скомпрометировать весь сервер. Изоляция пространства имен может обеспечить безопасную среду для устранения этого риска.
Например, используя пространство имен, можно безопасно выполнять произвольные или неизвестные программы на вашем сервере. В последнее время растет число конкурсов по программированию и платформ «хакатонов», таких как HackerRank, TopCoder, Codeforces и многих других. Многие из них используют автоматизированные конвейеры для запуска и проверки программ, представленных участниками. Часто невозможно заранее узнать истинный характер программ конкурсантов, а некоторые даже могут содержать вредоносные элементы. Запуская эти программы в пространстве имен в полной изоляции от остальной системы, программное обеспечение можно тестировать и проверять, не подвергая риску остальную часть машины. Точно так же онлайн-сервисы непрерывной интеграции, такие как Drone.io, автоматически загружают ваш репозиторий кода и выполняют тестовые сценарии на своих собственных серверах. Опять же, изоляция пространства имен — это то, что делает возможным безопасное предоставление этих услуг.
Инструменты пространства имен, такие как Docker, также позволяют лучше контролировать использование системных ресурсов процессами, что делает такие инструменты чрезвычайно популярными для использования поставщиками PaaS. Такие сервисы, как Heroku и Google App Engine, используют такие инструменты для изоляции и запуска нескольких приложений веб-сервера на одном и том же реальном оборудовании. Эти инструменты позволяют им запускать каждое приложение (которое может быть развернуто любым из нескольких разных пользователей), не беспокоясь о том, что одно из них использует слишком много системных ресурсов или мешает и/или конфликтует с другими развернутыми службами на той же машине. При такой изоляции процессов можно даже иметь совершенно разные стеки зависимого программного обеспечения (и версий) для каждой изолированной среды!
Если вы использовали такие инструменты, как Docker, вы уже знаете, что эти инструменты способны изолировать процессы в небольших «контейнерах». Запуск процессов в контейнерах Docker аналогичен их запуску в виртуальных машинах, только эти контейнеры значительно легче виртуальных машин. Виртуальная машина обычно эмулирует аппаратный уровень поверх вашей операционной системы, а затем поверх него запускает другую операционную систему. Это позволяет вам запускать процессы внутри виртуальной машины в полной изоляции от вашей реальной операционной системы. Но виртуальные машины тяжелые! Контейнеры Docker, с другой стороны, используют некоторые ключевые функции вашей реальной операционной системы, включая пространства имен, и обеспечивают аналогичный уровень изоляции, но без эмуляции оборудования и запуска еще одной операционной системы на том же компьютере. Это делает их очень легкими.
Пространство имен процесса
Исторически ядро Linux поддерживало единое дерево процессов. Дерево содержит ссылку на каждый процесс, работающий в настоящее время в иерархии родитель-потомок. Процесс, если он имеет достаточные привилегии и удовлетворяет определенным условиям, может инспектировать другой процесс, присоединив к нему трассировщик, или даже может убить его.
С введением пространств имен Linux стало возможным иметь несколько «вложенных» деревьев процессов. Каждое дерево процессов может иметь полностью изолированный набор процессов. Это может гарантировать, что процессы, принадлежащие одному дереву процессов, не могут проверять или уничтожать — фактически даже не могут знать о существовании — процессов в других родственных или родительских деревьях процессов.
Каждый раз, когда компьютер с Linux загружается, он запускается только с одним процессом с идентификатором процесса (PID) 1. Этот процесс является корнем дерева процессов, и он инициирует остальную часть системы, выполняя соответствующие работы по обслуживанию и запуская правильные демоны/сервисы. Все остальные процессы начинаются ниже этого процесса в дереве. Пространство имен PID позволяет создать новое дерево с собственным процессом PID 1. Процесс, который это делает, остается в родительском пространстве имен, в исходном дереве, но делает дочерний процесс корнем своего собственного дерева процессов.
Благодаря изоляции пространства имен PID процессы в дочернем пространстве имен не могут узнать о существовании родительского процесса. Однако процессы в родительском пространстве имен имеют полное представление о процессах в дочернем пространстве имен, как если бы они были любым другим процессом в родительском пространстве имен.
Можно создать вложенный набор дочерних пространств имен: один процесс запускает дочерний процесс в новом пространстве имен PID, и этот дочерний процесс порождает еще один процесс в новом пространстве имен PID и так далее.
С введением пространств имен PID один процесс теперь может иметь несколько связанных с ним PID, по одному для каждого пространства имен, в которое он попадает. В исходном коде Linux мы видим, что структура с именем pid
, которая раньше отслеживала только один PID, теперь отслеживает несколько PID с помощью структуры с именем upid
:
struct upid { int nr; // the PID value struct pid_namespace *ns; // namespace where this PID is relevant // ... }; struct pid { // ... int level; // number of upids struct upid numbers[0]; // array of upids };
Чтобы создать новое пространство имен PID, необходимо вызвать системный вызов clone()
со специальным флагом CLONE_NEWPID
. (С предоставляет оболочку для раскрытия этого системного вызова, как и многие другие популярные языки.) В то время как другие пространства имен, обсуждаемые ниже, также могут быть созданы с помощью системного вызова unshare()
, пространство имен PID может быть создано только во время нового процесс порождается с помощью clone()
. Как только clone()
вызывается с этим флагом, новый процесс немедленно запускается в новом пространстве имен PID в новом дереве процессов. Это можно продемонстрировать с помощью простой программы на C:
#define _GNU_SOURCE #include <sched.h> #include <stdio.h> #include <stdlib.h> #include <sys/wait.h> #include <unistd.h> static char child_stack[1048576]; static int child_fn() { printf("PID: %ld\n", (long)getpid()); return 0; } int main() { pid_t child_pid = clone(child_fn, child_stack+1048576, CLONE_NEWPID | SIGCHLD, NULL); printf("clone() = %ld\n", (long)child_pid); waitpid(child_pid, NULL, 0); return 0; }
Скомпилируйте и запустите эту программу с привилегиями суперпользователя, и вы увидите вывод, похожий на этот:
clone() = 5304 PID: 1
PID, напечатанный из child_fn
, будет равен 1
.
Несмотря на то, что приведенный выше код руководства по пространству имен ненамного длиннее «Hello, world» на некоторых языках, многое произошло за кулисами. Функция clone()
, как и следовало ожидать, создала новый процесс путем клонирования текущего и начала выполнение в начале функции child_fn()
. Однако при этом он отсоединил новый процесс от исходного дерева процессов и создал отдельное дерево процессов для нового процесса.
Попробуйте заменить static int child_fn()
следующей, чтобы напечатать родительский PID с точки зрения изолированного процесса:
static int child_fn() { printf("Parent PID: %ld\n", (long)getppid()); return 0; }
Запуск программы на этот раз дает следующий результат:
clone() = 11449 Parent PID: 0
Обратите внимание, что родительский PID с точки зрения изолированного процесса равен 0, что указывает на отсутствие родителя. Попробуйте снова запустить ту же программу, но на этот раз удалите флаг CLONE_NEWPID
из вызова функции clone()
:
pid_t child_pid = clone(child_fn, child_stack+1048576, SIGCHLD, NULL);
На этот раз вы заметите, что родительский PID больше не равен 0:
clone() = 11561 Parent PID: 11560
Тем не менее, это только первый шаг в нашем уроке. Эти процессы по-прежнему имеют неограниченный доступ к другим общим или разделяемым ресурсам. Например, сетевой интерфейс: если созданный выше дочерний процесс будет прослушивать порт 80, это помешает любому другому процессу в системе прослушивать его.
Сетевое пространство имен Linux
Именно здесь становится полезным сетевое пространство имен. Сетевое пространство имен позволяет каждому из этих процессов видеть совершенно другой набор сетевых интерфейсов. Даже петлевой интерфейс отличается для каждого сетевого пространства имен.
Изоляция процесса в его собственном сетевом пространстве имен включает введение другого флага в вызов функции clone()
: CLONE_NEWNET
;
#define _GNU_SOURCE #include <sched.h> #include <stdio.h> #include <stdlib.h> #include <sys/wait.h> #include <unistd.h> static char child_stack[1048576]; static int child_fn() { printf("New `net` Namespace:\n"); system("ip link"); printf("\n\n"); return 0; } int main() { printf("Original `net` Namespace:\n"); system("ip link"); printf("\n\n"); pid_t child_pid = clone(child_fn, child_stack+1048576, CLONE_NEWPID | CLONE_NEWNET | SIGCHLD, NULL); waitpid(child_pid, NULL, 0); return 0; }
Выход:

Original `net` Namespace: 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: enp4s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000 link/ether 00:24:8c:a1:ac:e7 brd ff:ff:ff:ff:ff:ff New `net` Namespace: 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
Что тут происходит? Физическое устройство Ethernet enp4s0
принадлежит пространству имен глобальной сети, на что указывает инструмент «ip», запускаемый из этого пространства имен. Однако физический интерфейс недоступен в новом сетевом пространстве имен. Более того, петлевое устройство активно в исходном сетевом пространстве имен, но «не работает» в дочернем сетевом пространстве имен.
Чтобы обеспечить удобный сетевой интерфейс в дочернем пространстве имен, необходимо настроить дополнительные «виртуальные» сетевые интерфейсы, охватывающие несколько пространств имен. После этого можно создавать мосты Ethernet и даже маршрутизировать пакеты между пространствами имен. Наконец, чтобы все это работало, в глобальном сетевом пространстве имен должен быть запущен «процесс маршрутизации», чтобы получать трафик от физического интерфейса и направлять его через соответствующие виртуальные интерфейсы в правильные дочерние сетевые пространства имен. Может быть, вы понимаете, почему такие инструменты, как Docker, которые делают всю эту тяжелую работу за вас, так популярны!
Чтобы сделать это вручную, вы можете создать пару виртуальных соединений Ethernet между родительским и дочерним пространствами имен, выполнив одну команду из родительского пространства имен:
ip link add name veth0 type veth peer name veth1 netns <pid>
Здесь <pid>
следует заменить идентификатором процесса в дочернем пространстве имен, наблюдаемым родителем. Выполнение этой команды устанавливает каналообразное соединение между этими двумя пространствами имен. Родительское пространство имен сохраняет устройство veth0
и передает устройство veth1
дочернему пространству имен. Все, что входит в один из концов, выходит через другой конец, как и следовало ожидать от реального Ethernet-соединения между двумя реальными узлами. Соответственно, обеим сторонам этого виртуального соединения Ethernet должны быть назначены IP-адреса.
Пространство имен монтирования
Linux также поддерживает структуру данных для всех точек монтирования системы. Он включает в себя информацию о том, какие разделы диска смонтированы, где они смонтированы, доступны ли они только для чтения и так далее. С пространствами имен Linux можно клонировать эту структуру данных, чтобы процессы в разных пространствах имен могли изменять точки монтирования, не влияя друг на друга.
Создание отдельного пространства имен монтирования имеет эффект, аналогичный выполнению chroot()
. chroot()
хорош, но он не обеспечивает полной изоляции, и его эффекты ограничены только корневой точкой монтирования. Создание отдельного пространства имен монтирования позволяет каждому из этих изолированных процессов иметь совершенно иное представление о структуре точек монтирования всей системы по сравнению с исходным. Это позволяет вам иметь отдельный корень для каждого изолированного процесса, а также другие точки монтирования, специфичные для этих процессов. При осторожном использовании в соответствии с этим руководством вы можете избежать раскрытия какой-либо информации о базовой системе.
Для этого требуется флаг clone()
CLONE_NEWNS
:
clone(child_fn, child_stack+1048576, CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS | SIGCHLD, NULL)
Первоначально дочерний процесс видит те же самые точки монтирования, что и его родительский процесс. Однако, находясь в новом пространстве имен монтирования, дочерний процесс может монтировать или размонтировать любые конечные точки, которые он хочет, и это изменение не повлияет ни на пространство имен его родителя, ни на какое-либо другое пространство имен монтирования во всей системе. Например, если родительский процесс имеет определенный раздел диска, смонтированный в корне, изолированный процесс увидит тот же самый раздел диска, смонтированный в корне в начале. Но преимущество изоляции пространства имен монтирования становится очевидным, когда изолированный процесс пытается изменить корневой раздел на что-то другое, поскольку изменение повлияет только на изолированное пространство имен монтирования.
Интересно, что на самом деле это делает плохой идеей создание целевого дочернего процесса напрямую с флагом CLONE_NEWNS
. Лучший подход — запустить специальный процесс «init» с флагом CLONE_NEWNS
, заставить этот процесс «init» изменить «/», «/ proc», «/ dev» или другие точки монтирования по желанию, а затем запустить целевой процесс. . Это обсуждается более подробно в конце этого учебника по пространству имен.
Другие пространства имен
Существуют и другие пространства имен, в которые эти процессы могут быть изолированы, а именно пользовательское, IPC и UTS. Пространство имен пользователя позволяет процессу иметь привилегии root в пространстве имен, не предоставляя ему такой доступ к процессам за пределами пространства имен. Изоляция процесса пространством имен IPC дает ему собственные ресурсы межпроцессного взаимодействия, например сообщения System V IPC и POSIX. Пространство имен UTS изолирует два конкретных идентификатора системы: nodename
и имя domainname
.
Быстрый пример, показывающий, как пространство имен UTS изолировано, показан ниже:
#define _GNU_SOURCE #include <sched.h> #include <stdio.h> #include <stdlib.h> #include <sys/utsname.h> #include <sys/wait.h> #include <unistd.h> static char child_stack[1048576]; static void print_nodename() { struct utsname utsname; uname(&utsname); printf("%s\n", utsname.nodename); } static int child_fn() { printf("New UTS namespace nodename: "); print_nodename(); printf("Changing nodename inside new UTS namespace\n"); sethostname("GLaDOS", 6); printf("New UTS namespace nodename: "); print_nodename(); return 0; } int main() { printf("Original UTS namespace nodename: "); print_nodename(); pid_t child_pid = clone(child_fn, child_stack+1048576, CLONE_NEWUTS | SIGCHLD, NULL); sleep(1); printf("Original UTS namespace nodename: "); print_nodename(); waitpid(child_pid, NULL, 0); return 0; }
Эта программа дает следующий результат:
Original UTS namespace nodename: XT New UTS namespace nodename: XT Changing nodename inside new UTS namespace New UTS namespace nodename: GLaDOS Original UTS namespace nodename: XT
Здесь child_fn()
печатает имя nodename
, изменяет его на что-то другое и снова печатает. Естественно, изменение происходит только внутри нового пространства имен UTS.
Дополнительную информацию о том, что предоставляют и изолируют все пространства имен, можно найти в руководстве здесь.
Взаимодействие между пространствами имен
Часто необходимо установить какую-то связь между родительским и дочерним пространством имен. Это может быть сделано для выполнения работы по настройке в изолированной среде, или это может быть просто сохранение возможности заглянуть в состояние этой среды извне. Один из способов сделать это — запустить демон SSH в этой среде. У вас может быть отдельный демон SSH внутри каждого сетевого пространства имен. Однако наличие нескольких запущенных демонов SSH требует много ценных ресурсов, таких как память. Именно здесь использование специального процесса «init» снова оказывается хорошей идеей.
Процесс «init» может установить канал связи между родительским пространством имен и дочерним пространством имен. Этот канал может быть основан на сокетах UNIX или даже может использовать TCP. Чтобы создать сокет UNIX, который охватывает два разных пространства имен монтирования, вам нужно сначала создать дочерний процесс, затем создать сокет UNIX, а затем изолировать дочерний процесс в отдельном пространстве имен монтирования. Но как мы можем сначала создать процесс, а потом изолировать его? Linux предоставляет unshare()
. Этот специальный системный вызов позволяет процессу изолировать себя от исходного пространства имен вместо того, чтобы родитель изолировал дочерний процесс в первую очередь. Например, следующий код имеет тот же самый эффект, что и код, ранее упомянутый в разделе сетевого пространства имен:
#define _GNU_SOURCE #include <sched.h> #include <stdio.h> #include <stdlib.h> #include <sys/wait.h> #include <unistd.h> static char child_stack[1048576]; static int child_fn() { // calling unshare() from inside the init process lets you create a new namespace after a new process has been spawned unshare(CLONE_NEWNET); printf("New `net` Namespace:\n"); system("ip link"); printf("\n\n"); return 0; } int main() { printf("Original `net` Namespace:\n"); system("ip link"); printf("\n\n"); pid_t child_pid = clone(child_fn, child_stack+1048576, CLONE_NEWPID | SIGCHLD, NULL); waitpid(child_pid, NULL, 0); return 0; }
И поскольку процесс «init» — это то, что вы разработали, вы можете сначала заставить его выполнить всю необходимую работу, а затем изолировать себя от остальной системы перед выполнением целевого дочернего процесса.
Заключение
Это руководство представляет собой просто обзор того, как использовать пространства имен в Linux. Это должно дать вам общее представление о том, как разработчик Linux может начать реализовывать изоляцию системы, неотъемлемую часть архитектуры таких инструментов, как Docker или Linux Containers. В большинстве случаев было бы лучше просто использовать один из этих существующих инструментов, которые уже хорошо известны и проверены. Но в некоторых случаях может иметь смысл иметь свой собственный, настраиваемый механизм изоляции процессов, и в этом случае это руководство по пространству имен очень поможет вам.
Под капотом происходит гораздо больше, чем я описал в этой статье, и есть больше способов ограничить целевые процессы для дополнительной безопасности и изоляции. Но, надеюсь, это может послужить полезной отправной точкой для тех, кто хочет узнать больше о том, как на самом деле работает изоляция пространства имен в Linux.