UMGUM.COM 

LUKS + LibVirt ( Наладка шифрования виртуальных дисков системы "Qemu-KVM", с полностью автоматическим дешифрованием и запуском виртуальных машин при подаче ключа шифрования. )

12 июля 2018  (обновлено 10 июля 2020)

OS: "Linux Debian 7/8/9/10", "Linux Ubuntu Server 14/16/18 LTS".
Apps: "dm-crypt", LUKS, "LibVirt", "Bash".

Задача: наладить шифрование монолитных файлов или блочных устройств, используемых в качестве виртуальных дисков системы виртуализации "Qemu-KVM", с полностью автоматическим дешифрованием контейнеров и запуском виртуальных машин при подаче ключа шифрования посредством USB-flashdrive.

Поставленная задача весьма прикладная и преследуемая цель состоит в снижении риска потери данных в случае потери или кражи физических носителей информации. Здесь и сейчас нам достаточно того, чтобы случайный человек не смог получить доступ к нашим данным. Разумеется, от намеренного взлома через комбинации социальной инженерии, воровство аппаратных ключей или подмену загрузчика операционной системы это не спасёт - но давайте повышать уровень защиты поэтапно?

Для достижения наших целей в современном "Linux" могут подойти три встроенных в "ядро" инструмента:

1. "eCryptfs" - пофайловое шифрование содержимого произвольной файловой системы, с ключём шифрования данных, зашифрованным указанным пользователем паролем.
2. "cryptsetup" - шифрование блочного устройства (монолитного файла), с ключём шифрования данных, зашифрованным указанным пользователем паролем.
3. LUKS - надстройка для "cryptsetup", добавляющая возможность использования более одного пароля, посредством которого зашифрован ключ шифрования данных.

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

Второй вариант (с прямым использованием "cryptsetup") неудобен тем, что для шифрования "мастер-ключа" можно использовать только один пароль, утрата которого приведёт к невозможности получить доступ к зашифрованным данным, а выдача одного пароля нескольким хранителям увеличивает риски его компрометации.

Третий вариант (с использованием LUKS) наиболее гибок за счёт того, что "мастер-ключ" шифрования данных шифруется комбинацией паролей (в количестве до восьми штук), каждый из который в любой момент может быть заменён или удалён - потому здесь и сейчас мы выбираем LUKS.

размер: 320 400 640 800 1024 1280

Немаловажно, что формат разметки шифрованных томов "cryptsetup" и LUKS поддерживается широко распространёнными утилитами вроде "FreeOTFE", "TrueCrypt", "VeraCrypt" и "LibreCrypt", работающими в "MS Windows" - таким образом мы не привязываемся слишком жёстко к одной программной платформе.

Последовательность дальнейших действий такова:

1. Установка программного обеспечения;
2. Выбор наиболее быстрого алгоритма шифрования;
3. Создание блочного устройства и его шифрование;
4. Замена "ключей/паролей" LUKS-контейнера;
5. Применение в системе виртуализации "Qemu-KVM";
6. Наладка автоматического монтирования подключаемого USB-диска;
7. Наладка автоматической загрузки ключей шифрования с подключаемого USB-диска;
8. Наладка автоматической дешифровки LUKS-контейнеров и запуска виртуальных машин;
9. Как это работает (резюме).


Установка программного обеспечения.

Устанавливаем необходимые для работы APT-пакеты:

# apt-get install cryptsetup haveged

Выше рассматривались три варианта шифрования данных, где "cryptsetup" и LUKS будто бы выделялись в отдельные программные продукты. Я сделал это для наглядности. Фактически LUKS является развитием подсистемы шифрования "cryptsetup". Ранее LUKS распространялась как дополнение (в пакете "cryptsetup-luks", например), но в современном "Linux" все возможности LUKS представлены в комбинации утилит и подсистем "cryptsetup", "dm-crypt" и "device mapper".

