
Мы все знаем, что HashiCorp Vault — это фактический стандарт для хранения секретов, а Kubernetes — для размещения приложений. Но как подружить их вместе? Существует множество инструментов для интеграции Vault с Kubernetes, и каждый из них имеет свои плюсы и минусы. Как выбрать подходящий?
В этой статье, созданной по мотивам выступления на DevOpsConf 2025, вы узнаете о самых популярных инструментах доставки секретов из HashiCorp Vault в Kubernetes, таких как External Secrets Operator, HashiCorp Vault Secrets Operator, HashiCorp Vault Agent Injector, HashiCorp Vault CSI Provider, Bank Vaults-Vault Secrets Webhook. Для каждого инструмента будет приведён пример настройки, объяснено, как именно секрет попадает в приложение, а также мы с вами сравним их с точки зрения ротации секретов и удобства использования.

Меня зовут Михаил Кажемский, я Lead DevOps из Hilbert Team. Я в IT уже больше 10 лет и пришёл в DevOps из разработки, поэтому побывал по обе стороны баррикад. Соавтор ряда курсов для инженеров на Яндекс Практикум по направлениям DevOps, Security и Data.
Hilbert Team — провайдер IT-решений для крупного и среднего бизнеса в области облачных технологий, DevOps, DevSecOps, DataOps, MLOps и FinOps. Партнёр Yandex Cloud со специализациями Yandex Cloud Professional по направлениям DevOps и Data Platform.
Используйте оглавление, если не хотите читать текст полностью:
→ Как работает HashiCorp Vault
→ Проблема доставки
→ Инструменты доставки
→ External Secrets Operator
→ HashiCorp Vault Secrets Operator
→ HashiCorp Vault CSI Provider
→ HashiCorp Vault Agent Injector
→ Bank-Vaults Secret Injection Webhook
→ Итоги
О чём мы будем говорить: основные понятия
Секреты, о которых мы будем говорить, это не рецепт любимого бабушкиного пирога и даже не алгоритм работы ключевого продукта. Не персональные данные (вы думаете это ещё секрет?), не случай, когда вы уронили и быстро подняли под, пока никто не заметил, не одноразовые пароли/токены — на то они и одноразовые.
Речь пойдёт о статических ключах доступа, токенах и паролях. Сегодня они и будут нашим секретом. Пример: дефолтный пароль приложения, подключение к базе, токен, иногда большой секрет формата private key.

