Не так давно наткнулся на статью Red Hat про разблокировку LUKS-шифрования по SSH. Суть там простая: сервер с зашифрованным корнем перезагружается, замирает в initramfs и ждёт парольную фразу на консоли, за которой никто не сидит. Решение у редхатовцев выглядит довольно простым: поставить пакет dracut-sshd, включить модуль NetworkManager в initramfs, дописать параметры ядра. Несколько команд, и застрявшая машина открывается по SSH с вашего дивана.

И стало мне интересно: а как та же задача решается, если у меня на серваках крутится не RHEL, а наш Альт? Копировать редхатовский рецепт вслепую смысла нет: у Альта собственная система сборки initramfs и свои механизмы ранней загрузки. Тем любопытнее посмотреть, как всё устроено здесь.

TL;DR. Сделать можно. Проверил это на живой виртуалке. Рецепт в итоге вышел на удивление прямолинейный, но с двумя-тремя особенностями, о которых лучше узнать из статьи.

Постановка задачи, или зачем шифровать то, что стоит в шкафу

Сначала два слова о проблеме, чтобы не тащить за собой тех, кто уже в теме. LUKS — это формат шифрования блочных устройств в Linux: корневая файловая система лежит внутри зашифрованного контейнера, и пока контейнер не открыт, ранней загрузке нечего монтировать. Разумная практика для сервера, который стоит где-нибудь в чужом дата-центре: вынули диски — получили криптографический мусор вместо базы клиентов.

Обратная сторона фичи: при каждой перезагрузке кто-то обязан вручную ввести фразу. Клавиатура, монитор, поездка в дата-центр (или BMC-консоль, если повезло с железом). Сервер перезагрузился ночью после обновления, и всё: он ждёт вас. Физически.

Идея разблокировки по сети существует столько же, сколько удалённые серверы: положить в initramfs сетевой стек и маленький SSH-сервер, подключиться к машине, пока она стоит на паузе, и вручить ей парольную фразу издалека. Red Hat делает это средствами dracut, своего генератора initramfs, и честно хвастается, что весь рецепт — пакет, строчка конфига и пара параметров ядра (на RHEL пакет тянется из EPEL, на Fedora он в основных репозиториях).

А теперь посмотрим, что для этого есть у альтоводов.

Альт не Рльех: make-initrd против dracut

В ALT Linux свой генератор initramfs, называется make-initrd. Это самостоятельная разработка, которую Альт использует с незапамятных времён. Устроен он концептуально иначе: образ собирается из «фич» (features), каждая фича — набор скриптов и файлов в /usr/share/make-initrd/features/. Нужна сеть в initramfs — есть фича network. Нужен SSH — есть фича dropbear. Нужен LUKS — есть фича luks.

Поддержка LUKS в make-initrd вынесена в отдельный пакет make-initrd-luks. При установке с шифрованием установщик сам ставит этот пакет, сам включает фичу luks в /etc/initrd.mk и сам записывает /etc/crypttab, а make-initrd исправно кладёт этот crypttab в образ.

Если пакета нет, указание отсутствующей фичи не вызывает ошибки — такое поведение задокументировано. Образ соберётся, но внутри не окажется ни cryptsetup, ни обработчика шифрованных разделов. Поэтому после установки пакета и каждой пересборки стоит внимательно смотреть на строку Used features: среди использованных фич должна быть luks.

Стенд: поднимаем сервак, который не жалко

Чтобы не экспериментировать на боевых машинах, я поднял в своём Proxmox виртуалку с Альт Сервер 11.1. В итоговой разметке получились отдельный /boot без шифрования (grub и initramfs должны читаться до ввода пароля), отдельный swap и корень внутри LUKS2-контейнера с aes-xts. Проще говоря, обычный, типовой сервер, только с шифрованием из коробки.

Первая же загрузка такой машины выглядит так. В initramfs работает событийная кухня: udev обнаруживает устройства, а демон ueventd передаёт их обработчикам. Обработчик 085-luks видит LUKS-раздел, заглядывает в скопированный в образ /etc/crypttab, не находит файла-ключа и запрашивает парольную фразу на консоли:

udev: session=5: @85-luks: Trying to ask the user for a password.
Enter passphrase for /dev/sda3:

И стоит. Загрузка дальше не идёт, потому что корень физически недоступен. Вот эту сцену мы и будем "лечить".

Рецепт

