TDE в Apache Ignite: история major feature в большом open source проекте

    Многим организациям, в особенности финансовым, приходится иметь дело с различным стандартами безопасности — например, с PCI DSS. Такие сертификации требуют шифрования данных. Прозрачное шифрование данных на диске Transparent Data Encryption реализовано во многих промышленных СУБД.

    Apache Ignite используется в банках, поэтому, в нём было решено реализовать TDE.

    Я расскажу, как мы разработали TDE силами сообщества, публично, через апачевские процессы.


    Ниже, текстовая версия доклада:

    Я постараюсь рассказать про архитектуру, про сложность разработки, как это реально выглядит в open source.

    Что сделано и что предстоит?


    На данный момент реализована Apache Ignite TDE. Phase 1.

    Она включает в себя базовые возможности работы с шифрованными кэшами:

    • Управление ключами
    • Создание шифрованных кэшей
    • Сохранение всех данных кэша на диск в зашифрованном виде

    В Phase 2 планируется включить возможность ротации (смены) мастер-ключа.
    В Phase 3 возможность ротации кэш-ключей.

    Терминология


    • Transparent Data Encryption — прозрачное (для пользователя) шифрование данных при сохранении на диск. В случае Ignite — шифрование кэша, потому что Ignite это про кэши.
    • Ignite cache — key-value кэш в Apache Ignite. Данные кэша умеют сохраняться на диск
    • Pages — страницы данных. В Ignite все данные разбиты по страницам. Страницы записываются на диск, и их нужно шифровать.
    • WAL — write ahead log. Там сохраняются все изменения данных в Ignite, все действия, которые мы выполнили для всех кэшей.
    • Keystore — стандартный java keystore, который keytool’ом джавовским генерируется. Он работает и сертифицирован везде, мы использовали его.
    • Master key — мастер-ключ. С помощью него шифруются ключи для таблиц, ключи шифрования кэша. Хранится в java keystore.
    • Cache keys — ключи, с помощью которых реально шифруются данные. Вместе с мастер-ключом получается двухуровневая структура. Мастер-ключ хранится отдельно от кэш ключей и основных данных — в целях безопасности, разделения прав доступа и т.п.

    Архитектура


    Реализовано все по следующей схеме:

    • Все данные кэша шифруются при помощи нового Encryption SPI
    • По умолчанию используется AES — промышленный алгоритм шифрования.
    • Мастер-ключ хранится в JKS-файле — стандартном java'овском файле для ключей.

    В банках и других организациях применяют свои алгоритмы шифрования: ГОСТ и другие. Понятно, что мы предусмотрели возможность подсунуть свой Encryption SPI — ту реализацию шифрования, которая нужна конкретному пользователю.

    Схема работы


    image

    Итак, у нас есть RAM — оперативная память со страницами, содержащими чистые данными. Использование RAM подразумевает, что мы не защищаемся от хакера, который получил root доступ и сдампил всю память. Мы защищаемся от администратора, который берет жесткий диск и продает его на Тушинском рынке (или где сейчас подобные данные продают).

    Помимо страниц с кэшем, данные также хранятся в write ahead log, который записывает на диск дельту измененных в транзакции записей. В metastore хранятся ключи шифрования кэшей. И в отдельном файлике — мастер-ключ.

    Каждый раз, когда создается ключ для кэша, перед записью или передачей в сеть, мы шифруем этот ключ при помощи мастер-ключа. Чтобы никто не мог получить ключ кэша, получив данные Ignite. Только украв и мастер-ключ и данные, можно получить к ним доступ. Это маловероятно, поскольку для доступа к этим файлам нужны различные права.

    Алгоритм действий таков:

    • При старте ноды вычитываем master key из jks.
    • При старте ноды вычитываем meta store и расшифровываем cache keys.
    • При join ноды в кластер:
      – сверяем хэши мастер ключа.
      – сверяем ключи для общих кэшей.
      – сохраняем ключи для новых кэшей.
    • При динамическом создании кэша — генерируем ключ и сохраняем его в meta store.
    • При чтении/записи страницы расшифровываем/шифруем её.
    • Каждую запись WAL для шифрованного кэша также шифруем.

    Теперь подробнее:

    При старте ноды у нас есть callback, который запускает наш EncryptionSPI. Мы, в соотетствии, с параметрами вычитываем мастер-ключ из jks-файла.

    Далее, по готовности metastore, достаём сохранённые ключи шифрования. Мастер-ключ при этом у нас уже есть, так что мы можем расшифровать ключи, получить доступ к данным кэша.

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

    Мы выполняем вот такие действия:

    • Когда новая нода приходит, она присылает в хэш от мастер-ключа. Мы смотрим, что он совпадает с имеющимся
    • Затем сверяем ключи для общих кэшей. От ноды приходит айдишник кэша и зашифрованный кэш-ключ. Мы сверяем их, чтобы убедиться, что все данные на всех нодах у нас зашифрованы одним и тем же ключом. Если это не так, то мы просто не имеем права пускать ноду в кластер — иначе произойдет разъезжание по ключам и по данным.
    • Если на новой ноде есть какие-то новые ключи и кэши, сохраняем их, чтобы дальше использовать.
    • При динамическом создании кэша предусмотрена функция генерации ключа. Генерируем его, сохраняем в meta store и можем дальше проводить описанные операции.

    Вторая часть — надстройка над I/O-операциями. В файл партиции пишутся странички. Наша надстройка смотрит, от какого кэша странички, соответствующим образом шифрует их и сохраняет.

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

    Сложности в разработке


    Сложности общие для всех более-менее сложных open source проектов:

    1. Для начала нужно вообще разобраться в устройстве Ignite. Для чего, что и как там было сделано, как и в какие места прицепить свои обработчики.
    2. Нужно обеспечить обратную совместимость. Это бывает достаточно сложно, неочевидно. Когда разрабатываешь продукт, который используется другими, нужно учесть, что пользователи хотят обновляться без проблем. Обратная совместимость — это правильно и хорошо. Когда делаешь такую большую доработку, как TDE, то изменяешь правила сохранения на диск, что-то шифруешь. И над обеспечением обратной совместимости приходится поработать.
    3. Еще один неочевидный момент связан с распределенностью нашей системы. Когда разные клиенты пытаются создать один и тот же кэш, нужно согласовать ключ шифрования, ведь по умолчанию будут сгенерированы два разных. Мы решили эту задачу. Подробней останавливаться не буду — решение заслуживает отдельного поста. Сейчас у нас гарантированно используется один ключ.
    4. Следующая важная вещь привела к большим доработкам, когда казалось, что всё уже готово (знакомая история?) :). Шифрование имеет overhead. У нас есть init vector — нулевые случайные данные, которые используются в алгоритме AES. Они хранятся в открытом виде, и с их помощью мы увеличиваем энтропию: одни и те же данные при разных сессиях шифрования будут по-разному зашифрованы. Грубо говоря, даже если у нас будет два Ивана Петрова, с одинаковой фамилией, то каждый раз при шифровании мы будем получать разные шифрованные данные. Это уменьшает вероятность взлома.

      Шифрование происходит блоками по 16 байт, и если данные не выровнены на 16 байт, то мы добавляем padding info — сколько у нас реально данных зашифровано. На диск нужно писать страницу, кратную 2 Кб. Таковы требования по перфомансу: мы должны использовать буфер диска. Если мы пишем не 2 Кб (не 4 или не 8, в зависимости от буфера диска), то тут же получаем большой перфоманс дроп.

      Как мы решили проблему? Пришлось залезть в PageIO, в оперативной памяти и отрезать 16 байт от каждой страницы, которая при записи на диск будет зашифрована. В эти 16 байт мы и записываем init vector.
    5. Еще одна сложность — ничего не поломать. Это обычное дело, когда ты приходишь и вносишь какие-то изменения. В действительности это не так просто, как кажется.
    6. В MVP получилось 6 тысяч строк. Сложно ревьювить, да и мало кто хочет этим заниматься — особенно из экспертов, у которых и так времени нет. У нас есть различные части — public API, core-часть, SPI managers, persistent store для страниц, WAL-менеджеры. Изменения в различных подсистемах требуют, чтобы их ревьюили различные люди. И это тоже дополнительные сложности накладывает. Особенно когда ты работаешь в сообществе, где все люди заняты своими задачами. Тем не менее, у нас все получилось.

    Что будет в TDE.Phase 2 и 3


    Сейчас реализована Phase 1. Вы, как разработчик, можете помочь с Phase 2. Задачи предстоят интересные. PCI DSS, как и другие стандарты, требует дополнительных свойств системы шифрования. Наша система должна уметь сменить мастер-ключ. Например, если он был скомпрометирован или просто пришло время в соответствии с политикой безопасности. Сейчас Ignite этого не умеет. Но в следующих релизах мы научим TDE менять мастер-ключ.

    То же самое и с возможностью смены cache key без остановки кластера и работы с данными. Если кэш долгоживущий и при этом хранит какие-то данные — финансовые, медицинские — Ignite должен уметь менять ключ шифрования кэша и все перешифровать на лету. Эту задачу мы будем решать в третьей фазе.

    Итого: Как реализовать большую фичу в open source проекте?


    Подведем итоги. Они будут актуальны для любого open source. Я участвовал и в Kafka, и в других проектах – везде история одинаковая.

    1. Начните с маленьких задач. Никогда не беритесь решать супербольшую задачу сразу. Нужно разобраться, что происходит, как происходит, как реализуется. Кто тебе будет помогать. И вообще – с какой стороны к этому проекту подходить.
    2. Разберитесь в проекте. Обычно все разработчики — по крайней мере, я — приходят и говорят: надо все переписать. До меня были все плохо, а сейчас я перепишу – и все будет хорошо. Желательно с такими заявлениями повременить, разобраться, что конкретно плохо и нужно ли это менять.
    3. Обсудите, нужны ли доработки. У меня были случаи, когда я приходил с наработками в различные комьюнити — например в Spark. Рассказывал, а комьюнити оказывалось не заинтересовано по каким-то причинам. По-всякому бывает. Тебе эта доработка нужна, а комьюнити говорит: нет, мы не заинтересованы, не будем merge'ить и помогать.
    4. Сделайте дизайн. Есть open source проекты, в которых это обязательно. Вы не можете начинать кодировать без дизайна, который согласовали комитеры и опытные люди. В Ignite это формально не так, но вообще это важная часть разработки. Нужно сделать описание на грамотном английском или русском языке — в зависимости от проекта. Чтобы текст читался и по нему было понятно, что конкретно вы собираетесь сделать.
    5. Обсудите public API. Основной аргумент: если будет красивый и понятный public API, который легко использовать – значит, и дизайн правильный. Эти вещи обычно соседствуют друг с другом.

    Дальше более очевидные советы, которым бывает не так-то просто следовать:

    • Реализуйте фичу ничего не сломав. Сделайте тесты.
    • Попросите и дождитесь (это бывает сложнее всего) ревью от нужных ребят, от нужных членов комьюнити.
    • Сделайте бенчмарки, узнайте нет ли у вас performance drop. Это особенно важно при доработке каких-то критических подсистем.
    • Дождитесь merge, сделайте примеры и документацию.

    Спасибо, что прочитали!
    Поделиться публикацией

    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.