Как можно догадаться из названия статьи, речь пойдет о доставке секретов в Kubernetes. Если очень упростить, то Kubernetes — это оркестратор для запуска наших контейнерных приложений. В нашей практике мы стараемся придерживаться подхода Infrastructure as Code (IaC) и разворачиваем полезную нагрузку через GitOps-контроллеры (например, Argo CD). Внутри — как бизнесовые, так и инфраструктурные сервисы, у каждого из которых свой набор конфигураций.
Вариантов, где могут жить конфигурации, несколько. Вот самые популярные:
В переменных окружения (env). Идеальный случай — если приложение следует принципам Twelve-Factor App и хранит настройки в environment.
В файлах. Самый частый вариант: YAML, JSON, INI — всё, как мы любим. Особенно характерно для инфраструктурных приложений.
Во внешних сервисах в рантайме. Приложение может тянуть конфиги из внешнего источника: etcd, Consul или прямо из Vault, но об этом поговорим отдельно.
Как работает HashiCorp Vault
Следующее понятие — HashiCorp Vault. На рынке существует множество систем управления секретами, и каждый подходит под свои задачи, у меня даже был об этом доклад на DevOpsConf’24. Но Vault — это уже взрослый уровень. До него, как правило, нужно дорасти: технически, организационно и по масштабу инфраструктуры. Если у вас небольшой проект с парой скриптов, то Vault будет излишним. А если у вас большая инфра с серьёзными требованиями к безопасности, политиками доступа, аудитом и интеграциями, то скорее всего подойдет.
Теперь давайте поговорим о том, как работает HashiСorp Vault. Принцип простой: есть клиенты (например, ваше приложение), которые стучатся в Vault. Чтобы получить доступ к секрету, нужно пройти аутентификацию и авторизацию. Если все пункты пройдены успешно, Vault возвращает нужные данные: пароль, токен, ключ и т. д.
Секреты хранятся в так называемых бэкендах (engine) — это могут быть обычные key-value хранилища, а могут быть и более продвинутые бэкенды с динамическими секретами, сертификатами и пр.
Основные достоинства HashiCorp Vault:
On-premises и Open Source (есть UI);
Поддержка широкого спектра движков и способов аутентификации;
Удобная настройка политик доступа на основе RBAC;
Версионирование секретов;
Аудит и возможность интеграции с SIEM;
Интеграция практически с чем угодно;
Возможность управлять с помощью инструментов IaC (Terraform, Ansible, Operator + GitOps) и API.
Один из главных плюсов HashiСorp Vault — отсутствие привязки к облаку. Это on-premises решение с открытым исходным кодом: можно развернуть у себя, всё контролировать и не зависеть от внешних сервисов. Но Open Source не означает «бесплатно». Vault — мощный инструмент, и чтобы он действительно защищал секреты, нужны эксперты. При неправильной настройке даже самый безопасный инструмент может превратиться в решето. Как правильно настраивать Vault — тема для отдельного доклада. Сегодня мы в это погружаться не будем, сконцентрируемся на доставке секретов.
Как устроен наш HashiСorp Vault
У нас есть секрет для нашего приложения.

Есть политика доступа к этому секрету — просто чтение.

Поскольку мы доставляем секреты в Kubernetes, то можем использовать способ аутентификации Kubernetes (Kubernetes auth).

Проблема доставки
У нас есть Kubernetes с нашими приложениями и Vault с секретами от этих приложений. Задача — доставить нужный секрет в нужное приложение. Чтобы это произошло, кто-то из Kubernetes должен пройти аутентификацию, получить доступ и забрать нужные данные.
Но важно: мы НЕ будем рассматривать вариант, где само приложение напрямую ходит в Vault. Такой подход требует, чтобы разработчики добавляли отдельную логику в код, что не всегда возможно. Мы пойдём другим путём — настроим доставку секретов внешними инструментами, без изменений в самих приложениях.
Представьте, что у нас есть курьер. Он сам всё аккуратно доставит туда, куда надо.

Какие проблемы могут быть с доставкой:
Кто будет курьером?
Как попасть в подъезд?Сложно ли его интегрировать?
Много ли потребляет?
Как будет аутентифицироваться?
Куда будем доставлять?
Как обновить секрет и вовремя доставить его в приложение?
Инструменты доставки
Давайте рассмотрим, на наш взгляд, 5 самых популярных инструментов доставки секретов:
External Secrets Operator
HashiСorp Vault Secrets Operator
HashiСorp Vault CSI Provider
HashiСorp Vault Agent Injector
Bank-Vaults Secret Injection Webhook
И сравним их по следующим свойствам:
Способы аутентификации;
Поддерживаемые бэкенды;
Доставка в k8s Secret;
Нагрузка на инфраструктуру;
Доставка в файл;
Доставка в environment;
-
Автоматическая синхронизация.
1. External Secrets Operator
Наверное, это самый универсальный инструмент, который интегрируется почти с любым KV-хранилищем:
AWS Secrets Manager
Azure Key Vault
Google Cloud Secret Manager
HashiCorp Vault
Yandex Lockbox
1Password Secrets Automation
и другие
Естественно, External Secrets Operator интегрируется с Vault. Работает он относительно просто. Мы его установили, и у нас появились три пода, которые отвечают за синхронизацию секретов.

Чтобы аутентифицироваться, понадобится сервисный аккаунт, поскольку у нас Kubernetes auth.