Устанавливаемый в дополнение к утилитам шифрования APT-пакет "haveged" скорее всего пригодится в случае использования "cryptsetup" на современных промышленных серверах с низким уровнем аппаратной энтропии. Для шифрования требуется обильный поток из "/dev/random", который не может быстро "разогнаться" сразу после загрузки сервера. Специально для увеличения уровня энтропии, необходимого генератору случайных чисел, применяется подсистема HAVEGE (HArdware Volatile Entropy Gathering and Expansion).

Выбор наиболее быстрого алгоритма шифрования.

Прежде чем приступать к шифрованию действительно больших объёмов данных, есть смысл протестировать эффективность доступных методов и выбрать наиболее эффективный (самый быстрый и с наименьшим потреблением системных ресурсов).

Пример тестирования с результатами, показываемыми на одном их своих серверов производства 2018-го года выпуска:

# cryptsetup benchmark

....
# Algorithm   Key  Encryption   Decryption
....
    aes-cbc  256b 390,9 MiB/s 1273,9 MiB/s
serpent-cbc  256b  42,8 MiB/s  321,4 MiB/s
twofish-cbc  256b  96,8 MiB/s  176,9 MiB/s
    aes-xts  256b 976,3 MiB/s  980,5 MiB/s
serpent-xts  256b 312,4 MiB/s  320,1 MiB/s
twofish-xts  256b 173,7 MiB/s  174,4 MiB/s
....

Современные компьютеры оснащены модулями аппаратного вычисления примитивов стандарта AES, так что выбор практически всегда за этим алгоритмом (но в одном из моих старых ноутбуков самым быстрым оказался "serpent"):

# lsmod | grep -i aes

aesni_intel ...

Касательно режима оперирования блоками данных есть разные мнения, но XTS очевидно гораздо быстрее в разы, а схема эксплуатации потенциальной уязвимости с возможностью подмены порций данных в XTS крайне мало реалистична. Как правило, на производстве выбирают высокую скорость работы.

Если углубляться в чтение отчётов о сравнительном тестированием, то для набора мелких порций данных с коротким ключём шифрования режим CBC может оказаться быстрее XTS. Мы это и в "бенчмарке" выше видели. Однако, насколько я уловил, для больших блоков данных XTS и с 256-битным ключём в итоге выгоднее.

И, очень важно осознавать, что внедрение прослойки шифрования потока данных существенно снизит скорость работы системы, нагрузив центральный процессор (более или менее, в зависимости от наличия аппаратного AES-модуля) дополнительной работой, в целом уменьшив производительность на порядки от 10% до 50%, а также снизив скорость ввода/вывода при работе с файловой системой на порядки от 20% до 40%. Проще говоря, по реалистично-пессимистичным оценкам при переводе сервера с аппаратным модулем AES-вычислений на полное шифрование его заявленная производительность уменьшается наполовину.

Создание блочного устройства и его шифрование.

Создаём "разрежённый" файл для LUKS-контейнера:

# mkdir -p /var/lib/libvirt/images-crypted
# fallocate -l 32G /var/lib/libvirt/images-crypted/example.net-d0.raw.crypted

...или LVM-том:

# lvcreate --zero y --size 32G --name example.net-d0.crypted vg1

Заранее создаём текстовый файл с ключевой парольной фразой, которая будет использована только для первичного инициирования контейнера и добавления рабочих ключей (это ни в коему случае не "особенный" ключ или пароль - в LUSK все пользовательские ключи равноправны - но удобно объявить какой-то ключ "первым" и сохранить после его в особо секретной резервной копии):

# mkdir -p /root/lukskeys
# echo -n $(pwgen 64 1) > /root/lukskeys/luks-root.0.key
# chmod -R go-rwx /root/lukskeys

Размечаем LUKS-контейнер с явным указанием параметров шифрования, ранее выявленных как обеспечивающих оптимальную производительность (вместо указания на файл ключа можно добавить опцию "--verify-passphrase", и тогда пароль будет запрошен для ввода вручную):

# cryptsetup --verbose --cipher "aes-xts-plain64:sha256" --key-size 256 luksFormat /var/lib/libvirt/images-crypted/example.net-d0.raw.crypted /root/lukskeys/luks-root.0.key

Просматриваем данные созданного контейнера:

# cryptsetup luksDump /var/lib/libvirt/images-crypted/example.net-d0.raw.crypted | grep Slot

Key Slot 0: ENABLED
Key Slot 1: DISABLED
....
Key Slot 7: DISABLED

В LUKS-контейнере предусмотрено восемь слотов для хранения ключа/пароля, посредством которого осуществляется разблокировка контейнера. Один ключ/пароль должен всегда присутствовать. Для разблокировки контейнера можно применять любой из перечисленных в нём.

Создаём дополнительный ключевой файл (представляющий собой "бинарный" вариант хранения "парольной фразы"):

# dd if=/dev/random of=/root/lukskeys/luks-username.1.key bs=1 count=64

Считывание по одному байту шестидесяти четырёх блоков случайных данных сформирует в итоге гарантированно уникальную ключевую последовательность размером в 512 бит, которую практически невозможно ввести вручную, что впоследствии будет способствовать минимизации риска прочтения ключа с экрана или посредством "кейлогера".

Вносим дополнительную ключевую последовательность в перечень ключей LUKS-контейнера (используя при этом уже имеющийся ключевой файл для доступа к контейнеру):

# cryptsetup --key-file /root/lukskeys/luks-root.0.key luksAddKey /var/lib/libvirt/images-crypted/example.net-d0.raw.crypted /root/lukskeys/luks-username.1.key

Теперь в перечне ключей, которыми можно расшифровать содержимое LUKS-контейнера, появился второй:

# cryptsetup luksDump /var/lib/libvirt/images-crypted/example.net-d0.raw.crypted | grep Slot

Key Slot 0: ENABLED
Key Slot 1: ENABLED
Key Slot 2: DISABLED
....

Открываем (разблокируем) LUKS-контейнер и публикуем его как произвольное блочное устройство:

# cryptsetup --key-file /root/lukskeys/luks-username.1.key luksOpen /var/lib/libvirt/images-crypted/example.net-d0.raw.crypted luks-example.net-d0.raw.decrypted

В перечне блочных устройств обнаруживаем наше:

# lsblk -p

NAME                                            MAJ:MIN RM SIZE RO TYPE
/dev/loop0                                        7:0    0   2G  0 loop
└─/dev/mapper/luks-example.net-d0.raw.decrypted 253:4    0   2G  0 crypt
....

Ядерный модуль "dm-crypt" размещает блочные устройства разблокированных LUKS-контейнеров в директории "/dev/mapper", и настройке это поведение не подлежит. А нам нужно, чтобы блочные устройства виртуальных дисков были видны в директории, выделенной системе виртуализации в качестве хранилища. Сделаем это посредством переопределения прав доступа и создания символической ссылки:

# chown libvirt-qemu:kvm $(readlink -f "/dev/mapper/luks-example.net-d0.raw.decrypted")
# ln -s /dev/mapper/luks-example.net-d0.raw.decrypted /var/lib/libvirt/images/example.net-d0.raw.decrypted

Важно не забывать не хранить ключи шифрования на одном компьютере с зашифрованными этими ключами данными. Нужно обязательно переносить ключи шифрования в надёжное место, удаляя их с компьютера, где размещены защифрованные файлы:

# rm -rf /root/lukskeys

Замена "ключей/паролей" в LUKS-контейнере.

Надстройка LUKS предоставляет простые и удобные инструменты для манипуляции пользовательскими ключами шифрования, которые используются для шифрования "мастер-ключа".

Единственный способ идентификации ключа при манипуляции ими посредством утилит LUKS - порядковый номер "слота" (конечно, сопоставить ключи можно по hash-сумме - но в качестве адреса всё равно придётся использовать порядковый идентификатор). Нет ничего похожего на поддержку "символических имён". Чтобы как-то связать номер "слота" LUKS-контейнера и пользователя, за которым закреплён сохранённый там ключ, я обычно именую выдаваемые пользователям файлы ключей с указанием номера "cлота" (например: "luks-username.1.key", "luks-username.2.key").

