Обновление. Инструкция в этой статье рабочая, но сам chroot устарел как инструмент. Сейчас в большинстве случаев лучше взять Docker: в нём есть куча готовых контейнеров на любой чих. Однако, иногда chroot незаменим и будет нелишним посмотреть как им пользоваться.
Chroot-окружение — это самый доступный способ запустить линукс внутри линукса (т.н. контейнер). Внутренняя система будет изолирована от основной только на уровне корневой файловой системы (rootfs). Сетевой стэк, псевдо-файловые системы /proc
, /sys
, /dev
будут общими для хоста и контейнера. Поэтому не стоит считать chroot-окружение достаточной мерой для изоляции опасных/недоверенных приложений. Для этого больше подходит systemd-nspawn или docker.
Основные риски:
- Из chroot-окружения можно убивать процессы хоста.
- Система из chroot-окружения имеет доступ к дисковым устройствам. Являясь рутом в контейнере, можно примонтировать блочное устройство хоста (какой-нибудь
/dev/sda1
) и делать с ним всё что угодно.
Если chroot так несовершенен, то зачем он нужен сегодня?
Не всё работает в systemd-nspawn контейнере. Из-за усиленных защитных мер в контейнере systemd-nspawn нельзя натянуть файл на петлевое устройство (loop device) и нельзя запустить ещё один контейнер (линукс внутри линукса внутри линукса). В чруте можно и то, и другое. Это может понадобится, например, для сборки livecd.
Не везде есть systemd. Таких дистрибутивов почти не осталось. Это либо что-то старое, либо Gentoo или Alpine. Chroot работает везде.
Дальше я расскажу как создать контейнер с Debian-based системой (Debian, Ubuntu, Kali). Делать надо то же самое, что и при создании systemd-nspawn контейнера, но есть небольшие отличия.
- Шаг 1. Debootstrap
- Шаг 2. Вход в систему (chroot)
- Шаг 3. Первичная настройка системы
- Шаг 4. Доведение до ума
- Шаг 5. Использование готового контейнера
Нам понадобятся…
Для запуска контейнера ничего дополнительного не требуется.
Запускать 32-битную систему можно как из 32-битной, так и из 64-битной. А вот 64-битную из 32-битной нельзя.
Для создания контейнера с Debian/Ubuntu понадобится утилита debootstrap
из одноимённого пакета. Он точно есть в Debian, Ubuntu, Archlinux и Gentoo. Наверняка есть и в других дистрибутивах. В конце концов это всего лишь набор shell-скриптов.
Шаг 1. Debootstrap
Debootstrap разворачивает в указанном каталоге минимальный набор пакетов, необходимый для самостоятельной работы системы. Это самый важный шаг. Если сейчас допустить ошибку, то всё придётся переделывать сначала. Инструкцию я вынес в отдельный пост debootstrap.
Рекомендую сделать бэкап сразу после этого шага, чтобы в случае чего можно было откатиться назад, а не переделывать всё с начала. Ну, как в компьютерной игре сохраниться. Я разворачиваю Debian Jessie, поэтому мои бэкапы содержат jessie в названии. Команды подсмотрены в Ubuntu community help wiki.
# tar cpzf jessie-boostrapped.tar.gz --one-file-system -C jessie .
или
# tar cpjf jessie-boostrapped.tar.bz2 --one-file-system -C jessie .
или
# tar cpJf jessie-boostrapped.tar.xz --one-file-system -C jessie .
Здесь jessie
— это каталог с корнем chroot-системы.
Наибольшее сжатие будет у .tar.xz
архива, самое быстрое у tar.gz
. Посередине находится tar.bz2
. У меня в цифрах получилось так:
Сжатие | Размер архива, MiB | Время компрессии, с |
---|---|---|
gzip | 91 | 7.5 |
bzip2 | 83 | 16.6 |
xz | 68 | 68.2 |
Восстановление из бэкапа:
# rm -rf jessie && mkdir jessie
# tar xpf /path/to/backup.tar.gz -C jessie --numeric-owner
или
# tar xpf /path/to/backup.tar.bz2 -C jessie --numeric-owner
или
# tar xpf /path/to/backup.tar.xz -C jessie --numeric-owner
Шаг 2. Вход в систему (chroot)
Чтобы процессы из chroot-окружения могли полноценно работать, необходимо примонтировать псевдо-файловые системы /proc
, /sys
, /dev
внутрь контейнера:
# cd jessie
# mount proc -t proc ./proc
# mount sys -t sysfs ./sys
# mount --bind /dev ./dev
# mount --bind /dev/pts ./dev/pts
/dev/pts
— виртуальная файловая система, которая динамически создаёт файлы терминалов /dev/pts/X
для каждого нового подключения. Это необходимо для нормального функционирования многих программ, работающих с терминалом, в т.ч. скриптов debconfig.
После того, как всё необходимое смонтировано, входим в контейнер:
# chroot ./ /bin/bash
Стоп, так делать не надо! Такой чрут потянет за собой переменные окружения из внешней системы (проверяется командой env
). Вот правильный чрут:
# chroot ./ /usr/bin/env -i HOME=/root TERM="$TERM" /bin/bash --login
Всё, мы внутри! В дальнейшем по тексту я буду отмечать чрутовый шелл приглашением (chroot)#
.
Можно ли поиметь сразу несколько терминалов для одного chroot-окружения? Да, просто делаем ещё один точно такой же чрут и получаем второй терминал. Повторно монтировать ничего не надо.
Шаг 3. Первичная настройка системы
Задаём пароль рута:
(chroot)# passwd
Устанавливаем часовой пояс:
(chroot)# ln -sf /usr/share/zoneinfo/UTC /etc/localtime
или
(chroot)# ln -sf /usr/share/zoneinfo/Europe/Moscow /etc/localtime
Если создать файл /etc/debian_chroot
с именем гостевой ОС, то bash будет выводить его в начале приглашения командной строки. Это позволит определить какой терминал в какой системе находится, поскольку хостнэйм у контейнера такой же, как у хоста.
(chroot)# echo jessie > /etc/debian_chroot
В чруте будет как-то так (white — имя моей основной системы):
(jessie)root@white:~#
Чтобы df
корректно работал, нужно сделать mtab:
(chroot)# cat >/etc/mtab <<EOF
rootfs / rootfs rw 0 0
EOF
Сеть должна работать без дополнительных манипуляций. Проверяем:
(chroot)# apt update
Если возникли проблемы с DNS, берём /etc/resolv.conf
с основной системы:
# cp /etc/resolv.conf ./etc/resolv.conf
Устанавливаем локаль:
(chroot)# apt install locales
(chroot)# echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen
(chroot)# locale-gen
(chroot)# update-locale LANG=en_US.UTF-8
- Вы можете не использовать
echo
, а раскомментировать необходимые локали в/etc/locale.gen
вручную. - Вместо
en_US.UTF-8
можно прописатьru_RU.UTF-8
, тогда всё будет по-русски. - Последние 3 команды можно заменить интерактивной
dpkg-reconfigure locales
.
Проверяем список доступных локалей и текущую локаль:
(chroot)# locale -a
(chroot)# locale
Локаль у рута будет пустой, даже если перезайти в чрут. Дело в том, что переменные окружения устанавливает не шелл, а процесс, который делает логин, а мы входим в чрут без логина. У меня есть два решения:
- Вручную проэкспортировать все необходимые переменные окружения через шелл:
(chroot)# echo "export LANG=en_US.UTF-8" >> ~/.profile
(chroot)# source ~/.profile
- Входить в чрут по ssh. Вариант рабочий, но неудобный, т.к. требует запущенного ssh-сервера в чруте.
Конфигурируем менеджер пакетов
При необходимости добавляем репозитории, указываем локальное зеркало и т.д.
Спортсмены отключают установку рекомендованных пакетов:
(chroot)# echo 'APT::Get::Install-Recommends "false";' >>/etc/apt/apt.conf
(chroot)# echo 'APT::Get::Install-Suggests "false";' >>/etc/apt/apt.conf
Мне нравится устанавливать и обновлять пакеты без подтверждения Do you want to continue? [Y/n]
. Ведь если я приказал установить пакет, значит я в этом уверен и не надо переспрашивать. К тому же так быстрее получается. Отключаем подтверждение:
(chroot)# echo 'APT::Get::Assume-Yes "true";' >>/etc/apt/apt.conf
В простейшей конфигурации контейнера гостевая и хостовая система используют один и тот же сетевой стэк. Это значит, что если 22 порт уже занят ssh-сервером хостовой системы, то ssh-сервер гостевой системы не запустится. Необходимо в его настройках изменить номер порта. Это же справедливо для веб-серверов и прочих сервисов, работающих на стандартных портах.
Чтобы сервисы не стартовали сразу после установки пакетов:
(chroot)# cat > /usr/sbin/policy-rc.d <<EOF
#!/bin/sh
exit 101
EOF
(chroot)# chmod a+x /usr/sbin/policy-rc.d
Самое время сделать второй бэкап:
# tar cpJf jessie-configured.tar.xz --exclude='./var/cache/apt/archives/*.deb' \
--exclude='./var/lib/apt/lists/*' --exclude='./var/cache/apt/*.bin' \
--exclude './proc/* ./sys/* ./dev/*' --one-file-system -C jessie .
Чтобы архив занимал меньше места, мы исключаем:
/var/cache/apt/archives/*.deb
— кэш deb-пакетов apt’а
/var/lib/apt/lists/*
— сохранённые индексы репозиториев (что лежит в таком-то репозитории)
/var/cache/apt/*.bin
— какая-то база данных apt’а
Шаг 4. Доведение до ума
Дополнительные пакеты, которые могут понадобиться:
build-essential
для сборки deb-пакетов
cron
— планировщик задач
file
— утилита для определения типа файла
less
— утилита просмотра текста с возможностью скроллить
logrotate
nano
— простенький консольный текстовый редактор
ncurses-term
для поддержки терминала rxvt-unicode-256color и других
openssh-server
rsync
для синхронизации файлов и каталогов, в т.ч. по сети
sudo
vim
или vim-tiny
zsh
Напоминаю, что посмотреть описание пакета можно командой
(chroot)# apt show ПАКЕТ
Чтобы получать zsh вместо bash, входить в контейнер нужно так:
# chroot ./ /usr/bin/env -i HOME=/root TERM="$TERM" /usr/bin/zsh --login
Создаём пользователя vas, входящего в группу sudo
. Вы можете создать другого :)
(chroot)# useradd -m -g users -G sudo -s /usr/bin/zsh vas
(chroot)# passwd vas
Переключение с рута на пользователя делается командой:
(chroot)# su - vas
Иногда бывает удобно сделать sudo без ввода пароля. На хостовой системе я себе таких шалостей не позволяю, а в контейнере можно. Только не редактируйте /etc/sudoers
напрямую! Используйте команду visudo
.
Разрешаем sudo без пароля для всех пользователей, входящих в группу sudo
:
В качестве вишенки на торте возьмём zshrc, vimrc и т.п. с хостовой системы.
Можно немного облегчить систему за счёт man-страниц, документации, локалей. Это для особо целеустремлённых спортсменов.
(chroot)# rm -rf /usr/share/doc/*
(chroot)# find /usr/share/locale -maxdepth 1 -mindepth 1 ! -name en_US -exec rm -rf {} \;
(chroot)# find /usr/share/i18n/locales -maxdepth 1 -mindepth 1 ! -name en_US -exec rm -rf {} \;
(chroot)# rm -rf /usr/share/man/*
(chroot)# rm -rf /usr/share/groff/*
(chroot)# rm -rf /usr/share/info/*
(chroot)# rm -rf /usr/share/lintian/*
(chroot)# rm -rf /usr/include/*
И последний бэкап на сегодня:
# tar cpJf jessie-vas.tar.xz --exclude='./var/cache/apt/archives/*.deb' \
--exclude='./var/lib/apt/lists/*' --exclude='./var/cache/apt/*.bin' \
--exclude './proc/* ./sys/* ./dev/*' --one-file-system -C jessie .
Всё, контейнер jessie готов к работе в chroot-окружении! Благодаря бэкапу, в любой момент можно откатиться к чистому состоянию или сделать ещё несколько однотипных контейнеров.
Размонтируем примонтированное, если контейнер пока не нужен:
# cd jessie && umount ./proc ./sys ./dev/pts ./dev
Иногда что-то не хочет размонтироваться. Так бывает, когда в чруте остались работающие процессы. Их надо найти и убить.
Первое, что приходит в голову — поискать pid-файлы сервисов:
(chroot)# ls /var/run/*.pid
(chroot)# cat /var/run/*.pid | xargs kill
Pid-файлы есть только у сервисов, а ведь могут остаться и другие процессы. А как их найти? Посмотрим в ядре!
# for p in /proc/[1-9]*; do \
test $(readlink -f $p/root) = /home/chroot/jessie && echo ${p#/proc/}; \
done | xargs --no-run-if-empty ps
Здесь цикл проходит по всем процессам и смотрит их параметр root
, который является симлинком на корневую ФС процесса. Если симлинк ведёт в каталог нашего контейнера (/home/chroot/jessie
в моём случае), командой echo
выводится PID этого процесса. С помощью xargs
вывод цикла превращается в аргументы для команды ps, чтобы показать вам процессы в удобном виде. Замените ps
на kill
, чтобы прибить их.
Некоторые процессы могут не захотеть умирать добровольно, тогда вам нужен смертельный kill -9
.
После этого всё точно размонтируется.
Шаг 5. Использование готового контейнера
В принципе вы его уже использовали, пока готовили. Но предположим, что он какое-то время лежал без дела и вдруг понадобился. Тогда первым делом монтируем всё необходимое:
# cd jessie
# mount proc -t proc ./proc
# mount sys -t sysfs ./sys
# mount --bind /dev ./dev
# mount --bind /dev/pts ./dev/pts
Если чрут будет использоваться регулярно, то имеет смысл настроить автоматическое монтирование (через /etc/fstab
), чтобы не набивать эти команды руками.
# echo "proc /var/lib/machines/jessie/proc proc defaults 0 0" >> /etc/fstab
# echo "sysfs /var/lib/machines/jessie/sys sysfs defaults 0 0" >> /etc/fstab
# echo "/dev /var/lib/machines/jessie/dev none defaults,rbind 0 0" >> /etc/fstab
После правки fstab проверим что всё монтируется как положено:
# mount -a
# mount | grep /var/lib/machines/jessie
Входим в контейнер:
# chroot ./ /usr/bin/env -i HOME=/root TERM="$TERM" /bin/bash --login
или
# chroot ./ /usr/bin/env -i HOME=/root TERM="$TERM" /usr/bin/zsh --login
и занимаемся в нём своими делами. Если надо запускать какие-то сервисы, то пуск и останов через systemd работать не будет. Однако старые добрые init-скрипты работают. Пример запуска ssh-сервера в чруте:
(chroot)# /etc/init.d/ssh start
Кстати, не забудьте предварительно сменить порт ssh-сервера!
Что ещё почитать про chroot?
- Используем chroot и debootstrap для всяких светлых целей — тестирование скриптов, программ и прочего. Статья, вдохновившая меня на использование chroot-контейнеров в своей практике. Подробно описаны недостатки chroot-окружения.