Раздеплоили сервисный аккаунт, создали в нашем auth роль eso-ro.

Под каждый namespace или под каждое приложение мы создаём отдельную роль и в ней описываем, с какого namespace будем подключаться и с каким именем сервисного аккаунта заходить после настройки.

Следующая сущность, которая нам нужна для доставки секрета, — SecretStore, который описывает подключение к Vault. Мы указываем куда идти, как и с каким сервисным аккаунтом.

На этом настройка закончена. Проверяем, что всё работает: делаем describe или просто смотрим status у нашего кастомного ресурса SecretStore.

Если всё настроено правильно, в статусе увидим status: “True”
. Если что-то пошло не так (например, проблема с подключением к Vault или ошибка аутентификации), это отразится в status, describe или в логах контроллера. Очень удобно для отладки.
Важно: SecretStore можно настроить не только на уровень одного namespace, но и на весь кластер (cluster-wide). В последнем случае потребуется выдать более широкие права, но такая опция есть, если нужно делиться Vault-секретами между разными неймспейсами.
Следующий важный объект — ExternalSecret. Настраивается он довольно просто: вы описываете, откуда брать секрет в Vault'е, какие ключи нужны и куда их положить в Kubernetes. Всё — секрет доставится в kind Secret сам.
Мы просто говорим: «Сходи в указанный SecretStore и забери нужные значения из секрета, например, dc25/superapp
». Всё остальное ExternalSecret сделает сам: подтянет данные и положит в Kubernetes Secret.
Он «отвечает»: «Окей, вот нужные секреты из Vault, кладу их в указанный Kubernetes Secret». Всё просто: вы настраиваете, откуда брать и куда класть, а остальное — дело оператора.

Смотрим статус — секрет успешно синхронизирован. При необходимости можно задать refreshInterval, чтобы секрет периодически обновлялся из Vault. Это особенно полезно, если вы всё-таки обновляете секреты. Пусть новые значения автоматически подтягиваются без ручных вмешательств.

Всё готово. Заходим в созданный Secret в Kubernetes и убеждаемся, что нужные данные доехали из Vault. Значения ключей на месте, можно подключать к приложению.

Если вы следуете принципам Twelve-Factor и храните часть конфигурации приложения в environment, а часть — в виде файлов (например, приватные ключи, сертификаты или YAML-конфиги), можно пойти простым путём: создать второй ExternalSecret, настроенный точно так же, как первый, но с другим составом данных.
В этом ExternalSecret в разделе data указываются ссылки только на те ключи Vault, которые вы хотите положить в файл. После синхронизации в Kubernetes появится второй Secret, и его уже можно будет смонтировать в контейнер через volumeMounts, как обычный файл. Так удобно передавать в приложение, например, приватный ключ, без необходимости парсить его из environment-переменной.

Теперь нужно, чтобы приложение получило доступ к созданному секрету. Для этого мы монтируем Kubernetes Secret как volume в нужное место внутри контейнера с помощью volumeMounts, а environment-переменные мы получаем через secretRef.

Заходим в под, в контейнер нашего приложения, и выполняем команду env.

Видим, что переменные окружения успешно подтянулись — всё на месте. Проверяем, смонтировались ли файлы.

Файлы на месте, приложение успешно стартует и получает нужные данные.
Теперь представим, что мы обновили секрет в Vault: например, изменили apiToken и приватный ключ. Видим, что данные в kind: Secret
обновились.

Секрет действительно обновился, потому что данные обновляются оператором в заданный при настройке интервал. Всё выглядит отлично… пока мы не заходим в контейнер.

А там — ничего не поменялось. Почему так вышло?
Во-первых, переменные окружения (environment) не обновляются «на лету» в уже запущенном процессе. Контейнер получил их при старте и дальше с ними живёт. Чтобы изменения вступили в силу, под нужно перезапустить.
Во-вторых, казалось бы, хотя бы файлы должны были обновиться, ведь мы смонтировали секрет через volumeMounts. Но и они не поменялись. Причина в том, что при монтировании мы использовали параметр subPath. В этом случае Kubernetes просто копирует файл внутрь контейнера при запуске, и после этого он «отрывается» от оригинального секрета. Даже если сам секрет обновится, файл в контейнере останется прежним.

