Привет! Меня зовут Даниил Гранкин, я разработчик внутреннего бэкенд-юнита в Wrike. В этой статье я поделюсь техническими подробностями разработки Wrike Lock — основного механизма шифрования данных в нашем продукте. А также расскажу, для чего мы используем Encryption as a Service (EaaS) и какие проблемы решаем с его помощью. Эта статья будет полезна, если вы ищете способы дистрибуции ключей шифрования, но готовые решения вам не подходят.
Wrike Lock: что внутри
Wrike — это SaaS-продукт. Пользовательские данные в нем хранятся и обрабатываются в инфраструктуре Wrike.
Мы прикладываем максимум усилий, чтобы обеспечить надежную передачу и хранение данных с ограниченным доступом. Шифрование данных в SaaS-продукте особенно важно: оно гарантирует, что данные пользователей защищены от слежки и вредоносных действий третьих лиц, которые могут попытаться их украсть.
Wrike Lock — это функциональность, которая помогает управлять безопасным хранилищем данных с помощью криптографии, сохраняя зашифрованные данные и расшифровывая их по запросу.
Шифрование. Управлять зашифрованными данными можно и стандартными ключами AES256. Но основная проблема шифрования в SaaS заключается в управлении и распространении ключей. Мы используем особый подход к управлению ключами.
Блокировка в Wrike работает для каждой учетной записи отдельно: учетная запись «A» с включенной блокировкой не может расшифровать данные учетной записи «B» и наоборот. Поэтому для каждой учетной записи у нас разные ключи шифрования.
Ключи шифрования данных — data keys. Система использует data keys для шифрования или расшифровки актуальных данных пользователя. Любая часть потенциально конфиденциальной информации шифруется с использованием ключей. При шифровании данные безопасно хранятся в базе данных вместе с идентификатором ключа. Никто не может получить доступ к данным в их первоначальном виде, даже имея доступ к самой базе.
При извлечении данных ключ, который система использовала для шифрования, можно легко найти с помощью идентификатора:
0x11<data_key_id_int>0x20<encrypted_bites>
Несколько ключей данных. В Wrike мы используем несколько баз данных для разных целей, а иногда и на разных уровнях платформы. Для разных наборов баз данных существуют специальные ключи шифрования. Это нужно нам для того, чтобы сделать управление эффективнее. В итоге мы имеем несколько текущих ключей шифрования данных для одной учетной записи.
Согласно общей практике шифрования, мы можем ротировать эти ключи и добавлять новые без повторного шифрования уже существующих данных. После ротации старый ключ хранится для расшифровки тех данных, в которых он был использован. Таких ключей может быть множество, все зависит от количества проведенных ротаций. Новый же ключ используется для шифрования новых данных.
Безопасное хранение ключей шифрования данных. Для этого мы ввели ключ учетной записи — account key, который шифрует каждый ключ.
Механизм хранения и получения зашифрованных ключей данных схож с механизмом хранения данных:
Шифруем data key.
Сохраняем зашифрованный data key в базе данных вместе с ID account key.
Расшифровываем data key с помощью соответствующего account key.
Это обеспечивает единую точку доступа ко всем активным ключам данных, которые принадлежат одной учетной записи.
Обеспечение безопасности точки доступа. В Wrike Lock ключ учетной записи шифруется и расшифровывается по запросу только владельцем учетной записи — клиентом.
С помощью AWS Key Management Service клиент управляет каждым запросом на шифрование и расшифровку account key. Клиент отвечает за свои данные, которые хранятся на наших серверах. Он может приостановить или даже отозвать доступ к расшифровке account key.
Сценарии идеальной катастрофы
«С большой силой приходит большая ответственность» — Дядя Бен, Человек-Паук
В любой момент что-то может пойти не так:
Серверы AWS выйдут из строя.
Клиент потеряет доступ к AWS KMS.
Мы потеряем доступ к AWS KMS.
Случится что-то еще.
При настройке Wrike Lock наши support-специалисты шифруют account key с помощью AWS KMS и асимметричного открытого ключа шифрования, который предоставляет клиент. Закрытый ключ хранится у клиента.
Теперь предположим, что одна из сторон теряет контроль над AWS KMS. В таком случае мы можем использовать специально зашифрованный account key, который может быть расшифрован клиентом с помощью закрытого ключа.
Полная функциональность включена в каждый сервис, которому нужна интеграция с Wrike Lock.
Отсюда следует, что каждый сервис:
Читает данные с одной и той же БД.
Отправляет к AWS KMS запросы на расшифровку ключа учетной записи.
Потенциально это может происходить для одних и тех же ключей, потенциально — одновременно.
Микросервисы: проблема разделения с Wrike Lock
Wrike представляет собой распределенный монолит — одно большое веб-приложение с многочисленными сервисами, которые разрабатываются, создаются и деплоятся параллельно.
Но это все еще не полноценная микросервисная платформа:
Сервисы работают с общей базой данных.
Код находится в одном монорепозитории.
Несколько крупных модулей содержат большую часть общей для всех сервисов логики.
Про наш путь к микросервисам в статье на Хабре подробно рассказал мой коллега Слава Тютюньков. Но мы коснемся этой темы и в этом тексте, так как наш путь к микросервисам положил начало проекту, которым я хочу поделиться.
В рамках перехода мы не можем только разрабатывать новые микросервисы. Старая функциональность остается, и ее нужно адаптировать. Процесс адаптации требует методичного выделения сервиса вместе со всеми сопутствующими функциональностями в отдельную логическую единицу. Мы называем этот процесс разделением.
При разделении мы столкнулись с проблемой: сервис работает с данными клиентов, поэтому требует интеграции с Wrike Lock, а его основная функциональность находится в большом модуле монорепозитория.
Решение не включать этот модуль в микросервис было вполне очевидным:
Нам не нужна большая часть логики, которая расположена в модуле для микросервиса.
Микросервис будет находиться в другом репозитории, поэтому мы должны управлять библиотекой, которая содержит модуль со всей логикой.
Логика довольно запутанная, поэтому мы не можем просто «вытащить» этот модуль в единую библиотеку.
Даже если бы мы могли извлечь модуль в библиотеку, в будущем мы все равно планируем его разобрать.
Итак, нам нужно найти другое решение — универсальное, масштабируемое, но изящное.
Encryption as s Service (EaaS)
Таким решением стал проект Encryption as a Service (EaaS).
Ожидания от инструмента были простыми:
Шифрование данных.
Расшифровка данных.
План состоял в том, что любой сервис на нашей микросервисной платформе должен иметь возможность использовать имеющуюся функциональность и без особых усилий инициировать запросы. Для сервисов, которые уже используют Wrike Lock, переход должен быть незаметным.
Поэтому мы предложили использовать клиентскую библиотеку: разработчики могут включить ее в сервис, которому нужно внедрить Wrike Lock.
В библиотеке есть все функции, которые могут понадобиться клиенту от Wrike Lock:
Управление областями шифрования (логика управления при различных вариантах использования разных запросов).
Шифрование и расшифровка данных.
Связь с самой услугой шифрования.
Первая идея состояла в том, чтобы отправлять зашифрованные данные на сервер и получать расшифрованные, но:
Данные могут достигать сотен или даже тысяч килобайт.
Чем больше данных, тем больше задержка.
Работа с данными только через API не позволяет их кэшировать.
Чтобы устранить все болевые точки, мы ввели API, который работает с ключами данных вместо самих данных:
Ключи данных имеют фиксированный размер менее 256 байт, включая все необходимые метаданные.
Клиентская библиотека может кэшировать ключи, чтобы избежать нескольких вызовов одного и того же ключа в течение короткого периода.
Минимальный API предоставляет текущий активный ключ данных или любой ключ данных по ID.
Wrike Lock — это платная функция, у некоторых клиентов она не включена. Нам нужно было их различить, чтобы сделать необходимые данные не зашифрованными.
Например:
При шифровании: простые данные -> простые данные.
При расшифровке: простые данные -> простые данные ИЛИ зашифрованные данные -> ошибка.
Поэтому API должен включать вызовы для проверки того, включен ли Wrike Lock у конкретной учетной записи.
Нам нужно включить клиентскую библиотеку в каждый сервис, который требует интеграции с Wrike Lock. Все это будет накапливаться в нескольких службах, запрашивающих ключи данных, поэтому нам нужен сервер.
Служба управления ключами Wrike Lock (WLKMS)
По сравнению с первоначальным подходом, у централизованного управления ключами больше всего преимуществ:
Меньше точек отказа.
Проще вносить изменения при управлении ключами.
Меньше обращений к AWS KMS.
Давайте отдельно обратим внимание на вызовы. Вызовы AWS KMS были причиной больших накладных расходов. Типичное время расшифровки ключа учетной записи с помощью KMS — 300 мс, типичное количество запросов на расшифровку ключа учетной записи — около 300 000 в месяц в рамках одного дата центра. Для расшифровки ключа учетной записи с помощью KMS мы получаем около суток времени ожидания в течение месяца во всех сервисах.
Большинство запросов исходят от разных сервисов, которые просят расшифровать один и тот же ключ учетной записи.
По нашим оценкам время ожидания после внедрения Wrike Lock сократится примерно в 10-20 раз. Это произойдет за счет того, что вызовы будут исходить из одной точки.
WLKMS будет кэшировать полученные расшифрованные ключи учетной записи.
Давайте теперь взглянем на нашу стратегию кэширования. Первоначальный план состоял в том, чтобы иметь три кэша:
Кэш ключей шифрования данных на стороне библиотеки клиента является надежной точкой для сокращения количества запросов на идентичные ключи.
Мы решили отказаться от кэша ключей на стороне WLKMS. Дополнительная задержка при получении ключа из БД и его расшифровке с использованием кэшированного ключа учетной записи незначительна, поскольку у нас уже есть задержка ввода-вывода в сети.
Кэш ключа учетной записи жизненно важен, поскольку доступ к AWS KMS стоит дорого.
Теперь, когда у нас есть все кэши, перейдем к ограничениям.
Мы обещаем, что через 35 минут после отзыва доступа к AWS KMS все сервисы перестанут шифровать или расшифровывать данные. Поэтому нам нужно было назначить временные ограничения для двух выбранных кэшей.
Мы использовали безопасный и простой подход: расшифрованный ключ учетной записи кэшировался в течение 30 минут, в то время как полученный ключ данных на стороне клиента имел тайм-аут в 5 минут.
В результате получается система, в которой максимальная задержка между отзывом доступа и невозможностью зашифровать/расшифровать данные теоретически составляет 35 минут.
Пример:
Клиентская библиотека запрашивает ключ шифрования данных у WLKMS.
WLKMS запрашивает расшифровку account key, получает расшифрованный ключ и сохраняет его в течение 30 минут.
Клиентская библиотека получает ключ данных, у нее на него будет 5 минут. Для примера это не имеет значения, так как мы ищем максимальную задержку между отзывом доступа и доступностью ключей.
Прямо перед истечением срока действия кэша account key некая клиентская служба запрашивает data key, который зашифрован этим ключом учетной записи.
Этот запрос не вызывает расшифровку от AWS KMS, а извлекает account key из кэша.
Ключ данных, предоставленный WLKMS, имеет тайм-аут в 5 минут.
Любой из следующих запросов будет осуществлять вызов AWS KMS — они не будут способствовать увеличению задержки.
Последний полученный ключ имеет тайм-аут в 5 минут. Клиентская библиотека осуществляет запрос этого ключа в момент времени t +29,99. Максимально возможное время задержки — ≤ 35 минут.
Этот подход помог нам преобразовать Wrike Lock в микросервис. Но мы решили пойти чуть дальше и минимизировать задержку получения ключей данных для наиболее активных учетных записей.
Решение состоит в том, чтобы сделать оболочку кэша асинхронной — как для кэша данных, так и для кэша ключа учетной записи.
Единственная дополнительная функциональность заключается в следующем: при получении запроса на ключ после сброса кэша — оболочка предоставляет просителю запрошенный ключ и одновременно инициирует процесс обновление экземпляра ключа.
Для кэша ключей шифрования данных на стороне клиентской библиотеки запрос отправляется к WLKMS. На стороне WLKMS отправляется запрос на расшифровку ключа аккаунта к AWS KMS.
Основной недостаток этого решения — нагрузка на инфраструктуру становится больше: число обращений к AWS KMS и число обращений к серверу увеличивается вдвое. Но процесс становится стабильнее для активных аккаунтов.
В худшем случае для запроса неинициализированного ключа шифрования данных потребуется около 300 мс, большая часть из них придется на ожидание расшифровки ключа аккаунта AWS KMS. В случае с наиболее активной учетной записью эта задержка будет присутствовать только в первый раз.
При кэшировании ключа учетной записи общая задержка будет состоять из задержки сети и базы данных на стороне службы сервера и не превысит 3 мс. Эта задержка никогда не произойдет у наиболее активных аккаунтов, потому что ключ данных обновляется при каждом запросе после истечения срока действия кэшированного ключа.
В лучшем случае ключ данных кэшируется на стороне клиентской службы, то есть ключ уже присутствует. Этот случай будет самым частым.
Выводы
Для управления ключами шифрования в SaaS требуется надежное решение с быстрым откликом.
При правильном обращении AWS KMS — полезный инструмент.
Переход на микросервисы в крупномасштабных проектах никогда не бывает легким.
Буду рад ответить на вопросы и комментарии!