Привет, Хабр! 👋
Это моя первая статья здесь, да и вообще-то - первая, поэтому - судите строго. Или не строго. Или не судите. В общем, как хотите.
Пишу её в первую очередь для себя — чтобы систематизировать накопленный опыт. Ну а если кому-то окажется полезно — буду только рад. Если нет... что ж, тоже не расстроюсь.
Сегодня мы поговорим о dm-crypt в Linux — точнее дане не столько о нем, сколько о его использовании в немного необычном ключе: запуск полноценной ОС из зашифрованного контейнера, без выделения отдельного дискового раздела.
Всё, что вы здесь прочитаете — это мой личный опыт, без претензий на истину в последней инстанции. Я не собираюсь вступать в вечные полемики "LUKS против plain", "dm-crypt против VeraCrypt" и тому подобное. Мне просто было интересно попробовать кое-что нестандартное.
Почему?
Потому что могу.
И потому что — как говорится, "руки чесались".
Как я к этому пришёл
Итак, начать, наверное, следует с того, что я хоть и являюсь довольно давним и убежденным пользователем Linux, отнести себя к профессионалам в области его администрирования и тонкой настройки я не могу. Скорее уже я энтузиаст, которому интересно ковыряться в системах, придумывать себе задачи и самому же их решать
Вот и тут было так же: я вдруг подумал — а что если...
...запустить полноценную Linux-систему не с отдельного раздела, а прямо из файла-образа?
Причём:
образ должен быть зашифрованным (dm-crypt plain),
система должна работать на реальном железе,
без костылей типа внешних загрузчиков,
ну и по возможности без слишком грязного initrd-напильника... хотя последнее вышло как всегда.
Зачем всё это было нужно?
Честно?
Просто ради интереса.
Без паранойи, без шапочек из фольги. Чисто "а давай замутим" в лучших традициях заскучавшего технаря.
Плюс идея позволяла бы:
держать разные экспериментальные системы внутри обычных файлов,
использовать полноценное шифрование "на лету",
а при необходимости вообще не оставлять прямых следов на физическом уровне.
ОС в качестве полигона для испытаний
Вся магия будет происходить в давно и беззаветно полюбившемся мне дистрибутиве — Arch linux. В принципе, при небольших поправках на специфику то же самое с применимо и к Void linux (проверено лично), и, в общем‑то, к любому другому дистрибутиву, но в этом обзоре рассказ будет вестись именно с прицелом на арч.
Маленькая оговорка про шифрование
В этой статье я использую dm-crypt plain, без LUKS и LUKS2.
Не потому что они плохие — просто потому что захотелось.
Так что если вдруг при чтении вас пробьёт на мысль "А зачем так извращаться?" — ответ прост:
"Потому что захотелось."
Начало работ: создание зашифрованного контейнера
Для начала я создал зашифрованный образ нужного размера.
Ничего сложного — старый добрый dd
:
dd if=/dev/urandom of=.local/share/newos.img bs=256M count=80 status=progress
Этот образ открываем через cryptsetup
, используя внешний файл-ключ:
sudo cryptsetup open --type=plain --key-file=/mnt/snkey ~/.local/share/newos.img root
Форматируем свежесозданный контейнер:
sudo mkfs.ext4 -L root /dev/mapper/root
Дальше устанавливаем туда минимальный Arch Linux с рабочим окружением (у меня — MATE). Сам процесс установки ОС в наш образ в рамках данной статьи я обозревать не буду — он ни чем кардинально не отличается от установки на физический раздел и прекрасно расписан в арчвики и куче других, адаптированных для разного уровня подготовки пользователей, источниках. Но один нюанс осветить, наверное, для понимания стоит:
поскольку наш образ при таком подходе зашифрован, ядро ОС и образ initramfs должны находится где‑то «во вне», за пределами зашифрованного блока, дабы можно было их беспрепятственно запустить. В моем случае для этой цели был выбран esp‑раздел (EFI System Partition), который был примонтирован в качестве /boot раздела в расшифрованную систему. Таким образом, ядро и начальный рам нашей «контейнерной» ОС доступны для загрузки.
Написание хука для загрузки контейнера
В базовой поставке Arch Linux и cryptsetup уже есть хук encrypt
, который позволяет загружать систему с зашифрованного раздела.
Но есть проблема: он работает только с устройствами, а не с файлами внутри файловых систем.
Иными словами — он умеет расшифровывать /dev/sdX
, но не умеет расшифровывать /mnt/somefile.img
на разделе, который никуда не примонтирован.
А мне надо было именно это.
Чтож, не даром говорят: хочешь, чтобы что-то было сделано хорошо - сделай это сам.
Что должен уметь кастомный хук
Парсить новый параметр ядра
src_rootfs
.Монтирова устройство, указанное в
src_rootfs
.Находить образ-файл.
Подключать его как loop-устройство (
/dev/loop0
).
Параметр загрузки ядра
Добавляем новый параметр для ядра:
src_rootfs=<устройство>:/путь/до/образа.img
Сам скрипт-хук (/usr/lib/initcpio/hooks/neoshy)
#!/usr/bin/ash
# Для упрощения жизни объявим функцию, которая будет парсить параметры cmdline ядра
# и распознавать переданное устройство по UUID, PARTUUID, LABEL, PARTLABEL, не и по
# привычному /dev/sdXY:
device_identify() {
local dev="$1"
case "$dev" in
UUID=*)
echo "/dev/disk/by-uuid/${dev#UUID=}"
;;
PARTUUID=*)
echo "/dev/disk/by-partuuid/${dev#PARTUUID=}"
;;
LABEL=*)
echo "/dev/disk/by-label/${dev#LABEL=}"
;;
PARTLABEL=*)
echo "/dev/disk/by-partlabel/${dev#PARTLABEL=}"
;;
/dev/*)
echo "$dev"
;;
# В случае ошибки выводим "страшное" сообщение:
*)
echo "[neoshy] ERROR: Unknown device or device format: $dev" >&2
exit 1
;;
esac
}
run_hook() {
echo "[neoshy] Starting hook"
# Вытаскиваем параметры ядра:
src_rootfs="$(getarg src_rootfs)"
if [ -z "$src_rootfs" ]; then
echo "[neoshy] ERROR: Missing required parameter: src_rootfs" >&2
exit 1
fi
# Получаем "сырое" значение девайса из параметра
src_dev="${src_rootfs%%:*}"
# Получаем путь до образа из этого же параметра:
src_img="${src_rootfs#*:}"
if [ -z "$src_dev" ] || [ -z "$src_img" ]; then
echo "[neoshy] ERROR: Invalid src_rootfs format" >&2
exit 1
fi
# Получаем реальный путь или идентификатор переданного устройства:
real_dev=$(device_identify "$src_dev")
# ...и монтируем его
echo "[neoshy] Mounting device: $real_dev"
mkdir -p /mnt
mount $real_dev /mnt
# На всякий случай проверяем наличие указанного образа:
if [ ! -f "/mnt$src_img" ]; then
echo "[neoshy] ERROR: Image file not found: /mnt$src_img" >&2
exit 1
fi
# ...и, если все ок, подключаем его, как loop-устройство
echo "[neoshy] Attaching loop device for: /mnt$src_img"
losetup --show --find "/mnt$src_img"
echo "[neoshy] Finishing hook"
}
install-файл (/usr/lib/initcpio/install/neoshy)
#!/bin/bash
build() {
add_module "loop"
add_binary losetup
add_binary mount
add_runscript
}
help() {
cat <<HELPEOF
Minimal neoshy hook installer:
Mounts src_dev and maps src_img to /dev/loopX before starting the encrypt hook.
HELPEOF
}
Установка файлов
Копируем файлы:
hook
в/usr/lib/initcpio/hooks/neoshy
install
в/usr/lib/initcpio/install/neoshy
Конфигурация /etc/mkinitcpio.conf
Обязательно правим:
MODULES=(<ваши модули> dm_mod dm_crypt xts sha256 exfat)
HOOKS=(<ваши хуки> neoshy encrypt filesystems fsck)
Важно:
Наш хук должен идти до хука encrypt
, т. к. encrypt
будет использовать луп-девайс, который создает наш хук; и в массив MODULES
нужно добавить модули dm_mod, dm_crypt, xts, sha256.
По идее, они все должны подтягиваться автоматически хуком autodetect
, на практике же у меня этого почему-то не произошло (подозреваю chtroot-окружение), поэтому их пришлось добавить вручную. Плюс сюда же нужно прописать модули для файловой системы, на которой лежит наш контейнер, если это не ext2,3,4., иначе раздел может не примонтироваться
Пересборка initramfs
sudo mkinitcpio -p linux
Или подставляйте ядро, которое реально используете (linux-lts
, linux-zen
и т.п.).
Настройка загрузки через EFI
Я использую минималистичный подход:
EFI → ядро → initramfs → система, без всяких grub-ов и других прослоек.
Напомню, что ESP-раздел у меня примонтирован в /esp
Пример скрипта загрузки (/esp/arch.nsh)
\ArchTest\vmlinuz-linux src_rootfs=UUID=<ваш UUID>/itxst/.local/share/newos.img cryptdevice=/dev/loop0:newos_root cryptkey=PARTUUID=<ваш PARTUUID>:0:3214325 crypto=:::: root=/dev/mapper/newos_root rw initrd=\ArchTest\initramfs-linux.img
Что здесь происходит:
src_rootfs
— указываем устройство и путь до образа.cryptdevice
— подключаем/dev/loop0
как новое шифрованное устройство.cryptkey
— если нужен файл-ключ.crypto=::::
— дефолтные параметры шифрования.root
— куда смонтировать расшифрованный контейнер.
Альтернативные варианты
Когда всё будет отлажено, можно добавить постоянную запись через efibootmgr
:
sudo efibootmgr --create --disk /dev/nvme0n1 --part 1 --loader '\ArchTest\vmlinuz-linux' --label "Arch Container Boot" --unicode 'src_rootfs=UUID=<ваш UUID>:/itxst/.local/share/newos.img cryptdevice=/dev/loop0:newos_root cryptkey=PARTUUID=<ваш PARTUUID> crypto=:::: root=/dev/mapper/newos_root rw initrd=\ArchTest\initramfs-linux.img' --verbose
Итого
На выходе мы получаем:
зашифрованную файловую систему с установленной ОС в одном файле;
полноценную загрузку через EFI без промежуточных загрузчиков;
возможность быстро экспериментировать с разными системами без переразметки диска.
На этом, собственно, с настройкой покончено. Перезагружаемся в EFI-Shell, запускаем из esp-раздела наш скрипт и... вэлкам в изолированный от основной ФС контейнер с нашей свежей арч-установкой. Не знаю, будет ли подобное извращение кому-нибудь полезно, но лично мне в итоге оказалось удобно для тестирования и обкатки новомодных и не очень тенденций в линукс-мире перед их применением на основной системе. Настройка шифрования и загрузки с зашифрованного контейнера в данном случае - просто интерес и разминка для мозгов, но потенциально тоже может быть полезно в некоторых сценариях в наше странное время.
P.S.:
Пока писал статью, вспомнил про старую фичу Wubi (Windows-based Ubuntu Installer).
Там тоже Linux ставился в файл на диске с Windows.
В чём-то подход похож:
Нет отдельного раздела.
ОС живёт в образе.
По большому счету, у нас происходит то же самое, только не на ntfs-разделе (можно и замутить, но лень) и плюс от себя я добавил шифрование этого самого образа.
P.P.S.:
Если вдруг кому-нибудь станет совсем скучно и захочется поковыряться не занимаясь копипастой — есть готовый пакет в AUR:
paru -S mkinitcpio-hook-neoshy
Или можно забрать с GitHub:
git clone https://github.com/andr31ab/mkinitcpio-hook-neoshy.git
Спасибо за внимание!
Пробуйте, экспериментируйте, шифруйте!
И не забывайте делать бэкапы. 😉