6. Смена корня

Практически все администраторы и разработчики рано или поздно встречаются с chroot-окружениями. Системный вызов chroot() позволяет задать для определенного процесса (и его потомков) каталог, который они будут рассматривать как корневой /, тем самым ограничивая для них область видимости иерархии файловой системы отдельной ветвью. Большинство применений chroot-окружений можно отнести к двум классам задач:

1. Обеспечение безопасности. Потенциально уязвимый демон chroot’ится в отдельный каталог и, даже в случае успешной атаки, взломщик увидит лишь содержимое этого каталога, а не всю файловую систему — он окажется в ловушке chroot’а.

2. Подготовка и управление образом операционной системы при отладке, тестировании, компиляции, установке или восстановлении. При этом вся иерархия файловых систем гостевой ОС монтируется или создается в каталоге системы-хоста, и при запуске оболочки (или любого другого приложения) внутри этой иерархии, в качестве корня используется данный каталог. Система, которую «видят» такие программы, может сильно отличаться от ОС хоста. Например, это может быть другой дистрибутив, или даже другая аппаратная архитектура (запуск i386-гостя на x86_64-хосте). Гостевая ОС не может увидеть полного дерева каталогов ОС хоста.

В системах, использующих классический SysV init, использовать chroot-окружения сравнительно несложно. Например, чтобы запустить выбранного демона внутри дерева каталогов гостевой ОС, достаточно смонтировать внутри этого дерева /proc, /sys и остальные API ФС, воспользоваться программой chroot(1) для входа в окружение, и выполнить соответствующий init-скрипт, запустив /sbin/service внутри окружения.
Но в системах, использующих systemd, уже не все так просто. Одно из важнейших достоинств systemd состоит в том, что параметры среды, в которой запускаются демоны, никак не зависят от метода их запуска. В системах, использующих SysV init, многие параметры среды выполнения (в частности, лимиты на системные ресурсы, переменные окружения, и т.п.) наследуются от оболочки, из которой был запущен init-скрипт. При использовании systemd ситуация меняется радикально: пользователь просто уведомляет процесс init о необходимости запустить ту или иную службу, и тот запускает демона в чистом, созданном «с нуля» и тщательно настроенном окружении, параметры которого никак не зависят от настроек среды, из которой была отдана команда. Такой подход полностью отменяет традиционный метод запуска демонов в chroot-окружениях: теперь демон порождается процессом init (PID 1) и наследует корневой каталог от него, вне зависимости от того, находился ли пользователь, отдавший команду на запуск, в chroot-окружении, или нет. Кроме того, стоит особо отметить, что взаимодействие управляющих программ с systemd происходит через сокеты, находящиеся в каталоге /run/systemd, так что программы, запущенные в chroot-окружении, просто не смогут
взаимодействовать с init-подсистемой (и это, в общем, неплохо, а если такое ограничение будет создавать проблемы, его можно легко обойти, используя bind-монтирование).
В свете вышесказанного, возникает вопрос: как правильно использовать chroot-окружения в системах на основе systemd? Что ж, постараемся дать подробный и всесторонний ответ на этот вопрос.

