Хабрахабр

Автоматическое разблокирование корневого LUKS-контейнера после горячей перезагрузки

Понятное дело, чтобы никто не украл с диска фотографии их любимых домашних котиков! Зачем вообще люди шифруют диски своих персональных компьютеров, а иногда — и серверов? Убрать бы ее, чтобы хотя бы иногда не приходилось ее набирать. Вот только незадача: зашифрованный диск требует при каждой загрузке ввести с клавиатуры ключевую фразу, а она длинная и скучная. Да так, чтобы смысл от шифрования не потерялся.

Котейка для привлечения внимания

Котейка

Можно вместо нее сделать ключевой файл на флешке, и он тоже будет работать. Ну, совсем убрать ее не получится. Если повезло с BIOS, почти можно! А без флешки (и без второго компьютера в сети) можно? Под катом будет руководство, как настроить шифрование диска через LUKS вот с такими свойствами:

  1. Ключевая фраза или ключевой файл нигде не хранится в открытом виде (или в виде, эквивалентном открытому) при выключенном компьютере.
  2. При первом включении компьютера требуется ввести ключевую фразу.
  3. При последующих перезагрузках (до выключения) ключевую фразу вводить не требуется.

6, Ubuntu 19. Инструкции проверены на CentOS 7. 1 в виртуальных машинах и на реальном железе (десктоп, ноутбук и два сервера). 04 и openSUSE Leap 15. Они должны работать и на других дистрибутивах, в которых есть работоспособная версия Dracut.

И да, по-хорошему, это должно было бы попасть в хаб "ненормальное системное администрирование", но такого хаба нет.

Я предлагаю задействовать отдельный слот LUKS-контейнера и хранить ключ от него… в оперативной памяти!

Что еще за слот?

Полезные данные на диске зашифрованы симметричным шифром, как правило aes-xts-plain64. LUKS-контейнер реализует многоуровневое шифрование. Мастер-ключ хранится в зашифрованном виде, в общем случае — в нескольких копиях (слотах). Ключ от этого симметричного шифра (мастер-ключ) генерируется на этапе создания контейнера как случайная последовательность байт. Каждый активный слот имеет отдельную ключевую фразу (или отдельный ключевой файл), с помощью которой можно расшифровать мастер-ключ. По умолчанию, активен только один из восьми слотов. В нашем случае, с помощью ключевой фразы (слот 0) или с помощью участка памяти, используемого как ключевой файл (слот 6). С точки зрения пользователя, получается, что разблокировать диск можно с помощью любой из нескольких различных ключевых фраз (или ключевых файлов).

86B. BIOS на большинстве материнских плат при перезагрузке не чистит память, или можно настроить, чтобы не чистил (известное исключение: "Intel Corporation S1200SP/S1200SP, BIOS S1200SP. 01. 03. 013020190050 01/30/2019"). 0042. При выключении питания содержимое оперативной памяти само через некоторое время стирается, вместе с незащищенной копией ключа. Поэтому там можно хранить ключ.

Итак, поехали.

Шаг первый: установить систему на зашифрованный с помощью LUKS диск

Файловая система на зашифрованном разделе может быть любой, можно еще использовать LVM, чтобы в одном контейнере были и корневая файловая система, и том для swap'а, и все остальное — кроме /boot. При этом раздел диска (например, /dev/sda1), монтируемый в /boot, должен остаться незашифрованным, а другой раздел, на котором будет все остальное (например, /dev/sda2) нужно зашифровать. SUSE делает все по-другому (шифрует /boot) и поэтому требует ручного разбиения диска. Это соответствует разбиению диска по умолчанию в CentOS 7 и в Debian при выборе опции шифрования.

В итоге должно получиться примерно следующее:

$ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sda 8:0 0 10G 0 disk ├─sda1 8:1 0 1G 0 part /boot
└─sda2 8:2 0 9G 0 part └─luks-d07a97d7-3258-408c-a17c-e2fb56701c69 253:0 0 9G 0 crypt ├─centos_centos--encrypt2-root 253:1 0 8G 0 lvm / └─centos_centos--encrypt2-swap 253:2 0 1G 0 lvm [SWAP]

В случае использования UEFI будет еще раздел EFI System Partition.

Для пользователей Debian и Ubuntu: необходимо заменить пакет initramfs-tools на dracut.

# apt install --no-install-recommends dracut

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

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

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

# touch -m 0600 /root/key
# head -c16 /dev/urandom > /root/key

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