Выше мы уже просматривали усечённый вывод сведений о зашифрованном контейнере, интересуясь занятостью "слотов" ключей шифрования - их номера и служат идентификаторами:

# cryptsetup luksDump /var/lib/libvirt/images-crypted/example.net-d0.raw.crypted | grep Slot

Key Slot 0: ENABLED
Key Slot 1: ENABLED
Key Slot 2: DISABLED
....

Зачищаем содержимое слота номер "1" LUKS-контейнера:

# cryptsetup --key-file /root/lukskeys/luks-root.0.key luksKillSlot /var/lib/libvirt/images-crypted/example.net-d0.raw.crypted 1

Добавляем в слот номер "1" произвольный пользовательский ключ шифрования:

# cryptsetup --key-file /root/lukskeys/luks-root.0.key --key-slot 1 luksAddKey /var/lib/libvirt/images-crypted/example.net-d0.raw.crypted /root/lukskeys/luks-username.1.key

Возможностей манипуляций ключами поболее приведённых в этом примере - но эти самые востребованные, в моей практике.

Резервное копирование "мастер-ключа" LUKS-контейнера.

Как выше неоднократно упоминалось, для непосредственного шифрования содержимого данных используется некий "master-key", хранящийся в "мета-данных" LUKS-контейнера в зашифрованном виде. Вводимые "парольные фразы" или ключевые файлы используются лишь для расшифровки "master-key". Сам ключ можно подсмотреть в перечне параметров подключения разблокированного LUKS-контейнера (доступно только суперпользователям):

# dmsetup --showkeys table /dev/mapper/luks-example.net-d0.raw.decrypted

Также "master-key" в отрытом виде можно получить посредством специального запроса сведений о неразблокированном LUKS-контейнере:

# cryptsetup --key-file /root/lukskeys/luks-root.0.key luksDump --dump-master-key /var/lib/libvirt/images-crypted/example.net-d0.raw.crypted

Очень желательно иметь резервную копию заголовочной части LUKS-контейнера, которая может пригодится в случае повреждение области "мета-данных" контейнера (при ручных манипуляциях с разметкой или сторонним ПО, не знающем формата контейнера):

# cryptsetup luksHeaderBackup /var/lib/libvirt/images-crypted/example.net-d0.raw.crypted --header-backup-file ./backup/example.net-d0.raw.crypted.header

(опционально) Проверяем работоспособность расшифрованного блочного устройства.

Для решения поставленной задачи описываемые в этом параграфе действия не требуются, но ради понимая процесса один раз можно проверить работоспособность расшифрованного блочного устройства и таким образом.

Создаём желаемую файловую систему в блочном устройстве:

# mkfs -t ext4 /dev/mapper/luks-example.net-d0.raw.decrypted

Просматриваем перечень блочных устройств и файловых систем, обнаруживая там наши:

# lsblk -f

Монтируем файловую систему расшифрованного блочного устройства в произвольное место несущей файловой системы:

# mkdir -p /mnt/example.net-d0.decrypted
# mount /dev/mapper/luks-example.net-d0.raw.decrypted /mnt/example.net-d0.decrypted

Проверяем работоспособность файловой системы любым удобным способом, после его размонтируем её и отключим расшифрованное блочное устройство, заблокировав тем самым доступ к содержимому шифрованного LUKS-контейнера.

# umount /mnt/example.net-d0.decrypted
# cryptsetup luksClose /dev/mapper/luks-example.net-d0.raw.decrypted
# sync

(опционально) Налаживаем автоматическое разблокирование LUKS-контейнера.

Так же, как и в примере выше, для решения поставленной задачи описываемые в этом параграфе действия не требуются, но знание о способах применения технологии не повредят.

Если при запуске операционной системы предусмотрен способ безопасной подачи внутрь ключа шифрования, то можно настроить автоматическое разблокирование LUKS-контейнера с последующим монтированием файловой системы.

Пример конфигурации для автоматического разблокирования LUKS-контейнера на этапе запуска операционной системы:

# vi /etc/crypttab

luks-example.net-d0.raw.decrypted /var/lib/libvirt/images-crypted/example.net-d0.raw.crypted /root/lukskeys/luks-username.1.key luks