Шаг первый — пакеты. Как я уже говорил, на системе, изначально установленной с LUKS-корнем, make-initrd-luks уже стоит, а фича luks уже включена в /etc/initrd.mk. Остаётся поставить dropbear — маленький SSH-сервер (сама фича dropbear уже есть в базовом пакете make-initrd, но бинарник надо откуда-то взять) — и sysklogd: фича dropbear требует фичу syslog, а syslog без бинарников klogd/syslogd в образ не собирается:

$ apt-get install dropbear sysklogd

Теперь важная строчка в /etc/crypttab. Установщик пишет туда запись с именем вида luks-; мы дописываем опцию headless в четвёртую колонку:

luks-ВАШ-UUID   UUID=ВАШ-UUID   none    luks,headless

Этот флаг разбирает штатный обработчик 085-luks. Без headless он занимает консоль и запускает собственный cryptsetup, а потом наш второй cryptsetup по SSH начинает с ним толкаться локтями. С headless обработчик не спрашивает фразу на консоли и освобождает дорогу удалённой разблокировке. Цена очевидна: обычного запроса фразы на локальной консоли в этой загрузке тоже не будет, поэтому резервный вариант загрузки следует проверить заранее.

Шаг второй — отдельный ключ для разблокировки. Заводим его заранее и не смешиваем с основным:

$ ssh-keygen -t ed25519 -f ~/.ssh/unlock_key -N ''
$ scp ~/.ssh/unlock_key.pub user@сервер:/tmp/unlock_key.pub
$ ssh user@сервер "sudo -S sh -c 'mkdir -p /root/.ssh; cat /tmp/unlock_key.pub >> /root/.ssh/authorized_keys; chmod 700 /root/.ssh; chmod 600 /root/.ssh/authorized_keys'; rm /tmp/unlock_key.pub"

Шаг третий — конфиг /etc/initrd.mk. Эти строки добавляются к существующему файлу, не заменяют его. Строку про luks добавлять не нужно: на установке с шифрованием она там уже есть (если вы шифровали корень уже после установки — добавьте и её, вместе с пакетом make-initrd-luks):

FEATURES += network syslog dropbear
PUT_FILES += /etc/passwd /etc/shells /root/.ssh/authorized_keys
PUT_FILES += /etc/dropbear/dropbear_rsa_host_key /etc/dropbear/dropbear_ecdsa_host_key

Про сеть вопросов быть не должно, как и про /etc/passwd с /etc/shells в PUT_FILES по идее тоже. SSH-серверу нужно будет знать, под кем пускать.

Шаг четвёртый — host-ключи dropbear. Без них dropbear сгенерирует новые при каждой загрузке, и ваш ssh будет каждый раз истерить про смену ключа хоста:

mkdir -p /etc/dropbear
dropbearkey -t rsa -f /etc/dropbear/dropbear_rsa_host_key
dropbearkey -t ecdsa -f /etc/dropbear/dropbear_ecdsa_host_key

Шаг пятый — маленькая самодельная служба, без которой не будет интерактивного входа. В штатном образе make-initrd нет каталога /dev/pts, поэтому dropbear не может выдать PTY, и ssh -t отваливается с «PTY allocation request failed». Забавно, что rc-скрипт фичи dropbear уже пытается смонтировать devpts, но монтировать некуда: каталога-то нет. Лечится одним скриптом.

Свои каталоги для админских настроек make-initrd пока не завёз, но у него есть переменная PUT_DIRS: каталоги из неё копируются в образ целиком, вместе с деревом. Создаём нашу службу в /etc:

mkdir -p /etc/make-initrd/devpts/etc/rc.d/init.d

Файл /etc/make-initrd/devpts/etc/rc.d/init.d/devpts:

#!/bin/bash
### BEGIN INIT INFO
# Provides:          devpts
# Required-Start:    cmdline
# Default-Start:     3 4 5
### END INIT INFO

[ "$1" = start ] || exit 0

. /.initrd/initenv
. /etc/init.d/functions

mount_devpts()
{
    mkdir -p /dev/pts
    mount -t devpts devpts /dev/pts -o mode=0620
}

action_shell 'Mounting devpts:' mount_devpts

Не забудьте chmod +x и допишите в /etc/initrd.mk строку:

PUT_DIRS += /etc/make-initrd/devpts

Шаг шестой — сеть и таймер. Фича network настраивается параметром ядра ip=dhcp. И сразу же, той же строкой, добавьте rootdelay=600 (может пригодиться): оба параметра — в переменную GRUB_CMDLINE_LINUX_DEFAULT файла /etc/sysconfig/grub2.