# cryptsetup luksAddKey --key-slot=6 --iter-time=1 /dev/sda2 /root/key
Enter any existing passphrase:

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

Шаг третий: выделить место в физической памяти для хранения ключа

Это linux/drivers/char/mem.c, отвечающий в том числе за устройство /dev/mem, а также модули phram (эмулирует MTD-чип, дает устройство /dev/mtd0) и nd_e820 (применяется при работе с NVDIMM, дает /dev/pmem0). В Linux есть по меньшей мере три разных драйвера, позволяющих обращаться к физической памяти по известному адресу. У них у всех есть свои неприятные особенности:

  • /dev/mem недоступен для записи при использовании Secure Boot, если дистрибутив применил набор патчей LOCKDOWN от Matthew Garrett (а этот набор патчей является обязательным, если дистрибутив собирается поддерживать Secure Boot с загрузчиком, подписанным Microsoft);
  • phram недоступен в CentOS и Fedora — мейнтейнер просто не включил соответствующую опцию при сборке ядра;
  • nd_e820 требует зарезервировать не менее 128 мегабайт памяти — так работает NVDIMM. Но это единственный вариант, работающий в CentOS с Secure Boot.

Поскольку идеального варианта нет, далее рассматриваются все три.

В особенности это касается компьютеров, в которых уже есть MTD-чипы или NVDIMM-модули. При использовании любого из способов нужна предельная аккуратность, чтобы случайно не затронуть устройства или диапазоны памяти, отличные от нужного. Также может сбиться нумерация существующих устройств, на которую полагаются конфигурационные файлы и скрипты. А именно, /dev/mtd0 или /dev/pmem0 может оказаться не тем устройством, которое соответствует зарезервированному для хранения ключа участку памяти. Соответственно, все сервисы, полагающиеся на существующие устройства /dev/mtd* и /dev/pmem*, рекомендуется временно отключить.

Нас интересуют два вида этой опции: Резервирование физической памяти в Linux осуществляется путем передачи ядру опции memmap.

  • memmap=4K$0x10000000 резервирует (т.е. помечает как reserved, чтобы ядро само не использовало) 4 килобайта памяти, начиная с физического адреса 0x10000000;
  • memmap=128M!0x10000000 помечает 128 мегабайт физической памяти, начиная с адреса 0x10000000, как NVDIMM (очевидно, фальшивый, но нам и такой сгодится).