Пример конфигурации для автоматического монтирования файловой системы из разблокированного LUKS-контейнера на этапе запуска операционной системы:

# vi /etc/fstab

....
/dev/mapper/luks-example.net-d0.raw.decrypted /mnt/example.net-d0.decrypted ext4 defaults 0 0

О применении шифрованных блочных устройств в системе виртуализации "Qemu-KVM".

Файл блочного устройства, полученный при разблокировании LUKS-контейнера может напрямую использоваться системой виртуализации.

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

# dd if=/var/lib/libvirt/images/example.net-d0.raw of=/dev/mapper/luks-example.net-d0.raw.decrypted bs=8M

Очевидно, что после того, как виртуальной машине был выдан в пользование виртуальный диск, нуждающийся в расшифровке перед использованием, автозапуск таковой следует выключить, чтобы какими-то манипуляциями вначале разблокировать LUKS-контейнер, и только потом явно запускать зависимую от него виртуальную машину. В "Qemu-KVM + LibVirt" автозапуск выключается удалением символической ссылки на файл конфигурации из специальной директории:

# rm /etc/libvirt/qemu/autostart/example.net.xml

Для визуализации статуса в описание виртуальной машины я добавляю сведения о том, что диски её шифрованы и автозапуск отключен:

# vi /etc/libvirt/qemu/example.net.xml

<domain type='kvm'>
  <name>example.net</name>
  <title>example.net [encrypted volume, manual running]</title>
  ....

Наладка автоматического монтирования подключаемого USB-диска.

Выше мы рассматривали этапы подготовки LUKS-контейнеров и варианты использования их в "ручном" режиме. Далее нам предстоит настроить автоматическую загрузку ключей шифрования с произвольного носителя, в данном случае - USB-флешки.

На рабочих станциях под управлением "Linux" автоматизацией монтирования подключаемых устройств занимаются подсистемы, интегрированные в "оконные менеджеры". На серверах автоматическое монтирование обычно не предусмотренно. Проще всего для монтирования файловых систем USB-устройств воспользоваться утилитой "usbmount", которая автоматически запускается подсистемой UDEV, а после монтирования и сама может запустить произвольное приложение:

# apt-get install usbmount

Простыми и очевидными опциями описываем правила автоматического монтирования, указывая реагировать только на "флешки" с одним типом файловой системы:

# vi /etc/usbmount/usbmount.conf

ENABLED=1
MOUNTPOINTS="/media/usb0 /media/usb1 /media/usb2 /media/usb3"
FILESYSTEMS="vfat"
MOUNTOPTIONS="sync,noexec,nodev,noatime,nodiratime"
FS_MOUNTOPTIONS="-fstype=vfat,ro,umask=177,codepage=866,iocharset=utf8"
VERBOSE=no

Для красоты удаляем избыточные директории точек монтирования утилиты "usbmount":

# rm -r /media/usb4 /media/usb5 /media/usb6 /media/usb7

В "Ubuntu 18.04" привычный "udev" заменили на "systemd-udevd", в котором ввели суровую изоляцию ресурсов посредством "namespace". В результате смонтированные в контексте "systemd-udevd" ресурсы не видны на общесистемном уровне. Самый простой способ обойти это поведение - отключить изоляцию:

# mkdir -p /etc/systemd/system/systemd-udevd.service.d
# vi /etc/systemd/system/systemd-udevd.service.d/override.conf

[Service]
MountFlags=shared

# systemctl daemon-reload
# systemctl restart systemd-udevd

Наладка автоматической загрузки ключей шифрования с подключаемого USB-диска.

Напишем простейший bash-скрипт, копирующий пользовательские ключи шифрования с подключаемого носителя:

# mkdir -p /usr/local/etc/luks
# vi /usr/local/etc/luks/auto-lukskey-load.sh && chmod a+x /usr/local/etc/luks/auto-lukskey-load.sh

#!/bin/bash