Что нужно сделать? Можем просто удалить subPath и смонтировать весь секрет в какую-то директорию целиком.

В этом случае Kubernetes создаёт симлинки на файлы прямо из volume. Это значит, что при обновлении секрета файлы автоматически обновятся внутри контейнера без необходимости перезапуска пода. Учитывайте это при монтировании секретов и вообще чего-либо в контейнер.

Рассмотрим External Secrets Operators по нашим параметрам:
Способы аутентификации. External Secrets поддерживают не все, но большую часть существующих способов аутентификации, в том числе Kubernetes Auth, AppRole, JWT, интеграции с зарубежными облаками.
Поддерживаемые бэкенды. External Secrets работает только с key-value бэкендом. Другие типы хранилищ Vault он не поддерживает. Есть возможность вручную прописать секреты с помощью специального ресурса, но по факту только KV поддерживается «из коробки».
Нагрузка на инфраструктуру. External Secrets Operator — это всего лишь несколько деплойментов, которые работают в фоне. Они не создают значительной нагрузки, потребляют минимум ресурсов и быстро обновляют секреты.
Доставка в k8s Secret. Фактически только туда и доставляет.
Доставка в файл. В файл непосредственно не доставляет, а делает только посредством k8s Secret.
Доставка в environment. В environment непосредственно не доставляет, а делает только посредством k8s Secret.
Автоматическая синхронизация: работает, но с ограничениями. Секреты действительно автоматически обновляются, если вы монтируете их как файлы (volume mount) или используете сторонний оператор/механизм, который будет по изменению секрета рестартовать под или деплоймент, чтобы изменения вступили в силу. Без этого приложение может не увидеть обновлённые значения, особенно если речь про environment-переменные.
У HashiСorp есть более специализированное решение, которое работает схожим образом, но заточено только на Vault.
2. HashiСorp Vault Secrets Operator
Vault Secret Operator от компании HashiСorp без проблем устанавливается с помощью Helm. Контроллер деплоится обычным деплойментом.

Создаём сервисный аккаунт. Вместо SecretStore используем ресурс VaultAuth. В нём указываем всё необходимое для подключения к Vault: в первую очередь, какой сервисный аккаунт будет использоваться для аутентификации. Этого, как правило, достаточно. Остальные параметры подключения к Vault (например, адрес и пути) можно задать через параметры установки самого оператора, обычно через Helm.

Следующий ресурс — VaultStaticSecret. Он заменяет ExternalSecret и работает схожим образом: указываем, какие данные из Vault нужно забрать, с каким интервалом обновлять (refreshInterval) и в какой Kubernetes Secret (destination) положить.

Смотрим в этот секрет, а туда приезжает всё целиком из нашего хранилища. Более того, ещё и raw data со всем секретом в формате JSON, закодированные в base64.

Это поведение настраивается. Есть директива transformation, а в ней параметр excludeRaw: true
. Также есть директивы includes/excludes
. Например, можно заинклюдить конкретные property. Так, в наш секрет приезжают только те сущности, которые мы хотим.

Кроме директивы transformation, в Vault Secrets Operator есть ещё одна полезная настройка — rolloutRestartTargets
. С её помощью можно настроить автоматический рестарт нужных Deployment, ReplicaSet, StatefulSet или Rollout, когда секрет обновляется. Это удобно: не нужно городить дополнительную автоматику, всё работает «из коробки».

Вы заранее указываете, какие ресурсы нужно перезапускать при обновлении секрета. Это и есть основное назначение rolloutRestartTargets
— автоматизировать перезапуск нужных компонентов при изменениях в секретах.
С точки зрения свойств:
Способ аутентификации. Здесь примерно то же, что у External Secrets Operator — Kubernetes Auth, AppRole, JWT и прочие облачные провайдеры.
Поддерживаемые бэкенды. Поддерживаются любые бэкенды, в том числе dynamic secrets, поскольку это инструмент HashiСorp.
Нагрузка на инфраструктуру. Минимальная, только один Deployment.
Доставка в k8s Secret. Да, доставляет только в k8s Secret.
Доставка в файл. Только через k8s Secret.
Доставка в environment. Только через k8s Secret.
Автоматическая синхронизация. При изменениях секрета может рестартовать под «из коробки».
Что если мы не хотим доставлять какой-то секрет в Kubernetes Secret? Kubernetes Secret — это не совсем секрет, и на него существует много векторов атак. Нам бы хотелось их минимизировать, то есть в этом случае просто не использовать Kubernetes Secret. Следующий инструмент, который нам поможет это сделать, — это Vault CSI Provider.
3. HashiСorp Vault CSI Provider
Основная задача Vault CSI Provider — доставка секретов в виде файлов, так как он реализует Container Storage Interface (CSI).
Как это работает? Мы устанавливаем CSI Provider в кластер.
При установке обратите внимание на опции enableSecretRotation и rotationPollInterval. Без их настройки автоматическая синхронизация секретов работать не будет, хотя в большинстве базовых примеров об этом не упоминается. Учтите этот момент при деплое.
Проверяем, как разворачивается Vault CSI Provider. Он устанавливается не в виде Deployment, а как DaemonSet. Это значит, что его поды запускаются на каждой ноде кластера.

Для аутентификации нам нужен сервисный аккаунт и сущность SecretProviderClass, в которой мы описываем подключение к Vault. Здесь же мы описываем, какие сущности хотим держать в этом SecretProviderClass, а также какие property хотим забирать из Vault.

Дальше всё просто. Подключаем SecretProviderClass к поду как обычный volume. Затем через volumeMount указываем, куда именно монтировать — например, в директорию /mnt/secrets
.

Там мы видим ссылки на наш volume, и предполагаем, что обновление пройдет успешно. Так оно и будет.


Если вы всё же хотите использовать этот инструмент для монтирования переменных окружения (environment), то понадобится Kubernetes Secret. Vault CSI Provider умеет при монтировании создавать объект kind: Secret
с нужными значениями и затем подключать его в контейнер как переменные окружения — всё автоматически.

Особенность в том, что созданный секрет существует только на время жизни пода: когда под удаляется, секрет исчезает вместе с ним. Новый секрет создаётся только при запуске пода, а не при создании SecretProviderClass. Выглядит это немного как «костыль», всё-таки основная задача Vault CSI Provider заключается в доставке секретов через монтирование файлов.
Свойства Vault CSI Provider:
Способы аутентификации. Очень ограничены. Поддерживаются только Kubernetes Auth и JWT.
Поддерживаемые бэкенды. Практически все.
Доставка в k8s Secret. Vault CSI Provider умеет доставлять в k8s Secret, но лучше не надо.
Нагрузка на инфраструктуру. Уже чуть больше. Поскольку всё деплоится DaemonSet’ами, то зависит от количества нод в инфре.
Доставка в файл. Да, это его основная задача.
Доставка в environment. Напрямую в контейнер environment доставлять не умеет.
Автоматическая синхронизация. Да, поддерживается. Однако после монтирования секрета Vault CSI Provider сам его не обновляет и не инициирует рестарт. Секрет просто существует в текущем состоянии, и чтобы получить обновлённые данные, нужно вручную перезапустить под — тогда секрет пересоздастся.
Помимо Vault CSI Provider у HashiСorp есть ещё достаточно мощный инструмент.
4. HashiСorp Vault Agent Injector
HashiСorp Vault Agent Injector устанавливается как часть основного чарта Vault (через параметр enabled=true и сопутствующие настройки). В кластере он разворачивается как обычный webhook.

Работает это так. У нас есть сервисный аккаунт, этого достаточно для настройки интеграции. Дальше в под добавляется аннотация agent-inject: true
.
Когда Webhook видит такую аннотацию, он реагирует: «О, ты запускаешься, я добавлю init-контейнер». Этот init-контейнер монтирует секреты в /mnt/secret:
он запрашивает их у Vault и сохраняет.
После этого к поду подключается второй контейнер — vault-agent. Init-контейнер завершает работу, а vault-agent продолжает жить вместе с подом и следит за тем, чтобы содержимое в /mnt/secret всегда было актуальным, регулярно обновляя его.
Важно: vault-agent по умолчанию стартует с достаточно большими запросами ресурсов. По дефолту vault-agent запускается с такими параметрами:

Чарты можно перенастроить, но если этого не сделать, vault-agent по умолчанию может запрашивать много ресурсов. Например, при запуске 10 подов, каждый агент запросит по 250 mCPU — в сумме это 2,5 CPU. При 100 подах — уже 25 CPU. Если не учесть это при деплое, шедулер может не разместить такие поды.
Что и куда монтировать задаётся в аннотациях пода: указывается путь, имя файла и секрет. Также адрес сервиса, путь аутентификации (AuthPath), роль и сервисный аккаунт, указанный в поде. Через аннотацию agent-inject-secret можно, например, задать -all.txt — это будет имя итогового файла.

В /mnt/secret/all.txt
появляется файл /mnt/secret/all.txt
со следующим содержимым:

При этом содержимое не в формате JSON, а в виде обычного текста: просто список ключей и их значений. Это не всегда удобно, особенно если в одном секрете лежит несколько разных настроек. Но у Vault Agent Injector есть полезная возможность — шаблоны (template rendering). С их помощью можно, например, сформировать файл private.key так, чтобы в него попал только нужный ключ, например APP_KEY, из указанного секрета.

Также можно собрать более сложный секрет, например, строку подключения для Postgress или что-то подобное: шаблоны позволяют задать нужный формат. В результате после запуска пода мы заходим внутрь и видим, что private.key на месте.

При этом sidecar-контейнер (vault-agent) будет постоянно следить за секретом и обновлять его содержимое, так как он смонтирован в под.
Для environment-переменных в официальной документации Vault Agent Injector есть один «грязный» лайфхак.

Можно сделать, например, файл .env на основе шаблона, в котором заранее прописаны нужные export-строки с переменными. Затем в entrypoint контейнера пода просто добавить команду source .env, чтобы загрузить эти переменные в окружение, и после этого запустить основной процесс.

Получается, что переменные окружения (environment variables), доставленные через файл .env, реально загружаются в процесс, запущенный внутри контейнера.
Помимо этого «грязного» способа с подменой entrypoint, Vault Agent Injector также поддерживает полноценный штатный механизм — supervisor mode. Он настраивается через отдельную большую ConfigMap с расширенными параметрами конфигурации взаимодействия с Vault.
В этом режиме Agent Injector может запускаться как entrypoint контейнера, подменять его, загружать переменные окружения и запускать основной дочерний процесс. При изменении секрета агент сам отправит сигнал (например, SIGTERM) дочернему процессу. Если приложение умеет корректно реагировать на этот сигнал, оно перечитает конфигурацию. Если нет, то агент завершит процесс, и он перезапустится. При этом сам под останется живым, перезапустится только исполняемый процесс внутри.
Таким образом, Vault Agent Injector действительно умеет напрямую доставлять и обновлять переменные окружения и файлы, не монтируя их через Kubernetes Secret, и без необходимости рестартовать весь под.
Свойства Vault Agent Injector:
Способы аутентификации. Поддерживает абсолютно все.
Поддерживаемые бэкенды. Все.
Нагрузка на инфраструктуру. Немаленькая — Vault Agent Injector живёт вместе с подом, работает постоянно, и это может быть ощутимо для кластера. Особенно если у вас десятки или сотни подов: каждый будет запускать отдельный агент, который держит соединение с Vault, следит за обновлением секретов и выполняет дополнительную логику. Это всё добавляет нагрузки, и её обязательно нужно учитывать при планировании ресурсов.
Доставка в k8s Secret. Нет, только непосредственно в сущности пода.
Доставка в файл. Есть.
Доставка переменных в environment. Только в режиме супервизора. Этот режим включает довольно сложную и хрупкую логику работы: агент становится entrypoint-ом, управляет запуском основного процесса, следит за обновлениями и может отправлять сигналы на перезапуск. Это всё звучит красиво, но на практике добавляет много сложностей и нестабильностей. Поэтому мы не рекомендуем использовать режим супервизора, если можно обойтись более простыми и надёжными решениями.
Автоматическая синхронизация. Поддерживается.
Есть ещё один инструмент, но не от HashiСorp, а от компании Bank-Vault (в прошлом называлась Banzai Cloud).
5. Bank-Vaults Secret Injection Webhook
Bank-Vaults Secret Injection Webhook — более простой инструмент. Устанавливается как webhook и работает в виде одного deployment’а.

Для работы нужен сервисный аккаунт. Всё настраивается с помощью аннотаций прямо в манифесте пода. В аннотациях указываем, куда ходить и какой сервисный аккаунт использовать.
А дальше всё становится очень удобно. Этот инструмент как раз предназначен для доставки секретов в переменные окружения. Достаточно в секции env указать ссылку на нужный секрет, и он подтянется автоматически. Кроме того, можно использовать его и с ConfigMap: прописываете в файле настройки приложения, а вместо значений — такие же ссылки на секреты. Всё работает так же.

Затем Webhook замечает аннотацию, но только если в манифесте действительно есть ссылка на Vault. В таком случае он автоматически добавляет в контейнер специальный бинарник vault-env.
Этот vault-env запускается как entrypoint для вашего приложения. Он сам обращается в Vault за нужными секретами, используя указанный сервисный аккаунт, и подставляет значения в нужные переменные.
При этом вам ничего дополнительно настраивать не нужно, всё работает автоматически на основе аннотаций и ссылок на Vault.

После запуска пода можно зайти внутрь (если разрешён Shell) и убедиться, что переменные окружения присутствуют: вы увидите те же самые ссылки на секреты, что были прописаны ранее. На первый взгляд может показаться, что секретов нет, в окружении других процессов они действительно не отображаются. Но если посмотреть содержимое environ у процесса с PID 1 (основного процесса в контейнере), то нужные переменные окружения с секретами там есть.
Чтобы окончательно убедиться, что всё работает правильно, смотрим логи запуска контейнера. В логах видно, что был запущен процесс с нужным entrypoint. Затем выполняем команду env | grep APP
, видим, что все нужные переменные, включая секреты, на месте.

Вы можете убедиться в этом вручную: зайдите в под с включённым Shell, вызовите утилиту vault-env, и вы снова увидите те же самые значения. Всё работает корректно: переменные доставлены и доступны именно там, где должны быть.

Защититься от утечек можно просто — запретите exec в поды. Это не даст возможности зайти внутрь и посмотреть переменные окружения.
Важно: vault-env не создаёт промежуточных сущностей. Секреты нигде по пути не сохраняются, они доставляются напрямую в память процесса и остаются только там.

Webhook в Bank-Vaults умеет больше, чем просто подменять environment-переменные. Он может мутировать и другие Kubernetes-объекты, такие как Secrets, ConfigMaps и любые Custom Resources.
Как это работает:
Вы добавляете аннотацию в нужный объект, например, Secret.
В data указываете ссылки на секреты в Vault (если используете data, значения должны быть закодированы в base64).
Webhook находит аннотацию, идёт с нужным сервисным аккаунтом в Vault.
Забирает нужные значения и заменяет ссылки на реальные данные.
В результате объект уже содержит секретные данные прямо из Vault.
Всё происходит прозрачно, без лишних сущностей, и в том же объекте, который вы аннотировали.
Вначале мы упомянули, что используем GitOps-контроллеры. Часто секреты у нас тоже описаны через Git. Мы всё настроили, аннотации добавили и ожидаем, что всё мутирует. Но в итоге ничего не меняется.
Почему? Потому что GitOps-контроллер просто перезаписывает объект так, как он описан в Git-репозитории. Он считается «главным» в кластере и возвращает всё в исходное состояние. Это нужно учитывать: GitOps-контроллер может перебить результат мутаций, так же как и в случае с HPA, если вы прописали количество реплик в Git, GitOps будет возвращать их к этому значению. Можно настроить обход, например, через ignore-поля.
Отдельно стоит упомянуть, что в некоторых чартах, например, Grafana или Prometheus, чувствительные данные можно передать через Secret. Если вы правильно настроите аннотации и GitOps-контроллер не будет мешать, то ссылка на Vault заменится на реальные данные.
Свойства Secret Injection Webhook:
Способы аутентификации. Примерно как у базового External Secrets, необходимый минимум существует.
Поддерживаемые бэкенды. Только KV.
Нагрузка на инфраструктуру. Маленькая, запускается только init контейнера.
Доставка в k8s Secret. Есть.
Доставка в файл. Нет, только если смонтировать через секреты.
Доставка в environment. Да, основная задача.
Автоматическая синхронизация. Не поддерживается, и это главное ограничение. Обновление происходит только вручную или по внешним событиям. Чтобы под перечитал переменные, приходится делать рестарт вручную или настраивать дополнительную логику, например, обновление аннотаций.
Итоги
В этой статье было рассказано о пяти инструментах — это универсальный External Secrets Operator, три инструмента от компании HashiСorp Vault и внешний от Bank-Vault. Их свойства вы увидите в таблице:

По способам аутентификации самым универсальным является Injection Webhook.
По бэкендам — самые универсальные — продукты от HashiСorp.
В k8s Secret не умеет доставлять только Injection Webhook от Vault.
В файл могут доставлять Vault CSI Provider и Vault Injection Webhook.
В environment умеет доставлять Vault Injection Webhook и напрямую, полноценно — Bank-Vaults Injection Webhook.
Автоматическую синхронизацию поддерживают все, кроме Bank-Vaults Injection Webhook.
Мы видим, что серебряной пули нет — инструменты все разного рода, под разные задачи. Если вы понимаете риски и знаете, как работать с k8s Secret, то есть два основных инструмента, которые отвечают за это — External Secrets Operator и Vault Secret Operator. Они достаточно универсальны и популярны, легко настраиваются. Но если у вас только Vault, используйте официальный оператор от HashiСorp — Vault Secret Operator.
Если мы хотим миновать секрет, то у нас остаётся на выбор три инструмента: Vault CSI Provider, Vault Injection Webhook, Bank-Vaults Injection Webhook.
Если нужно работать с файлами — выбирайте Vault CSI Provider: он берёт секрет и монтирует его в файл.
Если нужны только переменные окружения — используйте Bank-Vaults Injection Webhook.
Большинство наших приложений работают именно с environment, и в нашей практике мы в основном применяем Bank-Vaults Injection Webhook — его более чем достаточно.
В редких случаях, когда нужно доставить секрет в файл, мы комбинируем его с другими инструментами. Поскольку используем ту же auth role и сервисные аккаунты, достаточно просто добавить другой инструмент, например, Secret Operator или Vault CSI Provider.
Vault Agent мы не используем, хотя у него хорошие возможности для шаблонизации файлов.
Спасибо за прочтение моей статьи, если вы больше любите смотреть, то есть видео моего выступления, а ещё подписывайтесь на наш телеграм-канал!
В конференции DevOpsConf я участвую уже третий год подряд, не пропустите новый сезон, будет много интересного.