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

в 15:20, , рубрики: CentOS, luks, Ubuntu, Настройка Linux, системное администрирование

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

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

Котейка

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

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

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

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

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

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

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

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

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

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

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

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

$ 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-tools реализована некорректная в нашем случае логика, применяемая к зашифрованным разделам с ключевым файлом. Такие разделы либо игнорируются полностью, либо содержимое ключевого файла копируется в initramfs (т.е. в итоге на диск) в открытом виде, что нам не надо.

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

Нам достаточно 128 случайных бит, т.е. 16 байт. Файл будет храниться на зашифрованном диске, поэтому никто, не знающий ключа шифрования и не имеющий 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 есть по меньшей мере три разных драйвера, позволяющих обращаться к физической памяти по известному адресу. Это linux/drivers/char/mem.c, отвечающий в том числе за устройство /dev/mem, а также модули phram (эмулирует MTD-чип, дает устройство /dev/mtd0) и nd_e820 (применяется при работе с NVDIMM, дает /dev/pmem0). У них у всех есть свои неприятные особенности:

  • /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 (очевидно, фальшивый, но нам и такой сгодится).

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

Важно: знак доллара ($) в конфигурационных файлах GRUB является спецсимволом и подлежит экранированию. Причем двойному: один раз — при генерации grub.cfg из /etc/default/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
...

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

При использовании модуля phram нужен еще один параметр командной строки ядра, который, собственно, и указывает модулю, какой кусок физической памяти использовать — наш, зарезервированный. Параметр называется phram.phram и содержит три части: название (произвольное до 63 символов, будет видно в sysfs), начальный адрес и длину. Начальный адрес и длина должны быть такими же, как и в 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

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

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

Придется подсказать 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. В том числе те, где он не используется по умолчанию, но доступен и работоспособен.

Перегенерировать initramfs нужно, чтобы обновить там файл /etc/crypttab. А еще — чтобы включить туда дополнительные модули ядра и правила udev. Иначе не создастся устройство /dev/mtd0 или /dev/pmem0. За включение и загрузку дополнительных модулей ядра отвечает параметр конфигурации Dracut force_drivers, за дополнительные файлы — install_items. Создаем файл /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

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

# 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-сервисе. Чтобы systemd понял, что это не демон и не ругался на отсутствие директивы ExecStart, нужно указать тип сервиса как oneshot и еще подсказать, что сервис считается запущенным, даже если с ним не связан никакой работающий процесс. Итого, вот файл /etc/systemd/system/savekey.service. Надо оставить только один из приведенных вариантов директивы 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

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

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

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

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

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

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

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

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

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

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

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

В CentOS, начиная с версии 7.2, есть команда grub2-setpassword, которая собственно и защищает GRUB паролем. В других дистрибутивах могут быть свои утилиты для той же задачи. Если их нет, то можно напрямую отредактировать файлы в каталоге /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. Атакующий имеет доступ к включенному компьютеру, позволяющий загрузиться с недоверенного носителя. Это может быть физический доступ без открывания корпуса или доступ через IPMI. Цель — root-доступ.

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

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

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

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

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

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

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

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

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

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

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

Автор: AEP

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js