Для начала, рассмотрим первое из перечисленных выше применений chroot: изоляция в целях безопасности. Прежде всего, стоит заметить, что защита, предоставляемая chroot’ом, весьма эфемерна и ненадежна, так как chroot не является «дорогой с односторонним движением». Выйти из chroot-окружения сравнительно несложно, и соответствующее предупреждение даже присутствует на странице руководства. Действительно эффективной защиты можно достичь, только сочетая chroot с другими методиками. В большинстве случаев, это возможно только при наличии поддержки chroot в самой программе. Прежде всего, корректное конфигурирование chroot-окружения требует глубокого понимания принципов работы программы. Например, нужно точно знать, какие каталоги нужно bind-монтировать из основной системы, чтобы обеспечить все необходимые для работы программы каналы связи. С учетом вышесказанного, эффективная chroot-защита обеспечивается только в том случае, когда она реализована в коде самого демона. Именно разработчик лучше других знает (обязан знать), как правильно сконфигурировать chroot-кружение, и какой минимальный набор файлов, каталогов и файловых систем необходим внутри него для нормальной работы демона. Уже сейчас существуют демоны, имеющие встроенную поддержку chroot. К сожалению, в системе Fedora, установленной с параметрами по умолчанию, таких демонов всего два: Avahi и RealtimeKit. Оба они написаны одним очень хитрым человеком ;-) (Вы можете собственноручно убедиться в этом, выполнив команду ls -l /proc/*/root.)

Возвращаясь к теме нашего обсуждения: разумеется, systemd позволяет помещать выбранных демонов в chroot, и управлять ими точно так же, как и остальными. Достаточно лишь указать параметр RootDirectory= в соответствующем service-файле. Например:


[Unit]
Description=A chroot()ed Service
[Service]
RootDirectory=/srv/chroot/foobar
ExecStartPre=/usr/local/bin/setup-foobar-chroot.sh
ExecStart=/usr/bin/foobard
RootDirectoryStartOnly=yes


Рассмотрим этот пример подробнее. Параметр RootDirectory= задает каталог, в который производится chroot перед запуском исполняемого файла, заданного параметром ExecStart=. Заметим, что путь к этому файлу должен быть указан относительно каталога chroot (так что, в нашем случае, с точки зрения основной системы, на исполнение будет запущен файл /srv/chroot/foobar/usr/bin/foobard). Перед запуском демона будет вызван сценарий оболочки setup-foobar-chroot.sh, который должен обеспечить подготовку chroot-окружения к запуску демона (например, смонтировать в нем /proc и/или другие файловые системы, необходимые для работы демона). Указав RootDirectoryStartOnly=yes, мы задаем, что chroot() будет выполняться только перед выполнением файла из ExecStart=, а команды из других директив, в частности, ExecStartPre=, будут иметь полный доступ к иерархии файловых систем ОС (иначе наш скрипт просто не сможет выполнить bind-монтирование нужных каталогов). Поместив приведенный выше текст примера в файл /etc/systemd/system/foobar.service, вы сможете запустить chroot’нутого демона командой systemctl start foobar.service. Информацию о его текущем состоянии можно получить с помощью команды systemctl status foobar.service. Команды
управления и мониторинга службы не зависят от того, запущена ли она в chroot’е, или нет. Этим systemd отличается от классического SysV init.

Новые ядра Linux поддерживают возможность создания независимых пространств имен файловых систем (в дальнейшем FSNS, от «file system namespaces»). По функциональности этот механизм аналогичен chroot(), однако предоставляет гораздо более широкие возможности, и в нем отсутствуют проблемы с безопасностью, характерные для chroot. systemd позволяет использовать при конфигурировании юнитов некоторые
возможности, предоставляемые FSNS. В частности, использование FSNS часто является гораздо более простой и удобной альтернативой созданию полновесных chroot-окружений. Используя директивы ReadOnlyDirectories=, InaccessibleDirectories=, вы можете задать ограничения по использованию иерархии файловых систем для заданной службы: ее корнем будет системный корневой каталог, однако указанные в этих
директивах подкаталоги будут доступны только для чтения или вообще недоступны для нее. Например:


[Unit]
Description=A Service With No Access to /home
[Service]
ExecStart=/usr/bin/foobard
InaccessibleDirectories=/home


Такая служба будет иметь доступ ко всей иерархии файловых систем ОС, с единственным исключением — она не будет видеть каталог /home, что позволит защитить данные пользователей от потенциальных хакеров. (Подробнее об этих опциях можно почитать на странице руководства.)
Фактически, FSNS по множеству параметров превосходят chroot(). Скорее всего, Avahi и RealtimeKit в ближайшем будущем перейдут от chroot() к использованию FSNS.

Итак, мы рассмотрели вопросы использования chroot для обеспечения безопасности.
Переходим ко второму пункту: подготовка и управление образом операционной системы при отладке, тестировании, компиляции, установке или восстановлении.
chroot-окружения, по сути, весьма примитивны: они изолируют только иерархии файловых систем. Даже после chroot’а в определенный подкаталог, процесс попрежнему имеет полный доступ к системным вызовам, может убивать процессы, запущенные в основной системе, и т.п. Вследствие этого, запуск полноценной ОС (или ее части) внутри chroot’а несет угрозу для хост-системы: у гостя и хоста отличается лишь содержимое файловой системы, все остальное у них общее. Например, если вы обновляете дистрибутив, установленный в chroot-окружении, и пост-установочный скрипт пакета отправляет SIGTERM процессу init для его перезапуска, на него среагирует именно хост-система! Кроме того, хост и chroot’нутая система будут иметь общую разделяемую память SysV (SysV shared memory), общие сокеты из абстрактных пространств имен (abstract namespace sockets) и другие элементы IPC. Для отладки, тестирования, компиляции, установки и восстановлении ОС не требуется абсолютно неуязвимая изоляция — нужна лишь надежная защита от случайного воздействия на ОС хоста изнутри chroot-окружения, иначе вы можете получить целый букет проблем, как минимум, от пост-инсталляционных скриптов при установке пакетов в chroot-окружении. systemd имеет целый ряд возможностей, полезных для работы с chroot-системами.

Прежде всего, управляющая программа systemctl автоматически определяет, что она запущена в chroot-системе. В такой ситуации будут работать только команды systemctl enable и systemctl disable, во всех остальных случаях systemctl просто не будет ничего делать, возвращая код успешного завершения операции. Таким образом, пакетные скрипты смогут включить/отключить запуск «своих» служб при загрузке (или в других ситуациях), однако команды наподобие systemctl restart (обычно выполняется при обновлении пакета) не дадут никакого эффекта внутри chroot-окружения.
Прим. перев.: Автор забывает отметить не вполне очевидный момент: такое поведение systemctl проявляется только в «мертвых» окружениях, т.е. в тех, где не запущен процесс init, и соответственно отсутствуют управляющие сокеты в /run/systemd. Такая ситуация возникает, например, при установке системы в chroot через debootstrap/febootstrap. В этом случае возможности systemctl ограничиваются операциями с символьными ссылками, определяющими триггеры активации юнитов, т.е. выполнением действий enable и disable, не требующих непосредственного взаимодействия с процессом init.

Однако, куда более интересные возможности предоставляет программа systemdnspawn, входящая в стандартный комплект поставки systemd. По сути, это улучшенный аналог chroot(1) — она не только подменяет корневой каталог, но и создает отдельные пространства имен для дерева файловых систем (FSNS) и для идентификаторов процессов (PID NS), предоставляя легковесную реализацию системного контейнера.
Прим. перев.: Используемые в systemd-nspawn механизмы ядра Linux, такие, как FS NS и PID NS, также лежат в основе LXC, системы контейнерной изоляции для Linux, которая позиционируется как современная альтернатива классическому OpenVZ. Стоит отметить, что LXC ориентирована прежде всего на создание независимых виртуальных окружений, с поддержкой раздельных сетевых стеков, ограничением на ресурсы, сохранением настроек и т.п., в то время как systemd-nspawn является лишь более удобной и эффективной заменой команды chroot(1), предназначенной прежде всего для развертывания, восстановления, сборки и тестирования операционных систем. Далее автор разъясняет свою
точку зрения на этот вопрос.

systemd-nspawn проста в использовании как chroot(1), однако изоляция от хост-системы является более полной и безопасной. Всего одной командой вы можете загрузить внутри контейнера полноценную ОС (на базе systemd или SysV init). Благодаря использованию независимых пространств идентификаторов процессов, процесс init внутри контейнера получит PID 1, что позволит работать ему в штатном режиме. Также, в отличие от chroot(1), внутри окружения будут автоматически смонтированы /proc и /sys.

Следующий пример иллюстрирует возможность запустить Debian на Fedora-хосте всего тремя командами:


# yum install debootstrap
# debootstrap --arch=amd64 unstable debian-tree/
# systemd-nspawn -D debian-tree/


Вторая из этих команд обеспечивает развертывание в подкаталоге ./debian-tree/ файловой структуры дистрибутива Debian, после чего третья команда запускает внутри полученной системы процесс командной оболочки. Если вы хотите запустить внутри контейнера полноценную ОС, воспользуйтесь командой


# systemd-nspawn -D debian-tree/ /sbin/init


После быстрой загрузки вы получите приглашение оболочки, запущенной внутри полноценной ОС, функционирующей в контейнере. Изнутри контейнера невозможно увидеть процессы, которые находятся вне его. Контейнер сможет пользоваться сетью хоста, однако не имеет возможности изменить ее настройки (это может привести к серии ошибок в процессе загрузки гостевой ОС, но ни одна из этих ошибок не должна
быть критической).
Прим. перев.: Впоследствии в systemd-nspawn была добавлена опция --private-network, позволяющая изолировать систему внутри контейнера от сети хоста: такая система будет видеть только свой собственный интерфейс обратной петли.
Контейнер получает доступ к /sys и /proc/sys, однако, во избежание вмешательства контейнера в конфигурацию ядра и аппаратного обеспечения хоста, эти каталоги будут смонтированы только для чтения. Обратите внимание, что эта защита блокирует лишь случайные, непредвиденные попытки изменения параметров. При необходимости, процесс внутри контейнера, обладающий достаточными полномочиями,
сможет перемонтировать эти файловые системы в режиме чтения-записи.

Итак, что же такого хорошего в systemd-nspawn?
1. Использовать эту утилиту очень просто. Вам даже не нужно вручную монтировать внутри окружения /proc и /sys — она сделает это за вас, а ядро автоматически отмонтирует их, когда последний процесс контейнера завершится.
2. Обеспечивается надежная изоляция, предотвращающая случайные изменения параметров ОС хоста изнутри контейнера.
3. Теперь вы можете загрузить внутри контейнера полноценную ОС, а не одну единственную оболочку.
4. Эта утилита очень компактна и присутствует везде, где установлен systemd. Она не требует специальной установки и настройки.

systemd уже подготовлен для работы внутри таких контейнеров. Например, когда подается команда на выключение системы внутри контейнера, systemd на последнем шаге вызывает не reboot(), а просто exit().
Стоит отметить, что systemd-nspawn все же не является полноценной системой контейнерной виртуализации/изоляции — если вам нужно такое решение, воспользуйтесь LXC. Этот проект использует те же самые механизмы ядра, но предоставляет куда более широкие возможности, включая виртуализацию сети. Могу предложить такую аналогию: systemd-nspawn как реализация контейнера похожа на GNOME 3 — компактна и
проста в использовании, опций для настройки очень мало. В то время как LXC больше похож на KDE: опций для настройки больше, чем строк кода. Я создал systemd-nspawn специально для тестирования, отладки, сборки, восстановления. Именно для этих задач вам стоит ее использовать — она неплохо с ними справляется, куда лучше, чем chroot(1).

Что ж, пора заканчивать. Итак:
1. Использование chroot() для обеспечения безопасности дает наилучший результат, когда оно реализовано непосредственно в коде самой программы.
2. ReadOnlyDirectories= и InaccessibleDirectories= могут быть удобной альтернативой созданию полновесных chroot-окружений.
3. Если вам нужно поместить в chroot-окружение какую-либо службу, воспользуйтесь опцией RootDirectory=.
4. systemd-nspawn — очень неплохая штука.
5. chroot’ы убоги, FSNS — 1337.
И все это уже сейчас доступно в Fedora 15.

Содержание
Вперед - Поиск виновных
Назад - Три уровня выключения