# Создаём директорию для временного размещения ключей шифрования
# (директория в файловой системе в ОЗУ - содержимое не сохраняется при перезапуске системы)
mkdir -p /dev/shm/lukskeys

# Находим ключи шифрования и копируем им в предусмотренное место файловой системы
find /media/ -type f -iname 'luks-*.key' -exec cp {} /dev/shm/lukskeys/ \;
chmod -R go-rwx /dev/shm/lukskeys

# Запускаем скрипт расшифровки LUKS-контейнеров и запуска зависимых от них виртуальных машин
/usr/local/etc/luks/decrypt-run-vms.sh

# Зачищаем содержимое директории с ключами шифрования
# (процедура дублируется - но лучше перестараться)
rm -rf /dev/shm/lukskeys/*

exit $?

Налаживаем автоматический запуск скрипта посредством "usbmount", сразу после успешного монтирования любого USB-диска:

# ln -s /usr/local/etc/luks/auto-lukskey-load.sh /etc/usbmount/mount.d/01_auto-lukskey-load

Капризный "usbmount" запускает свои "хуки" посредством отдельной подсистемы "run-parts", ожидающей файлы с именами определённого формата, вроде "01_abc" - так что важно проверить, будет ли принят к исполнению наш скрипт:

# run-parts --test /etc/usbmount/mount.d

/etc/usbmount/mount.d/00_create_model_symlink
/etc/usbmount/mount.d/01_auto-lukskey-load

Наладка автоматической дешифровки LUKS-контейнеров и запуска виртуальных машин.

Мы подошли к последнему этапу - наладке автоматизации использования ранее подготовленных LUKS-контейнеров в качестве виртуальных дисков виртуальных машин "Qemu-KVM".

Для управления виртуальными машинами через "LibVirt" и доступа к описаниям в XML-конфигурациях установим необходимые APT-пакеты:

# apt-get install libvirt-clients libxml2-utils

Сопоставим перечень виртуальных машин с назначенными им виртуальными дисками, указав исходный шифрованный LUKS-контейнер и целевое имя файла виртуального диска:

# vi /usr/local/etc/luks/decrypt-run-vms.xml

<vms>
  <vm name='example.net'>
    <luks>
      <disk source='/var/lib/libvirt/images-crypted/example.net-d0.raw.crypted'
            target='/var/lib/libvirt/images/example.net-d0.raw.decrypted' />
    </luks>
  </vm>
  <vm name='example.com'>
    <luks>
      <disk source='/var/lib/libvirt/images-crypted/example.com-d0.raw.crypted'
            target='/var/lib/libvirt/images/example.com-d0.raw.decrypted' />
      <disk source='/var/lib/libvirt/images-crypted/example.com-dX.raw.crypted'
            target='/var/lib/libvirt/images/example.com-dX.raw.decrypted' />
      ....
    </luks>
  </vm>
  ....
</vms>

Напишем bash-скрипт, пытающийся расшифровать LUKS-контейнеры, опубликовать их в указанном месте и запустить соответствующие им виртуальные машины, опираясь на сведения из вышеприведённого конфигурационного файла:

# vi /usr/local/etc/luks/decrypt-run-vms.sh && chmod a+x /usr/local/etc/luks/decrypt-run-vms.sh

#!/bin/bash

# Не допускаем запуск этого скрипта, если он уже работает
# (на этапе старта кратковременно наблюдается два процесса: инициализирующий и исполнения)
STATE="$(ps wax | grep --invert-match grep | grep --ignore-case --count 'decrypt-run-vms.sh')"
[ ! -z "${STATE}" -a "${STATE}" -le "2" ] || { echo "Script is already running. Necessary to wait for the completion of the previous launch."; exit 0; }

# Задаём набор служебных констант
CONF="/usr/local/etc/luks/decrypt-run-vms.xml"
LOG="/var/log/decrypt-run-vms.log"
DATE=`date +"%Y-%m-%d %H:%M:%S"`

# Проверяем наличие ожидаемых приложений и утилит
[ -x "$(command -v xmllint)" ] && [ -x "$(command -v virsh)" ] || { echo "There are no necessary applications and utilities for work."; exit 1; }

# Прерываем работу в отсутствии ключей шифрования
# (ожидаем имена файлов начинающиеся с "luks-" и завершающиеся ".key")
[ ! -f /dev/shm/lukskeys/luks-*.key ] && { echo "${DATE}: Missing encryption keys in directory \"/dev/shm/lukskeys\"." | tee -a "${LOG}"; exit 1; }

# Перебираем перечень виртуальных машин
VMCOUNT="$(xmllint --xpath 'count(/vms/vm)' ${CONF})"
for (( I=1; I<=$VMCOUNT; I++ )) ; do
  FORTH=false

  # Вычленяем имя виртуальной машины
  VMNAME=`xmllint --xpath "string(/vms/vm[$I]/@name)" "${CONF}"`
  [ -z "${VMNAME}" ] && { echo "The virtual machine name cannot be empty. Operation aborted."; continue; }

  # Проверяем наличие уже запущенной указанной виртуальной машины
  STATE="$(virsh list --name --state-running | grep -i -c ${VMNAME})"
  [ "${STATE}" -eq "0" ] || { echo "${DATE}: Virtual machine \"${VMNAME}\" is already up and running. Operation aborted." | tee -a "${LOG}"; continue; }

  # Перебираем перечень виртуальных дисков
  VDCOUNT=`xmllint --xpath "count(/vms/vm[$I]/luks/disk)" "${CONF}"`
  for (( II=1; II<=$VDCOUNT; II++ )) ; do

    # Вычленяем адреса файлов LUKS-контейнера и диска виртуальной машины
    VDSOURCE=`xmllint --xpath "string(/vms/vm[$I]/luks/disk[$II]/@source)" "${CONF}"`
    VDTARGET=`xmllint --xpath "string(/vms/vm[$I]/luks/disk[$II]/@target)" "${CONF}"`
    VDTNAME="$(basename ${VDTARGET})"
    [ -z "${VDSOURCE}" -o -z "${VDTARGET}" -o -z "${VDTNAME}" ] && { echo "File resource address parameters cannot be empty. Operation aborted."; continue; }

    # Проверяем наличие LUKS-контейнера для расшифровки
    [ ! -e "${VDSOURCE}" ] && { echo "${DATE}: There is no LUKS container for decryption. Operation aborted." | tee -a "${LOG}"; continue; }

    # Проверяем наличие уже расшифрованного блочного устройства
    if [ ! -b "/dev/mapper/luks-${VDTNAME}" ] ; then

      # Перебираем предоставленные ключи шифрования
      # (ожидаем имена файлов начинающиеся с "luks-" и завершающиеся ".key")
      cd "/dev/shm/lukskeys"
      for KEY in $(ls --format=single-column | grep --extended-regexp "^luks-.*\.key$") ; do

        # Пробуем открыть LUKS-контейнер
        cryptsetup --key-file "${KEY}" luksOpen "${VDSOURCE}" "luks-${VDTNAME}"
        if [ "$?" -eq "0" ] ; then
          FORTH=true
          chown libvirt-qemu:kvm $(readlink -f "/dev/mapper/luks-${VDTNAME}")
          echo "${DATE}: Container \"${VDSOURCE}\" successfully decrypted with a key \"${KEY}\"." | tee -a "${LOG}"
          break
        else
          echo "${DATE}: Key \"${KEY}\" is not suitable to decrypt the container \"${VDSOURCE}\"." | tee -a "${LOG}"
        fi
      done

    else
      echo "${DATE}: An decrypted container file was found at the address \"/dev/mapper/luks-${VDTNAME}\"." | tee -a "${LOG}"
    fi

    # Проверяем корректность ссылки на расшифрованное блочное устройство и удаляем неверную
    [ -e "${VDTARGET}" -a ! -b "$(readlink -f ${VDTARGET})" ] && { rm -f "${VDTARGET}"; }

    # Создаём ссылку на расшифрованное блочное устройство по указанному адресу
    ls "${VDTARGET}" 1>/dev/null 2>&1
    if [ "$?" -ne "0" -a -b "/dev/mapper/luks-${VDTNAME}" ] ; then
      ln -s "/dev/mapper/luks-${VDTNAME}" "${VDTARGET}"
      echo "${DATE}: For decrypted container \"/dev/mapper/luks-${VDTNAME}\" established link \"${VDTARGET}\"." | tee -a "${LOG}"
    fi
  done

  # Пробуем запустить указанную виртуальную машину
  if [ "${FORTH}" == "true" ] ; then
    virsh start "${VMNAME}"
    if [ "$?" -eq "0" ] ; then
      echo "${DATE}: Virtual machine \"${VMNAME}\" successfully running." | tee -a "${LOG}"
    else
      echo "${DATE}: Failure to boot the \"${VMNAME}\" virtual machine." | tee -a "${LOG}"
    fi
  fi
done

# Зачищаем содержимое директории с ключами шифрования
# (процедура дублируется - но лучше перестараться)
rm -rf /dev/shm/lukskeys/*

exit $?

Как это работает.

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

Логика загрузки ключей следующая:

1. Все подключения USB-устройств отслеживаются подсистемой UDEV в соответствии с конфигурацией "/lib/udev/rules.d/usbmount.rules" и автоматически запускается скрипт "/usr/share/usbmount/usbmount".
2. Если подключаемое устройство USD-flashdrive, то в соответствии с конфигурацией "/etc/usbmount/usbmount.conf" его файловая система монтируется в иерархии "/media/usb".
3. После успешного монтирования файловой системы USB-flashdrive скриптом "usbmount" запускается набор скриптов в директории "/etc/usbmount/mount.d" - среди них ссылка на скрипт загрузки ключей шифрования "/usr/local/etc/luks/auto-lukskey-load.sh".
4. Скрипт элементарно ищет в иерархии "/media/usb" все файлы с именами, подходящими под ключ шифрования (начинающиеся с "luks-" и заканчивающиеся ".key"), и копирует их в создаваемую при необходимости директорию "/dev/shm/lukskeys", располагаемую в оперативной памяти компьютера (не сохраняется при перезапуске операционной системы).
5. После копирования ключей автоматически запускается скрипт "/usr/local/etc/luks/decrypt-run-vms.sh" этапа расшифровки дисков и запуска виртуальных машин.

Очевидно, что можно вручную подложить ключ шифрования в директорию "/dev/shm/lukskeys" и запустить скрипт "/usr/local/etc/luks/decrypt-run-vms.sh" (без аргументов), минуя этап автоматической загрузки ключей с USB-flashdrive.

Логика расшифровки и запуска виртуальных машин следующая:

1. Скрипт "/usr/local/etc/luks/decrypt-run-vms.sh" опирается в работе на конфигурацию "/usr/local/etc/luks/decrypt-run-vms.xml", где перечислены управляемые им виртуальные машины и расшифровываемые виртуальные диски.
2. Скриптом осуществляется вначале перебор всех указанных виртуальных машин, для каждой виртуальной машины перебираются все указанные виртуальные диски, каждый из которых скрипт пытается расшифровать поочерёдно (до первого успеха) всеми ключами шифрования, обнаруженными в директории "/dev/shm/lukskeys".
3. После расшифровки виртуальных дисков осуществляется попытка запустить их виртуальную машину.
4. Сведения о результатах операций записывается в журнал событий "/var/log/decrypt-run-vms.log".
5. После завершения процедур расшифровки и запуска все ключи шифрования из директории "/dev/shm/lukskeys" автоматически удаляются.

Таким образом, внесённые в существующую схему взаимодействий изменения сосредоточены в четырёх файлах, что позволяет счесть схему несложной:

1. /etc/usbmount/usbmount.conf
2. /usr/local/etc/luks/auto-lukskey-load.sh
3. /usr/local/etc/luks/decrypt-run-vms.sh
4. /usr/local/etc/luks/decrypt-run-vms.xml


Заметки и комментарии к публикации:


Оставьте свой комментарий ( выразите мнение относительно публикации, поделитесь дополнительными сведениями или укажите на ошибку )