Шаг седьмой — пересборка всего, что зависит от конфигов:

make-initrd
update-grub

В выводе make-initrd появится строка Used features — проверьте глазами, что там есть luks, network, syslog и dropbear. Дальше можно перезагружаться и смотреть.

Момент истины

Перезагружаемся и ждём какое-то время: в initramfs поднимаются сеть (DHCP) и dropbear. Подключаемся самым обычным образом:

ssh -t -o HostKeyAlias=server-initramfs -i ~/.ssh/unlock_key root@сервер

Благодаря службе из пятого шага получаем живое приглашение (initramfs)$ — полноценный интерактивный сеанс. Открываем контейнер:

cryptsetup open /dev/disk/by-uuid/ВАШ-LUKS-UUID ИМЯ-ИЗ-CRYPTTAB

UUID — из второй колонки crypttab, имя (у установщика оно вида luks-) — из первой. cryptsetup сам спросит парольную фразу и отключит эхо; по пути ввод шифрует SSH. Оговорка для параноиков: root на клиенте или в initramfs остаётся доверенной стороной, и от уже подменённого initramfs это не спасает.

Дальше всё происходит само: обработчики прогоняют fsck, монтируют корень, прощаются с dropbear — и наша SSH-сессия обрывается, а система грузится как ни в чём не бывало.

А теперь про rootdelay=600, который может пригодиться. По умолчанию initramfs ждёт корень 180 секунд. Не успели разблокировать — загрузка валится в аварийную оболочку, и дёшево это не лечится: rootdelayd намертво захватывает консольную блокировку, остальные обработчики виснут на ней же, и даже правильно введённая после дедлайна фраза уже ничего не оживит — только перезагрузка и новая попытка. В принципе 180 секунд вполне достаточно чтобы успеть ввести парольную фразу, но для надёжности интервал проще увеличить до rootdelay=600 (на общее время загрузки это всё равно не повлияет, если конечно поторопиться ввести фразу).

Проверяем после загрузки: findmnt -n -o SOURCE / отдаёт /dev/mapper/luks-ВАШ-UUID, cryptsetup status показывает активный LUKS2. Сервер поднялся, что не может не радовать.

Про безопасность, без неё никак

Список оговорок, идущий в комплекте с любым рецептом «SSH в раннюю загрузку».

Ваш публичный ключ и host-ключи лежат в initramfs, который лежит в /boot, который не шифруется. Кто-то с физическим доступом к диску получит и ключ, и возможность подменить загрузочную среду. От угрозы «диск вынули и унесли» это спасает; от угрозы «диск подменили и подслушивают фразу» — нет, тут нужны другие механизмы, от Secure Boot до выноса /boot на съёмном носителе.
Отдельный ключ для разблокировки — не гарантированная защита: запись в authorized_keys без принудительной команды даёт полноценный root в среде initramfs. Ограничение через forced command в dropbear я на стенде не проверял, поэтому вслух обещать не буду.

В known_hosts у машины появится два разных ключа: initramfs-dropbear и системный openssh. Два SSH-сервера — два ключа, всё честно. ssh при встрече второго устроит сцену про man-in-the-middle — не поддавайтесь: у ранней загрузки свой HostKeyAlias (в командах выше он уже прописан), а отпечаток при первом знакомстве сверяем по консоли. Строгую проверку не выключаем: лучше один раз посмотреть отпечаток глазами, чем потом гадать, кто это у нас там разблокировал сервер.

Заключение

Соберём в одну кучу. Два пакета, три штатные фичи, один самодельный скрипт с открытку, словечко headless в crypttab, rootdelay в параметрах ядра, отдельный ключ, вшитые passwd с shells и host-ключи, пересборка образа с загрузчиком. После перезагрузки вас встречает ssh -t со знакомым запросом фразы, дальше сервер справляется сам. Ручной работы вышло примерно как у редхатовцев; разница в том, что их последнюю милю за них уже написал пакет, а свою мы бахнули сами. Всего-лишь несколько строчек, между прочим (честно говоря на старте я думал, что решение будет куда сложнее).

И напоследок. DHCP в ранней загрузке иногда капризничает, headless запрещает серверу спросить вас на консоли, а таймер тикает при любых раскладах. Так что это, конечно, удобно, но не должно быть единственным ключом от машины. Так что резервный доступ (IPMI, консоль хостера, второй интерфейс со статикой) должен жить отдельно, а всю связку сначала гоняем на сервере, до которого можно дотянуться рукой.