Выбор инструментов для доставки секретов в Kubernetes. Наш путь delivery of secrets
Привет, Хабр! Меня зовут Натиг Нагиев, я Devops-инженер в МТС Диджитал.
Проект, над которым я сейчас работаю, занимается обеспечением авторизации внешних клиентов. Это Mission Critical система, поэтому нам нужно было ускорить и оптимизировать доставку секретов в контейнеры с микросервисом, избежать дополнительных рабочих нагрузок в kubernetes, гарантировать безопасность при доставке секретов и исключить внешние зависимости.
Сейчас де-факто стандартом для таких задач стало хранилище HashiCorp Vault, поэтому мы рассматривали и пробовали разные инструменты: Vault Agent Injector, Vault CSI Provider, External Secrets Operator и OpenBao —, но в итоге остановились на связке Bank-Vault и Vault Secrets Operator.
В этом посте я расскажу, чем они интересны и какие у них есть плюсы и минусы, а в продолжении — как мы реализовали итоговую систему.
Vault Agent Injector: удобный и безопасный, но потребляет слишком много ресурсов
По сути, это Kubernetes-контроллер, который перехватывает события типа Create и Update для под. Он анализирует наличие аннотации «vault.hashicorp.com/agent-inject: true» и при его наличии изменяет спецификацию пода с помощью других аннотаций. Это удобный и простой инструмент: мы добавляем лишь аннотации, а все действия по извлечению секретов из Vault и их шаринг в контейнер c микросервисом выполняются агентом
Инит-контейнер при запуске пода забирает секреты из Vault и предварительно загружает их в общий объем памяти, так называемый Volume Shared Memory перед запуском контейнера с микросервисом, это важно с точки зрения безопасности. Таким образом секреты не сохраняются в Kubernetes Secrets ни в каком виде. Кроме того, с помощью аннотаций можно как включить, так и отключить Sidecar-контейнер, который периодически проверяет актуальность наших секретов.
К сожалению, у Vault Agent Injector есть свои минусы:
Дополнительные рабочие нагрузки в виде init и sidecar containers, которые на тот момент мы не могли себе позволить, потому что наши кластеры не обладали значительными ресурсами. У нас много сервисов и гора инфраструктурных решений, связанных с мониторингом, логированием, трейсингом, ингресами, раннерами и так далее.
Не совсем тривиальный синтаксис темплейтинга в аннотациях, что также увеличивает сложность.
В целом Vault Agent Injector обеспечивает высокий уровень безопасности при доставке и обновлении секретов в микросервисах, минимизируя риски утечки данных. Однако он требует значительных ресурсов и его темплейтинг в аннотациях не сразу понятен и не прост. Поэтому мы стали рассматривать другие варианты.
External Secrets Operator: годный и хороший, но все-таки сторонний инструмент
External Secrets Operator автоматически синхронизирует секреты из внешних источников и добавляет их в Kubernetes в виде результирующих Opaque Secrets. Соответственно, у него есть провайдер с Vault, но нативно работает только KV Secret Engine. Для остальных типов Secret Engine необходимо использовать абстракцию в виде Vault Generator.
Один из ресурсов External Secrets Operator — SecretStore. По сути, это бэкенд, который определяет, какое хранилище секретов использовать и как к нему подключаться В нем указывается адрес кластера Vault и дополнительные опции, например, связанные с валидацией по TLS.
Второй — это External Secret, на нём остановимся чуть подробнее. Здесь стоит отметить две необходимые опции:
refreshInterval: показывает, как часто нужно обращаться за секретами в Vault, что влияет на скорость и оптимизацию доставки наших секретов;
secretStoreRef: это ссылка на SecretStore при обращении к Vault, благодаря которой External Secrets понимает, откуда забирать секреты.
External Secrets Operator покрывает основные кейсы работы с Vault и ее интеграции с kubernetes. Но мы его не используем, потому что это стороннее, а не вендорское решение. Плюс оператор External Secrets работает с внешним API хранилища секретов: облачного решения для хранения секретов, внешнего Vault за пределами кластера Kubernetes, или какого-либо другого решения для хранения секретов. Мы же для каждого кластера разворачиваем свой Vault, чтобы избежать единой точки отказа — в случае падения одного из кластеров.
Vault CSI Provider: хорошо решает кейсы, но есть внешние зависимости
Это обвязка поверх Hashicorp Vault. Мы взяли его в работу, потому что на первый взгляд он показался нам самым удачным:
Поставщик Vault CSI Provider легко развертывается включением одной директивы Helm чарте «csi.enabled=true»;
Можно монтировать секреты в файловую систему pod. Это и плюс, и минус, к чему я еще вернусь;
Он поддерживает преобразование секрета Vault в ключ Kubernetes Opaque Secret и, соответственно, в переменные среды pod;
Отсутствуют дополнительные рабочие нагрузки в виде init и sidecar контейнеров.
Однако у этого решения есть и минусы, которые в нашем случае оказались некритичными:
Имеется точка отказа в виде Storage, предоставляемыми внешними CSI-хранилищами;
Рутинное преобразование секретов из Vault в Opaque Secrets. От нас требуется прописать в манифесте Secret Provider Class маппинг каждого секрета из Vault в ключ Opaque Secret. Если у нас очень много секретов для микросервиса, то их нужно будет расписать в манифесте Secret Provider Class;
Для монтирования секретов в файловую систему pod используется Pod Security Policies. Он был объявлен как deprecated в Kubernetes v1.21, и окончательно удален в 1.25.
Работа с Vault CSI Provider
Мы сначала установили драйвер CSI Secret Store, используя обычный Helm чарт. Из интересных опций здесь:
syncSecret.enabled=true — включаем синхронизацию наших секретов.
enableSecretRotation=true — активируем опцию enableSecretRotation, которая запускает ротацию секретов.
rotationPollInterval=30s — настраиваемый интервал ротации, то есть период, с которым CSI-драйвер впоследствии проверяет и актуализирует наши секреты.
Затем мы задеплоили Vault CSI Provider: он ставится из того же чарта, что и сам Vault, и активируется одной опцией — csI.enabled=true. Включив ее, мы задеплоили на все наши worker ноды Vault CSI Provider в качестве Daemonset, что позволяет нам монтировать секреты в файловую систему pod.
Ключевой сущностью в связке csi-secret-store/vault-csi-provider является CustomResource «SecretProviderClass», который поставляется вместе с драйвером csi-secret-store. Основные его задачи — монтирование секретов в файловую систему pods (Volume/VolumeMounts) и преобразование секретов Vault в секреты типа Opaque в k8s.
Стоит отметить важный момент в манифесте SecretProviderClass. Параметр «objectName» — это имя объекта для маппинга секретов из Vault в ключ Opaque Secret в k8s. Он указывается как параметр для сбора секретов из Vault, так и в secretObjects для преобразования полученных секретов из Vault в секреты в k8s.
Чтобы наши секреты попали в pod из Vault, SecretProviderClass монтируется через volumeMounts. Тут стоит обратить внимание на директиву volumeAttributes.secretProviderClass — с ее помощью мы передаем имя описанного выше secretProviderClass для того, чтобы csi-driver смог корректно смонтировать volume c секретами в pod.
Также отмечу время аренды TTL. Если есть много микросервисов и нет возможности выделить кластеру Vault дополнительный объем хранилища, то возникает проблема с переполнением кластера незавершенными арендами, то есть c leases. По умолчанию аренда выдается на 60 минут, но вопрос решается изменением одной опции командой «vault auth tune -default-lease-tt|=30s kubernetes/» (так как метод авторизации у нас kubernetes). Тем самым мы закрываем проблему с незавершенными арендами.
Что мы получили в итоге
Спустя год работы с Vault CSI Provider мы поняли, что он не решает задач безопасности в kubernetes и у него есть проблемы:
Автоматизация рутинных ручных действий после развертывания кластера Vault средствами IaC, таких, как включение метода авторизации, подключение Secret Engine и изменение тех же параметров TTL, о которых уже упоминалось. Рассмотрим это подробнее далее.
Периодические отвалы CSI-хранилищ, что связано с присутствием внешних зависимостей. При этом мы не собирались изобретать велосипеды, писать скрипты или использовать какие-то внешние решения для авторизации, либо ставить отдельный инстанс Vault для распечатывания. Выйти на дополнительные внешние решения нам также не хотелось, так как у нас уже был опыт работы с Consul, и это создало еще одну внешнюю зависимость в виде стороннего бэкенда.
Пришлось искать другие варианты.
Мы натолкнулись на OpenBao, появившийся как ответ на изменение лицензии HashiCorp Vault с MPL v2.0 на BSS v1.1. Но на тот момент он показался нам сырым для интеграций с kubernetes, хотя, по сути, это форк версии HashiCorp Vault 1.14.8.
Итоговое решение: Bank-Vaults и Vault Secrets Operator
Вернувшись к решениям от HashiCorp Vault, мы взяли в работу такой инструмент, как Vault Secrets Operator. С помощью него мы смогли закрыть проблемы, связанные с использованием Vault CSI Provider:
Перестали описывать каждый секрет в values файлах микросервисов.
Ушли от монтирования секретов в файловую систему в качестве volume/volumeMounts, так как все наши сервисы идентичны по функциональности и могут забирать секреты из переменных окружения;
Отказались от использования CSI-хранилищ тем самым ушли от внешних зависимостей
Кроме этого, мы параллельно уходили от ванильного Vault и пришли к Bank-Vaults. Это решение развертывает кластер, в котором уже есть инструменты для автоматизации рутинных действий.
С его помощью мы средствами laC реализовали:
Распечатывание кластера Vault. Мы автоматизируем процесс создания и доставки Opaque Secret с Unseal токенами.
Изменение опций Vault: начальные параметры, которые необходимо задать в дефолтной конфигурации сразу после развёртывания кластера Vault, теперь доставляем вместе с процессом развёртывания кластера Vault.
Связка Vault Secrets Operator и Bank-Vaults хорошо показала себя в производственной среде. И сейчас мы остановились на ней.
В следующем посте я расскажу об использовании данного решения, о его плюсах и минусах, а также о задачах, которые остались открытыми. На этом у меня все, готов ответить на ваши вопросы.