LaurVas

Запускаем Debian в chroot-окружении

   chroot debian

Обновление. Инструкция в этой статье рабочая, но сам 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 контейнера, но есть небольшие отличия.

Нам понадобятся…

Для запуска контейнера ничего дополнительного не требуется.

Запускать 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-сервера в чруте.

Конфигурируем менеджер пакетов

При необходимости добавляем репозитории, указываем локальное зеркало и т.д.

/etc/apt/sources.list для Debian Squeeze
deb http://archive.debian.org/debian squeeze main contrib non-free
deb http://archive.debian.org/debian-security squeeze/updates main contrib non-free
deb http://archive.debian.org/debian squeeze-lts main contrib non-free
# deb http://archive.debian.org/backports.org squeeze-backports main contrib non-free
/etc/apt/sources.list для Debian Wheezy
deb http://mirror.yandex.ru/debian wheezy main contrib non-free
deb http://mirror.yandex.ru/debian-security wheezy/updates main contrib non-free
deb http://mirror.yandex.ru/debian wheezy-updates main contrib non-free
# deb http://mirror.yandex.ru/debian wheezy-backports main
/etc/apt/sources.list для Debian Jessie
deb http://mirror.yandex.ru/debian jessie main contrib non-free
deb http://mirror.yandex.ru/debian-security jessie/updates main contrib non-free
deb http://mirror.yandex.ru/debian jessie-updates main contrib non-free
# deb http://mirror.yandex.ru/debian jessie-backports main
/etc/apt/sources.list для Ubuntu Precise Pangolin
deb http://mirror.yandex.ru/ubuntu precise main multiverse restricted universe
deb http://mirror.yandex.ru/ubuntu precise-security main multiverse restricted universe
deb http://mirror.yandex.ru/ubuntu precise-updates main multiverse restricted universe
/etc/apt/sources.list для Ubuntu Trusty Tahr
deb http://mirror.yandex.ru/ubuntu trusty main multiverse restricted universe
deb http://mirror.yandex.ru/ubuntu trusty-security main multiverse restricted universe
deb http://mirror.yandex.ru/ubuntu trusty-updates main multiverse restricted universe
/etc/apt/sources.list для Ubuntu Xenial Xerus
deb http://mirror.yandex.ru/ubuntu xenial main multiverse restricted universe
deb http://mirror.yandex.ru/ubuntu xenial-security main multiverse restricted universe
deb http://mirror.yandex.ru/ubuntu xenial-updates main multiverse restricted universe
/etc/apt/sources.list для Kali Sana
deb http://old.kali.org/kali sana main contrib non-free
deb http://old.kali.org/kali-security sana/updates main contrib non-free
/etc/apt/sources.list для Kali Rolling
deb http://http.kali.org/kali kali-rolling main non-free contrib

Спортсмены отключают установку рекомендованных пакетов:

(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:

/etc/sudoers
%sudo    ALL=(ALL:ALL) NOPASSWD:ALL

В качестве вишенки на торте возьмём 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-сервера!