При использовании $ начальный адрес зарезервированной области памяти должен быть кратным 0x1000 (т.е. Вариант с $ подходит для использования с /dev/mem и phram, вариант с ! — для nd_e820. 128 мегабайтам). 4 килобайтам), при использовании ! — кратным 0x8000000 (т.е.

Причем двойному: один раз — при генерации grub.cfg из /etc/default/grub, второй раз — при интерпретации получившегося конфигурационного файла на этапе загрузки. Важно: знак доллара ($) в конфигурационных файлах GRUB является спецсимволом и подлежит экранированию. в /etc/default/grub в итоге должна появиться такая строка: Т.е.

GRUB_CMDLINE_LINUX="memmap=4K\\\$0x10000000 ... остальные опции..."

С восклицательным знаком таких сложностей нет: Без двойного экранирования знака $ система просто не загрузится, так как будет думать, что у нее всего 4 килобайта памяти.

GRUB_CMDLINE_LINUX="memmap=128M!0x10000000 ... остальные опции..."

Карта физической памяти (а она нужна, чтобы выяснить, какие адреса резервировать) доступна пользователю root в псевдофайле /proc/iomem:

# cat /proc/iomem
...
000f0000-000fffff : reserved 000f0000-000fffff : System ROM
00100000-7ffddfff : System RAM 2b000000-350fffff : Crash kernel 73a00000-7417c25e : Kernel code 7417c25f-747661ff : Kernel data 74945000-74c50fff : Kernel bss
7ffde000-7fffffff : reserved
80000000-febfffff : PCI Bus 0000:00 fd000000-fdffffff : 0000:00:02.0
...

Угадать, какую часть памяти BIOS при перезагрузке не трогает, заранее надежно не получится. Оперативная память отмечена как "System RAM", нам достаточно зарезервировать одну ее страницу для хранения ключа. Поэтому в общем случае придется действовать методом проб и ошибок. Разве что если есть еще один компьютер с точно такой же версией BIOS и с такой же конфигурацией памяти, на котором данное руководство уже пройдено. Обычно достаточно отступить на 128 мегабайт (0x8000000) от краев. Как правило, BIOS при перезагрузке изменяет данные только в начале и в конце каждого диапазона памяти. 0x10000000) работают. Для виртуальных машин KVM с 1 GB памяти и более, предложенные варианты (memmap=4K$0x10000000 и memmap=128M!

Параметр называется phram.phram и содержит три части: название (произвольное до 63 символов, будет видно в sysfs), начальный адрес и длину. При использовании модуля phram нужен еще один параметр командной строки ядра, который, собственно, и указывает модулю, какой кусок физической памяти использовать — наш, зарезервированный. Начальный адрес и длина должны быть такими же, как и в memmap, но суффиксы K и M не поддерживаются.

GRUB_CMDLINE_LINUX="memmap=4K\\\$0x10000000 phram.phram=savedkey,0x10000000,4096 ..."

Правильная команда для этого зависит от дистрибутива. После редактирования /etc/default/grub необходимо перегенерировать настоящий конфигурационный файл, который читает GRUB при загрузке.

# grub2-mkconfig -o /boot/grub2/grub.cfg # CentOS (Legacy BIOS)
# grub2-mkconfig -o /boot/efi/EFI/centos/grub.cfg # CentOS (UEFI)
# update-grub # Debian, Ubuntu
# update-bootloader --reinit # SUSE

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

Шаг четвертый: настроить LUKS на чтение ключа из памяти

Каждая его строка состоит из четырех полей: Настройки шифрования дисков хранятся в файле /etc/crypttab.

  • устройство, которое должно получиться при разблокировке,
  • зашифрованное устройство,
  • откуда брать ключевой файл (none означает ввод ключевой фразы с клавиатуры),
  • необязательное поле для опций.

Что, собственно, и потребуется при первой загрузке. Если ключевой файл существует, но не подходит, то Dracut спрашивает ключевую фразу.

Пример файла /etc/crypttab из свежеустановленного дистрибутива:

# cat /etc/crypttab # до редактирования
luks-d07....69 UUID=d07....69 none

Т.е. Ключевым файлом в нашем случае будет кусок физической памяти. Опции нужны для того, чтобы указать, какой кусок файла является ключом. /dev/mem, /dev/mtd0 или /dev/pmem0, в зависимости от выбранной технологии доступа к памяти.

# cat /etc/crypttab # после редактирования
# При использовании /dev/mem:
luks-d07....69 UUID=d07....69 /dev/mem keyfile-offset=0x10000000,keyfile-size=16
# При использовании phram:
luks-d07....69 UUID=d07....69 /dev/mtd0 keyfile-size=16
# При использовании nd_e820:
luks-d07....69 UUID=d07....69 /dev/pmem0 keyfile-size=16

Вот только просто так это работать не будет.

А именно, он берет устройство из третьей колонки и ждет, когда соответствующий ему device unit станет активным. Дело в том, как systemd определяет, когда можно разблокировать устройство. Но device unit — это не то же самое, что само устройство. Вроде логично: не имеет смысла пытаться разблокировать LUKS-контейнер, пока не появилось устройство с ключевым файлом. Устройства /dev/mem и /dev/mtd0 являются посимвольными, поэтому по умолчанию не отслеживаются и никогда не будут признаны готовыми. Systemd по умолчанию создает device unit'ы только для устройств ядра, относящихся к подсистемам поблочных устройств и сетевых интерфейсов.

Придется подсказать systemd, что он должен их отслеживать, путем создания правил udev в файле /etc/udev/rules.d/99-mem.rules:

# /dev/mem
KERNEL=="mem", TAG+="systemd"
# /dev/mtd*
KERNEL=="mtd*", TAG+="systemd"
# Устройства /dev/pmem* являются поблочными и отслеживаются по умолчанию

Шаг пятый: перегенерировать initramfs

В том числе те, где он не используется по умолчанию, но доступен и работоспособен. Напоминаю: в статье рассматриваются только дистрибутивы, использующие Dracut.

А еще — чтобы включить туда дополнительные модули ядра и правила udev. Перегенерировать initramfs нужно, чтобы обновить там файл /etc/crypttab. За включение и загрузку дополнительных модулей ядра отвечает параметр конфигурации Dracut force_drivers, за дополнительные файлы — install_items. Иначе не создастся устройство /dev/mtd0 или /dev/pmem0. Создаем файл /etc/dracut.conf.d/mem.conf с таким содержимым (пробел после открывающей кавычки обязателен, это разделитель):

# При использовании /dev/mem:
install_items+=" /etc/udev/rules.d/99-mem.rules"
# При использовании phram:
install_items+=" /etc/udev/rules.d/99-mem.rules"
force_drivers+=" phram"
# При использовании nd_e820:
force_drivers+=" nd_e820 nd_pmem"

Собственно перегенерация initramfs:

# dracut -f

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

# mv /boot/initramfs-5.0.0-19-generic.img /boot/initrd.img-5.0.0-19-generic

При установке новых ядер автоматическое создание initramfs через Dracut осуществляется корректно, баг затрагивает только ручной запуск dracut -f.

Шаг шестой: перезагрузить компьютер

Перезагрузка нужна, чтобы подействовали изменения в конфигурации GRUB и Dracut.

# reboot

На данном этапе ключа в памяти нет, поэтому понадобится ввести ключевую фразу.

Как минимум, в псевдофайле /proc/iomem нужный участок памяти должен быть помечен как "reserved" (при использовании /dev/mem или phram) или как "Persistent Memory (legacy)". После перезагрузки необходимо проверить, правильно ли сработало резервирование памяти.

Еще при использовании phram или nd_e820 необходимо убедиться, что устройство /dev/mtd0 или /dev/pmem0 действительно ссылается на зарезервированный ранее участок памяти, а не на что-то другое.

# cat /sys/class/mtd/mtd0/name # ожидаемый результат: "savedkey"
# cat /sys/block/pmem0/device/resource # должно вывести начальный адрес

Если это не так, то надо найти, какое именно из устройств /dev/mtd* или /dev/pmem* "наше", а затем исправить /etc/crypttab, перегенерировать initramfs и перепроверить результат после еще одной перезагрузки.

Шаг седьмой: настроить копирование ключевого файла в память

Один из способов запустить какую-либо команду на этапе завершения работы системы — это прописать ее в директиве ExecStop в systemd-сервисе. Ключевой файл будет копироваться в память перед перезагрузкой. Итого, вот файл /etc/systemd/system/savekey.service. Чтобы systemd понял, что это не демон и не ругался на отсутствие директивы ExecStart, нужно указать тип сервиса как oneshot и еще подсказать, что сервис считается запущенным, даже если с ним не связан никакой работающий процесс. Надо оставить только один из приведенных вариантов директивы ExecStop.

[Unit]
Description=Saving LUKS key into RAM
Documentation=https://habr.com/ru/post/457396/ [Service]
Type=oneshot
RemainAfterExit=true
# Если используется /dev/mem:
ExecStop=/bin/sh -c 'dd if=/root/key of=/dev/mem bs=1 seek=$((0x10000000))'
# Если используется /dev/mtd0:
ExecStop=/bin/dd if=/root/key of=/dev/mtd0
# Если используется /dev/pmem0:
ExecStop=/bin/dd if=/root/key of=/dev/pmem0 [Install]
WantedBy=default.target

Конструкция с /bin/sh нужна, так как dd не понимает шестнадцатеричную запись.

Активируем сервис, проверяем:

# systemctl enable savekey
# systemctl start savekey
# reboot

А если придется, то это, как правило, означает, что адрес начала зарезервированной области памяти выбран неправильно. При последующей перезагрузке вводить ключевую фразу от диска не придется. Ничего страшного — поправить и перегенерировать несколько файлов и перезагрузить компьютер два раза.

При использовании /dev/mem начальный адрес упоминается еще и в /etc/crypttab (поэтому придется перегенерировать initramfs) и в systemd-сервисе. При использовании phram или nd_e820 править придется только конфигурацию GRUB.

Но это еще не все.

Вопросы безопасности

Т.е. Любое обсуждение вопросов безопасности основывается на модели угроз. Я в курсе, что некоторые примеры ниже надуманные. на целях и средствах атакующего.

Есть те же виды атак, нацеленных на получение ключевой фразы, в том числе Evil Maid, и те же средства защиты. Ситуации с физическим доступом к выключенному компьютеру ничем не отличаются от таковых без настроенного хранения ключа в памяти. На них не останавливаемся, так как здесь нет ничего нового.

Более интересны ситуации, когда компьютер включен.

Атакующий не имеет физического доступа к компьютеру, не знает ключевую фразу, но имеет root-доступ через ssh. Ситуация 1. Например, для доступа к старым посекторным бекапам образа диска виртуальной машины. Цель — ключ для расшифровки диска.

Вопрос в том, как это соотносится с тем, что было до выполнения этой инструкции. Собственно, ключ на блюдечке — в файле /root/key. Существует команда dmsetup table --target crypt --showkeys, которая показывает мастер-ключ, т.е. Ответ: для luks1, угроза не является новой. Для luks2, понижение безопасности в этом сценарии действительно имеет место: ключи dm-crypt хранятся в связке ключей на уровне ядра, и посмотреть на них из userspace невозможно. тоже данные, позволяющие получить доступ к старым бекапам.

Атакующий может пользоваться клавиатурой и смотреть на экран, но не готов открыть корпус. Ситуация 2. Ключевую фразу не знает, никакие другие пароли тоже не знает. Например, воспользовался утекшим паролем от IPMI или перехватил noVNC-сессию в облаке. Цель — root-доступ.

Ключевая фраза не понадобилась, так как ключ был успешно прочитан из памяти. Пожалуйста: перезагрузка через Ctrl-Alt-Del, добавление опции ядра init=/bin/sh через GRUB, готово. К сожалению, эта функциональность реализована в разных дистрибутивах по-разному. Чтобы от такого защититься, надо бы запретить GRUB'у грузить то, чего нет в меню.

2, есть команда grub2-setpassword, которая собственно и защищает GRUB паролем. В CentOS, начиная с версии 7. Если их нет, то можно напрямую отредактировать файлы в каталоге /etc/grub.d и перегенерировать grub.cfg. В других дистрибутивах могут быть свои утилиты для той же задачи.

В файле /etc/grub.d/10_linux изменить переменную CLASS, добавить в конец опцию --unrestricted, если ее там не было:

CLASS="--class gnu-linux --class gnu --class os --unrestricted"

В файле /etc/grub.d/40_custom добавить строки, задающие имя пользователя и пароль, которые нужны для редактирования командной строки ядра:

set superusers="root"
password_pbkdf2 root grub.pbkdf2....... # взять из вывода grub2-mkpasswd-pbkdf2

Или, если такую функциональность надо отключить вовсе, вот такую строку:

set superusers=""

Атакующий имеет доступ к включенному компьютеру, позволяющий загрузиться с недоверенного носителя. Ситуация 3. Цель — root-доступ. Это может быть физический доступ без открывания корпуса или доступ через IPMI.

Соответственно, загрузку с каких попало носителей надо бы запретить в BIOS. Он может загрузить свой GRUB с флешки или CD-ROM и добавить init=/bin/sh к параметрам вашего ядра, как в предыдущем примере. И еще защитить изменение настроек BIOS паролем.

Атакующий имеет физический доступ к включенному компьютеру, в том числе может открыть корпус. Ситуация 4. Цель — узнать ключ или получить root-доступ.

Атаку на модули памяти путем их охлаждения (Cold boot attack) никто не отменял. В общем-то это проигрышная ситуация в любом случае. При перезагрузке компьютера отключить диск, поменять grub.cfg на предмет init=/bin/sh, подключить обратно, дать системе перезагрузиться. Также теоретически (не проверял) можно воспользоваться тем, что современные SATA-диски поддерживают горячее переподключение. Получится (если я правильно понимаю) root-доступ.

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

Прочие вопросы

Use-after-free в чистом виде. Хранить ключ в памяти при перезагрузке — это издевательство. Это еще и защищает от замены параметров ядра. Более чистое решение — использовать kexec и складывать ключ в динамически генерируемый initramfs. Современные дистрибутивы сделали настройку kexec слишком сложной. Да, это так, если kexec работает.

Получается, ключевая фраза больше не нужна? Действительно, если вы в этом так уверены, ее можно удалить. В датацентрах и тем более в облаке питание никогда не пропадает. А при необходимости — быстро уничтожить все данные легко запоминаемой командой sudo poweroff. Получится работающий сервер, ключа от диска которого никто не знает¹ и поэтому не выдаст, но систему на котором можно штатными средствами обновлять.

¹Если не посмотрел в /root/key — но там все равно не набираемая с клавиатуры абракадабра, которую к тому же можно менять по cronу.

Есть IPMI, где я могу ввести пароль от диска. Зачем все это надо? Не хотелось бы лезть туда для каждой перезагрузки. На старых серверах IPMI работает только через старые версии Java.

Я умею разблокировать диск через SSH. Зачем все это надо? Одно другому не мешает. Прекрасно! Но что, если надо выдать права на sudo reboot пользователю, который не заслуживает знания ключевой фразы?

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть