Наша команда занимается развитием продуктов в сфере информационной безопасности в Лиге Цифровой Экономики. В этой статье хотим поделиться опытом создания нашего продукта — модуля управления инфраструктурными секретами ЦУП 2.0 на основе открытого программного обеспечения. В нашем случае это «ванильный» Vault версии 1.7, который был доступен по свободной лицензии MPL. Взяв его за основу в 2021 году, мы почти сразу столкнулись со следующими проблемами его использования:
отсутствие горизонтального масштабирования;
ограничение производительности при использовании в высоконагруженном режиме;
неадаптивный мониторинг;
узкие возможности управления доступом.
Все эти проблемы нам пришлось решать, т. к. стояла задача развернуть наш модуль секретов в проекте, где он будет интегрирован с системами Mission Critical+. Как мы это сделали — расскажем подробнее в статье.
Началось все с масштабирования. Мы знали, что в Enterprise Vault реализована возможность работы с Namespaces (или тенантами), которая в значительной мере решает проблему масштабирования, но в Opensource Vault этой возможности нет. К тому же разграничение доступа в Opensource Vault ограничивается только политиками доступа на уровне путей и методов запроса. Тоже полезно, но требовалось больше: необходимы были Namespaces, как в Enterprise.
«Вызов принят», — подумали мы.
Пара месяцев разработки, и в нашей реализации появились Namespaces (или тенанты). У нас тенант первого уровня — это новое ядро Vault.
Мы также включили в Namespace свои изолированные объекты следующих типов:
Secret Engines (движки секретов);
Auth Methods (методы аутентификации);
Policies (политики);
Identities (группы);
Tokens (токены).
Мы сделали так, чтобы Namespace первого уровня (или тенант) мог содержать в себе вложенный Namespace второго уровня. Второго уровня — содержать соответственно третьего и т. д.
Естественно, API по управлению этими тенантами важно было оставить совместимым с API Enterprise Vault. Следовательно, если имеются или используются в проекте инструменты управления тенантами, основанные на спецификации API Enterprise Vault, их можно было переиспользовать и с нашим продуктом.
Может возникнуть вопрос — что делать с большим числом объектов, которые необходимо создать, чтобы дать возможность управлять секретами множеству систем-потребителей, используя мультитенантное разграничение?
Мы ввели понятие Хранилище: это ряд тенантов, вложенных друг в друга. Подобные хранилища можно строить, опираясь на иерархию подразделений, методы аутентификации, политики, группы, внешние группы в Active Directory или в зависимости от движков, используемых для управления секретами.
Для контроля за этой большой абстракцией (Хранилищем) мы реализовали в продукте так называемый модуль комплексных операций (МКО). Наружу выставили методы управления Хранилищами, а под капотом выполняется управление низкоуровневыми структурами, перечисленными выше. Таким образом и в систему автоматизации на уровне политик выдаётся доступ для управления Хранилищем. Для низкоуровневых структур он запрещен.
В качестве бэкенда для каждого тенанта выступает отдельная схема в СУБД. Схемы могут размещаться как в основном кластере СУБД, так и в отдельных выделенных кластерах. Разместить тенант в изолированном кластере БД можно как при создании хранилища, так и позже, в процессе использования, когда появится понимание, что место конкретному тенанту в отдельном кластере. Эта особенность позволила нам значительно продвинуться в решении задачи масштабирования на уровне Storage Backend (то есть на уровне БД).
Далее встал вопрос с производительностью. Для начала необходимо было разобраться, от кого и когда возникает нагрузка. Для этой цели пришлось немного доработать метрики мониторинга.
Для мониторинга производительности нашего приложения и нагрузки на каждый тенант в метрики мониторинга мы добавили теги Namespace.
Это позволило нам:
идентифицировать тенанты, на которые идет больше обращений, чем на другие,
составить рейтинг самых нагруженных (нагружающих vault) потребителей,
выявить тех, которые, скорее всего, некорректно сконфигурировали свои системы для управления секретами,
помочь им настроить интеграцию и таким образом более оптимально утилизировать вычислительные ресурсы.
Кроме того, если говорить о работе с потребителями, у нас получилось сделать квоты на запросы (Rate Limits), которые позволяют на уровне доступа ко всему тенанту ограничить полосу пропускания (RPS) — в том числе за счет добавленной нами функциональности по управлению временем ответа клиенту для контроля за потоком запросов.
Вкупе с метриками мониторинга квоты позволяют идентифицировать и ограничивать паразитную нагрузку на систему, высвобождая ресурсы для остальных потребителей.
В связи с реализацией тенантов как изолированных ядер возникла проблема распаковки нашего приложения, т. к. при распаковке загружаются все эти ядра. В масштабе нашего крупного проекта это несколько тысяч тенантов. Распаковка такого большого числа тенантов требовала значительных вычислительных ресурсов и занимала определенное время (для десяти тысяч это составляло около 5 минут). В случае возникновения аварии, которая может спровоцировать переезд мастера vault на другой узел кластера, распаковка тенантов за 5 минут — это 5 минут недоступности системы. Для класса критичности Mission Critical+ это было недопустимо.
Что мы сделали, чтобы эту проблему решить?
Мы реализовали быстрое переключение с ведомого узла кластера Vault на лидера для того, чтобы можно было без длительной недоступности ставить новые релизы на промышленные стенды.
В рамках этой функциональности мы сделали так чтобы выполнялась неполная распаковка (unseal) performance-ноды кластера при старте. Неполная — потому что не запускается expiration manager, в остальном распаковка такая же, как на master-ноде. Во время такой распаковки performance-нода уже может маршрутизировать все запросы (так же, как обычная slave-нода). В ответ на запрос такая performance-нода отдаст статус unsealing (код 200), то есть запросы с балансировщика на неё могут поступать и во время процесса распаковки. Такую ноду мы называем «предансильной» нодой.
В случае отправки пользователем запросов на изменение данных (изменение роутера — создание, удаление движков типа secrets, auth, audit; работа с политиками, группами, entity, identity) запрос выполняется на master-ноде. Затем отправляется запрос на синхронизацию состояния на КАЖДУЮ performance-ноду в кластере. После отдаётся ответ пользователю.
Таким образом поддерживается консистентность данных между master и performance нодами. Синхронизация tokenstore-а не происходит, при переключении мастера кэш tokenstore очищается.
В целом при переключении мастера одна из performance-нод в кластере становится новым мастером. Её состояние (данные) находится в соответствии с состоянием мастера. Процесс, который происходит до того, как этот новый мастер сможет обрабатывать запросы, получил название «доансил» — в его рамках включается expiration manager и очищается кэш ноды.
Таким образом переключение мастера происходит гораздо быстрее по сравнению с полной распаковкой нового мастера.
При изменении кэша на мастере (изменение пользователем каких-то секретов) master-нода отправляет запрос на инвалидацию кэша на КАЖДУЮ performance-ноду. Каждая performance-нода периодически опрашивает мастер, поэтому у master-ноды есть список performance-нод, которым она отправляет сообщения о синхронизации либо инвалидации кэша. В случае, если при синхронизации performance-ноды происходит ошибка, эта нода входит в состояние suspended — то есть выходит из балансировки для предотвращения получения устаревших данных.
Во время распаковки performance-ноды запросы на синхронизацию и инвалидацию кэша складываются в буфер сообщений на performance-ноде. Эти сообщения обработаются тогда, когда performance-нода полностью распакуется.
При переключении мастера старый мастер проходит процесс, называемый «пресил». Это процесс, обратный «доансилу». Всё, что включается на «доансиле», выключается на «пресиле».
Функциональность по быстрому переключению с ведомого на лидера дополняет наши доработки по плавному завершению работы сервера vault (Graceful Shutdown).
Суть этих доработок в том, что процесс вывода узла из балансировки, обновление сервера, инициализация и ввод обратно в балансировку выполняется мягко, без потери запросов пользователей.
В промежуток между тем, как был установлен статус завершения или приостановки работы, и моментом, когда балансировщик действительно прекратит перенаправление запросов на сервер Vault, будет принято некоторое количество запросов. В худшем случае время, когда запросы все еще будут доходить, равняется времени опроса healthcheck, установленного в настройках балансировщика.
Если узел — stand by, то все принятые от балансировщика запросы должны быть обработаны.
В случае, если узел является лидером, обработка всех новых запросов прекращается немедленно (в том числе полученных через перенаправление), а полученные ранее, но необработанные запросы обрабатываются так, как это предусмотрено логикой передачи лидерства (step down).
В ответ на перенаправление запросов возвращается ответ, результатом которого будет http-ответ клиенту с кодом 429 и заголовком Retry-After. В агенте мы также добавили возможность работы после перезагрузки.
Для корректной обработки ответов с кодом 429 и заголовком Retry-After нами были доработаны SDK и, соответственно, агент.
Сервера приложений обновлять без недоступности научились. А как же быть с серверами кластера БД? И здесь пришлось немного поломать голову над реализацией множественного подключения (кластеризации на уровне БД)
Для этой цели на основе оригинального postgresql storage backend был разработан storage backend, далее — postgresql_v2 storage backend, поддерживающий в качестве СУБД один или более кластеров PostgreSQL на базе Patroni и с настроенной между кластерами асинхронной физической репликацией. Postgresql_v2 storage backend предоставляет набор метрик либо телеметрии оригинального postgresql storage backend. Реализация postgresql_v2 storage backend позволяет использовать его в качестве storage backend системы.
В результате мы получили вот такие поддерживаемые конфигурации:
-
Один кластер PostgreSQL:
кластер, включающий два узла PostgreSQL и арбитр, между узлами PostgreSQL кластера настроена синхронная физическая репликация.
-
Два кластера PostgreSQL:
Мастер-кластер, включающий два узла PostgreSQL и арбитр, между узлами PostgreSQL кластера настроена синхронная физическая репликация.
StandBy кластер, включающий два узла PostgreSQL и арбитр, между узлами PostgreSQL кластера настроена синхронная физическая репликация, между кластерами настроена асинхронная физическая репликация.
Теперь выключение узлов кластера БД для регламентных работ по обновлению или аварийные ситуации на одном из узлов не приносят с собой недоступности системы.
Кроме того, если указывать в строке подключения узлы как основного кластера, так и резервного, переключение на резервный кластер БД осуществляется без переконфигурирования и перезагрузки серверов vaut. Достаточно на сетевом уровне обеспечить связь vault с резервным кластером БД и разорвать эту связь с основным кластером, который, например, находится в аварийном состоянии.
Эту возможность можно также использовать при наблюдении деградации СХД на основном кластере БД и оперативно переключать приложение на работу с резервным кластером БД.
Вот так выглядит полная схема холодного резерва БД:
Таким образом сделанное нами холодное резервирование базы данных — полнофункциональный набор компонентов БД, асинхронно реплицирующий себе данные с основного кластера PostgreSQL.
Холодный резерв БД осуществляет резервирование не только функциональных компонентов БД, но и компонентов, которые реализуют нефункциональные требования по обеспечению горячего резерва.
Таким образом, при переключении на холодный резерв БД система будет защищена от сбоев дубликатом горячего резерва. Холодный резерв сможет функционировать в качестве основных мощностей, а те мощности, которые были выведены из строя, после восстановления должны стать холодным резервом. Для обеспечения актуальности копии данных серверы PostgreSQL должны быть активны и настроены на каскадную репликацию с серверов основного кластера.
Все описанные мероприятия и доработки значительно повысили доступность приложения.
С простоями при регламентных работах на серверах приложения или серверах кластера БД мы разобрались. Но оставался еще один очень важный и непростой вопрос — это поведение систем-потребителей секретов в момент базовой и главной процедуры модуля управления секретами — ротации секрета. В чём тут проблема?
Система-потребитель может получить секрет. В это время vault может выполнить ротацию пароля. И система с полученным секретом уже не сможет выполнить успешное подключение, например, к своей БД.
Для решения этой проблемы нами были реализованы «Комплекты ТУЗ» (ТУЗ — технологические учетные записи), естественно, с сохранением обратной совместимости методов получения секретов.
Суть доработки заключается в том, что наш модуль секретов должен быть способен обеспечивать поддержку комплектов пар ТУЗ, состоящих из смещенных друг относительно друга ТУЗов и их секретов, по срокам действия. Таким образом при запросе параметров доступа теперь выдавается, а при доставке — доставляется актуальный секрет с гарантированным остаточным сроком действия. Соответственно, ротация секретов ТУЗ настроена таким образом, чтобы ТУЗ и секреты, для которых настроена доставка, были актуальны на всех доступных ресурсах — источниках, их использующих.
Всё здорово! Мы развернулись в проекте, обеспечили гибкую масштабируемость, отличную производительность и высокую доступность, настроили и ввели нашу систему в эксплуатацию. «Учётки крутятся», клиенты подключаются. Но что видит команда сопровождения на графиках мониторинга потребления ресурсов серверов приложения? Видят они неравномерную утилизацию ресурсов.
Напомним архитектуру ванильного Opensource vault: обработка клиентских запросов выполняется на Master-узле. Slave-узел лишь перенаправляет запрос на мастер, ждёт ответа от мастера и возвращает его клиенту. Таким образом в кластере из 3, 5, 7 узлов утилизирует ресурсы только Master-узел. Slave-узлы по сути простаивают. Масштабирование только вертикальное: сколько бы узлов в кластер ни добавляли, производительность ограничена мощностью мастера.
Чтобы перераспределить нагрузку, нами была реализована функциональность по чтению со standby узлов.
Доработка заключается в обеспечении горизонтального масштабирования производительности системы для запросов, которые не приводят к записи информации в базу данных (физический бэкенд). Масштабирование становится возможным с помощью обработки таких запросов на stand-by узлах с перенаправлением «неподходящих» запросов на лидера кластера.
Таким образом, реализованы следующие функции:
Фильтрация запросов на stand-by узлах и перенаправление на лидера тех запросов, которые не могут быть обработаны на stand-by узлах (запросы на запись).
Авторизация запросов на стороне stand-by узлов.
Актуализация кэша stand-by узлов при изменении кэша лидера.
Ну и помимо сложных нефункциональных доработок, повышающих отказоустойчивость и производительность приложения, мы внесли ряд изменений в функциональную часть Vault:
сделали плагин для движка по управлению сертификатами, выпускаемыми внешними удостоверяющими центрами;
разработали плагин для движка по генерации SSH-ключей;
сделали плагин для движка по управлению учетными данными и Keytab-файлами системы на базе IPA и доработали плагины для стандартных движков, где добавляли методы аутентификации;
и прочее. Об этом, кому интересно, можем рассказать отдельно :)
Вот такие интересные задачи были решены нашей командой. Но впереди не менее интересные запланированы на будущие релизы.
Оксана Бубело, менеджер продукта ЦУП 2.0 Модуль управления секретами, Александр Сальков, руководитель группы разработки, и Константин Рыжов, архитектор продукта