Vault Kubernetes auth в CI/CD: pod-ы забирают секреты сами, ConfigMap с паролями ушёл в прошлое
Внедрили HashiCorp Vault Kubernetes auth-метод в CI/CD: pod-ы теперь получают секреты через ServiceAccount без хранения паролей в ConfigMap или env-переменных.
HashiCorp Vault 1.1 (март 2019) улучшил Kubernetes auth-метод и производительность: управление секретами стало стандартной практикой в K8s-пайплайнах.
Когда разворачиваешь Kubernetes-кластер, вопрос «где хранить пароли» стоит с первого дня. Классический ответ, который видим у большинства клиентов - Kubernetes Secrets в base64 плюс несколько копий тех же значений в ConfigMap «для удобства», и, конечно, те же переменные продублированы в .env-файле где-то в репозитории. CI/CD читает секреты через переменные окружения в Gitlab или Jenkins, которые туда положил кто-то из команды год назад и давно забыл как.
Это работает. Ровно до момента, пока не нужно ротировать - и оказывается, что никто не знает все места, где лежит пароль от базы.
В марте HashiCorp выпустил Vault 1.1 с заметными улучшениями Kubernetes auth-метода. Мы взяли это как повод наконец-то закрыть тему секретов в managed-инфраструктуре одного из клиентов по-человечески.
Что такое Kubernetes auth в Vault
Идея простая: pod в Kubernetes имеет ServiceAccount, к которому Kubernetes автоматически монтирует JWT-токен. Vault умеет принимать этот токен, проверять его через Kubernetes API и, если всё в порядке, выдавать Vault-токен с нужными правами.
То есть pod не хранит никакого заранее известного пароля или ключа. Он приходит к Vault и говорит: «я вот этот ServiceAccount в вот этом namespace, дай мне секрет для подключения к базе». Vault проверяет по Kubernetes API - да, такой ServiceAccount существует, namespace правильный, - и выдаёт токен с ограниченными правами и TTL.
Для приложения это выглядит как обращение к Vault Agent Sidecar или напрямую к Vault API при старте. Никаких паролей в переменных окружения, никаких ConfigMap с credentials.
Как мы это разворачивали
Vault у клиента уже стоял - использовался для хранения SSL-сертификатов и нескольких мануальных секретов. Это упростило задачу: не нужно разворачивать с нуля, нужно настроить Kubernetes auth-метод и переключить приложения.
Конфигурация на стороне Vault сводится к нескольким шагам:
- Включить auth-метод.
vault auth enable kubernetes- одна команда. - Настроить доступ к Kubernetes API. Vault нужен ServiceAccount с правами читать TokenReview и pod информацию. Создали отдельный ServiceAccount
vault-authв namespacevault, прописали ClusterRoleBinding с нужными правами. - Создать роли. Роль в Vault связывает Kubernetes ServiceAccount и namespace с Vault-политикой. Например, ServiceAccount
app-backendв namespaceproductionполучает права на чтение секретов изsecret/production/backend/*.
На стороне приложения мы пошли через Vault Agent в виде init-контейнера - он добавляется в Deployment вручную, монтирует общий emptyDir-volume и кладёт секрет в файл до старта основного контейнера. Выглядит так (упрощённо):
initContainers:
- name: vault-agent-init
image: vault:1.1
command: ["vault", "agent", "-config=/etc/vault/agent.hcl"]
volumeMounts:
- name: secrets
mountPath: /vault/secrets
volumes:
- name: secrets
emptyDir: {}
Vault Agent забирает секрет при старте pod-а и кладёт его в файл внутри общего volume. Приложение читает файл, а не переменную окружения. При ротации секрета Vault Agent может обновить файл без перезапуска pod-а - но это мы пока не трогали, это отдельная история.
Что сломалось по пути
Несколько мест, где споткнулись.
TokenReview-права. Vault-у нужно проверять JWT-токены ServiceAccount через Kubernetes API. Если ClusterRoleBinding настроен неправильно или ServiceAccount vault-auth не имеет нужных прав, Vault молча падает с ошибкой аутентификации, которую сложно интерпретировать без отладочных логов. Добавили VAULT_LOG_LEVEL=debug на время настройки - стало намного яснее.
Namespace в роли. Роль в Vault должна явно перечислять разрешённые namespace. Мы сначала поставили * для быстрой проверки, и забыли уточнить для продакшн-роли. Итог - pod из staging мог бы (теоретически) получить продовый секрет, если ServiceAccount называется так же. Поправили до явного списка namespace.
Vault Agent версия. Injector тянет Vault Agent image, версия которого должна совпадать с версией Vault сервера. У нас сначала стояла более старая версия image - Agent стартовал, но получал неожиданные ошибки при обновлении секретов. Явно зафиксировали версию через Helm values.
Состояние после внедрения
Из CI/CD убрали все прямые credentials. Gitlab CI теперь не хранит пароли от базы - он создаёт namespace и Deployment с нужными ServiceAccount и аннотациями, остальное делает Vault. Jenkins-job для миграций тоже переехал на Vault: job-pod получает DB credentials через Vault Agent при запуске.
Ротация стала управляемой. Раньше смена пароля от базы - это обход нескольких мест: Gitlab CI variables, ConfigMap, .env на сервере разработчика, который делал «быструю проверку» полгода назад. Теперь меняем секрет в Vault, pod-ы при следующем перезапуске получают новое значение.
Один вопрос остался открытым: динамические секреты Vault для PostgreSQL - когда Vault сам создаёт временные учётные записи с TTL для каждого pod-а. Это интересная возможность, но требует поддержки со стороны приложения (повторное получение credentials при истечении TTL). На текущем стеке клиента это требует изменений в коде - оставили на следующую итерацию.
Пока работаем на статических секретах с ручной ротацией через Vault. Это уже кратно лучше, чем ConfigMap с base64-паролем, который никто не менял с момента запуска.