Homemade FDE: Part Zero
Здравствуйте, меня зовут Андрей и у меня есть какое-то количество железа без шифрования диска:
Кхм, точнее, пока без шифрования диска! Не подумайте дурного, Full Disk Encryption (FDE) все еще не самоцель, а лишь средство некого успокоения :) Осознавая, что ни один пост полного потока сознания не выдержит, я решил перестать пытаться уместить все в один и начать мини-серию зарисовок о шифровании дисков со вкусом домашних пирожков (ммм!). Этот пост, как вы могли догадаться, в больше степени вступительный :)
О задачке
Люблю, когда задачки звучат банально - эта не исключение:
- при утере багажа хочу выбирать новую железку, а не переживать за “нюдсы”
- при этом сами нюдсы тонким слоем размазаны по:
- гибридному (arm64/amd64) k8s кластеру
- парку minipc с Proxmox
- всякой сопутствующей мелочевки, типа OpenWrt и малиновых шлюзов
- оберегать нюдсики будем шифрованием, а ему нужен ключ
- инфраструктура при этом должна максимально самостоятельно восстанавливаться
Последнее и будет краеугольным камнем всей этой истории, как и основным отличием от привычного шифрования ноута или печи:
- если с ноутом нет проблемы ввести пароль, то тут банально не набегаешься
- большая часть домашних инков происходит в моё отсутствие
В прочем, это уже пошли какие-то подсказки к решению задачки, а о них ниже.
О концепции
Жаль, но магии в нашем мире не существует, поэтому мы можем…
Хранить ключ локально
Вариант с пустой тратой тактов CPU при хранении ключа в открытом виде в initramfs мы рассматривать не будем, сорий. А вот вставить TPM/PIV/FIDO2/YetAnotherHwStorage мы можем, но тогда придется уверовать как минимум в:
- secure boot (особенно хочу напомнить про arm64 хосты)
- не доступность консоли, см. Mashing Enter to bypass full disk encryption
- не возможность поснифать SPI/I2C шину в случае с TPM, см. TPM sniffing attacks
- или USB в случае с PIV токеном, см. APDU-level attacks
- еще ворох всяких внезапностей
На мой вкус, Secure и Measured boot - однозначное благо, просто строить FDE только на этом я бы не рискнул. Конечно, всегда удобно, когда железка сама бутается, главное чтобы не в чужих руках ;)
В остальном же:
- эта довольно запарная (и дорогая, кстати) история
- сильно ограничивает в железе (куда-то просто нечего вставить)
- зато можем использовать для любых секретиков или в роли device identity
- но о адекватных логах можно забыть by design
В общем, этот способ нам совсем не подходит и мы идем дальше.
Или приносить ключ
Под “приносить” я понимаю удаленный принос, например, с помощью dracut-crypt-ssh. Вот, кстати, еще неплохая зарисовка на тему: Remotely unlock a LUKS-encrypted Linux server using Dropbear. Любители побегать с виртуальной клавиатурой или FIDO2 токеном извините, но мы с вами не подружимся.
Ключевые особенности этого способа:
- пограничник не получит нюдсы, потому что никто не принесет ключ - это лайк
- скорее всего понадобится stateful сервис приноса ключей - а это дизлайк
- требует приседаний на конечном устройстве
- скорее всего подходит только для FDE
- зато имеет богатейшее логирование (!)
- и возможность полностью удаленно открыть диск
Вот принос ключа по SSH нам уже подходит. Со всех сторон хороший варик, если смочь придумать какой-то мониторинг (или вебхук, хех?) + навести порядки с инвентаризацией. Единственное, не покидает чувство перегруженности - stateful сервис, усложнение процесса загрузки, вот это все. Да и порядок с инвентаризацией не мой конек, чо уж. Оставим этот способ на потом :)
А можно и сходить за ключем
Под “сходить” я понимаю сетевой сервис, в который можно сходить как для формирования ключа (e.g. McCallum-Relyea key exchange), так и для получения оного (e.g. Escrow, KMS, etc). Сейчас это не так важно, а важно то, что таким образом мы как бы привязываем ключ к доступности некого независимого сервиса по сети - имеем рубильник, так сказать. А рубильник это хорошо, рубильник это надежно.
Ключевые особенности:
- пограничник не получит нюдсы, потому что некуда будет сходить за ключем
- как и в предыдущем варике с приносом, - храним мастер-ключ в одном месте
- как и с локальным хранением, - можем использоваться не только для FDE
- при этом сервис может быть полностью stateless
- и с т.з. конечного устройства все максимально просто: нужен ключ -> сходи за ним
- имеем аудитные логи. Не богатейшие, но приличные
Звучит так, что мы за приемлемую цену собрали плюсы предыдущих способов. Это ли не заявка на победу? Чудный компромисс между простотой и эффективностью реализации - то, чего сильно не хватает домашней инфраструктуре :) Да, чуть страдает observability и debugability, зато мы не ходим на потенциально скомпрометированный хост (это он ходит к нам!).
Конкретрики бы
Раз уж с самой концепцией мы определились - настало время ее конкретизировать, не все же руками в воздухе водить. И так, мы хотим, чтобы конечное устройство само ходило по сети в наш сервис, который…
Хранил бы ключи
Как эталонный пример такого решения можно рассматривать Vault by HashiCorp и разнообразные попытки дружить его с FDE (e.g. luks-vault). Но нам такое, в принципе, не нравится (stateful < stateless
), поэтому не останавливаемся и движемся дальше.
Или совместно вырабатывал ключ
Например, как то делается с типичным envelope encryption в Key Management Service (KMS):
- клиент генерирует ключ шифрования данных (DEK), он же keyfile в LUKS
- ходит в KMS для ширования/расшифрования DEК на ключе шифрования ключей (KEK)
- а сам KEK или формируется из пароля, или хранится в TPM/на смарт-карте
Вот и stateless сервис получился. Бонусом имеем не экспортируемый KEK (при использовании смарт-карты) и простоту его ротации (достаточно двигать версию KEK в зашифрованном DEK). Осталось только решить, что делать с безопасностью транспорта - должен ли это быть TLS или что-то еще. А так приличное решение, не зря все уважающие себя облачные провайдеры имеют KMS с поддержкой envelope encryption (сложно отказаться от поддержки, так-то ^^).
Или же воспользоваться алгоритмом McCallum-Relyea и его эталонной реализацией в демоне Tang (отличный пост на тему: Network-Bound Disk Encryption). Как и в случае с KMS на выходе получаем stateless сервис, с помощью которого клиент может выработать DEK. Ну а KEK, точно так же складываем в TPM/Smart Card, и дело в шляпе. Кстати, в случае использования Tang коллеги из Red Hat заранее подумали и о безопасности транспорта - это хорошо, это нам нравится.
Или же можно сделать некий аналог HMAC Secret Extension из FIDO2 или HKDF, только в виде сервиса. Ну а чо, в нашем случае будет ваще ничем не хуже KMS, зато сервис можно крутить хоть на картошке.
Но это все слова, а когда пришла пора выбирать - я опешил. Все варианты по-своему и хороши, и плохи. Смотрите сами:
- полноценный KMS пилить долго, а польза от огрызка не очевидна
- связка Tang + Clevis всем хороша, даже финальный ключ по сети не полетит. Но:
- требует или кастомного клиента (привет поделки на ESP), или сборки jose
- не выглядит удобно расширяемым (вопрос необходимости дискуссионный)
- HKDF и производные как-будто не достаточно прогрессивны %)
Потому устав от мук выбора, я решил сделать все разом :) Если точнее, то начать с HMAC-based деривации ключа, а уж если не устроит в эксплуатации или по соображениям безопасносте, то пойти делать поддержку McCallum-Relyea key exchange. В результате этой могучей мысли и родился проект BoundBox
.
Project “BoundBox”
Ну вот, теперь-то все встало на свои места! Осталось прикинуть план:
- сначала мутим какой-то безопасный транспорт
- поверх него строим расширяемый протокол
- в рамках которого реализуем сервисные ручки + HKDF*
- а уже потом McCallum-Relyea key exchange и возможно даже простенький KMS
Идем по порядку и разбираемся с транспортом. Несмотря на то, что около дефолтный ответ будет TLS (или mTLS, если с аутентификацией клиента), в эту концепцию он довольно скверно ложится:
- требует PKI, что и усложнит наливку и эксплуатацию
- без хаков проверка сертификата требует корректного времени, а время это ОЧЕНЬ сложно. Trust Me, I’m an Engineer!
Так, окей, если не TLS, то что еще:
- умеет во взаимную аутентификацию клиента и сервера
- обеспечивает шифрование канала
- исповедует Trust On First Use (TOFU)
- не требует корректного времени на клиенте
- реализован под любой утюг
Ну конечно же SSH. Может я немного предвзят, но выглядит идеальным кандидатом, который закрывает примерно все вопросы.
Остается решить, что же будем делать с протоколом приложения. Но SSH и тут норм, зацените:
- SSH предоставляет двунаправленный канал связи клиента с сервером
- клиент может запросить запуск команды (см. RFC 4254)
- а сервер вернуть как выхлоп, так и статус выполнения
Что это мне напоминает? А HTTP/1.0
без заголовков мне это напоминает (ну или HTTP/0.9
с телом запроса) -> вот она, родная гавань :)
А мы начинаем мутить JSON API поверх SSH (ну не красота ли?):
$ echo '{"request": "body"}' | ssh boundbox.local -- /some/path
{"response": "body"}
И собирать все в кучу:
Словами это звучит так:
- сначала клиент производит всякие приготовления:
- локально генерирует приватный ключ
- локально генерирует одну и более соль
- прикапывает отпечаток публичного ключа сервера (или получает его с наливкой)
- затем каждый раз, когда ему нужен DEK:
- берет ранее сгенерированный приватный ключ и соответствующую соль
- идет с этим в BoundBox по SSH (попутно аутентифицируя его)
- в это время BoundBox:
- аутентифицирует клиента
- выполняет
HMAC-SHA-256(BoundBoxKEK, HMAC-SHA-256(clientSalt, clientKey))
- возвращает результат
Таким образом мы получаем привязку соли как к конкретному клиентскому ключу (сервер убеждается в его владении клиентом), так и к мастер-ключу самого BoundBox. Довольно пряминько на мой взгляд.
Пока считаю вступление свершившимся. Думаю, в следующей части будет сильно больше технических подробностей а не как тут.
Closure
Дальше я планирую написать еще, как минимум, два поста:
- один про аппаратно-софтовую реализацию BoundBox (upd: а вот и он)
- другой о пусконаладочных работах, готовке initramfs и дальнейших планах
Но это потом, а пока всем кота (づ˶•༝•˶)づ♡