Vault 1.7 + External Secrets Operator: убираем хардкод паролей из Kubernetes
Разворачиваем Vault 1.7 для управления секретами в Kubernetes: External Secrets Operator забирает динамические credentials из Vault и кладёт в Secret, TTL - 1 час.
HashiCorp Vault 1.7: улучшенные dynamic secrets для AWS/GCP/Azure, автоматическая ротация credentials
У одного из заказчиков в кластере Kubernetes обнаружился классический грех: AWS-ключи, ключи сервисных аккаунтов GCP и пароли к PostgreSQL жили прямо в ConfigMap и в аргументах деплоя. Кто-то когда-то «временно» положил туда, потом забыл, потом это разъехалось по нескольким окружениям. Секреты менялись раз в несколько месяцев вручную, когда кто-то вспоминал. Vault 1.7 с его переработанными dynamic secrets дал повод наконец разобраться с этим нормально.
Что изменилось в Vault 1.7
HashiCorp выпустила 1.7 в марте, и среди изменений есть несколько, которые упрощают именно этот сценарий.
Dynamic secrets для AWS - движок генерирует временные IAM-ключи с нужными политиками прямо в момент запроса. Ключ живёт ровно столько, сколько указан lease TTL, после чего Vault сам отзывает его через AWS API. Никаких «ключи ротируем раз в квартал», никаких общих ключей на несколько сервисов.
Аналогично для GCP - Vault умеет выдавать краткоживущие service account tokens или создавать Service Account Keys с ограниченным временем жизни. В 1.7 улучшили надёжность отзыва ключей: раньше при сбоях Vault мог оставлять «висячие» ключи в GCP, которые не отзывались. Теперь это чинится через WAL (write-ahead log) при рестарте.
Автоматическая ротация статических credentials - если всё-таки нужен статический пароль (например, для PostgreSQL), Vault 1.7 умеет его ротировать по расписанию самостоятельно, без ручного вмешательства. Пароль в Vault меняется, старый аннулируется.
Схема с External Secrets Operator
Подключить Vault к Kubernetes можно несколькими способами. Vault Agent Injector - популярный вариант, но он требует sidecar-контейнера в каждом поде и добавляет шум в манифесты. External Secrets Operator (ESO) работает иначе: он живёт как отдельный контроллер в кластере, опрашивает Vault по расписанию и сам создаёт Kubernetes Secrets. Поды работают с обычными Secret-ами и ничего не знают про Vault.
Принцип такой:
Vault (dynamic secrets)
|
| ExternalSecret CR (TTL sync = 55min)
v
External Secrets Operator
|
v
Kubernetes Secret (автообновляется)
|
v
Pod (монтирует как обычно)
ESO сам разбирается с обновлением Secret до истечения TTL. Мы выставили lease TTL у Vault в 1 час, а период синхронизации в ExternalSecret - 55 минут. С небольшим запасом, но без гонки.
Конфигурация Vault
Vault мы деплоим через официальный Helm-чарт, state хранится в Consul. AppRole - метод аутентификации для ESO: получает role_id и secret_id, которые выдаются отдельным bootstrap-процессом через Terraform.
Политика для ESO минимальная - только чтение из нужных path:
path "aws/creds/app-role" {
capabilities = ["read"]
}
path "gcp/token/app-sa" {
capabilities = ["read"]
}
path "database/creds/app-postgres" {
capabilities = ["read"]
}
Движки настраиваем через Terraform. Для AWS - стандартный aws secrets engine, role с нужными IAM-политиками, TTL 1 час. Для PostgreSQL - database secrets engine с динамическими пользователями: Vault создаёт пользователя в базе на время lease, потом удаляет.
ExternalSecret в кластере
apiVersion: external-secrets.io/v1alpha1
kind: ExternalSecret
metadata:
name: app-aws-creds
namespace: production
spec:
refreshInterval: 55m
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: app-aws-creds
creationPolicy: Owner
data:
- secretKey: AWS_ACCESS_KEY_ID
remoteRef:
key: aws/creds/app-role
property: access_key
- secretKey: AWS_SECRET_ACCESS_KEY
remoteRef:
key: aws/creds/app-role
property: secret_key
Под монтирует Secret как обычно - через envFrom или volumeMounts. Ни одна строчка в Deployment не меняется. Когда ESO обновляет Secret, новые значения попадают в следующий pod при рестарте или пересоздании.
Здесь есть нюанс: если pod долго живёт без рестарта, он продолжает работать со старыми переменными окружения, которые уже отозваны. Для большинства сервисов это не проблема - деплои происходят чаще, чем раз в час. Но для long-running process-ов нужно либо явно читать Secret через volume (тогда он обновляется в файловой системе автоматически), либо добавить логику перезагрузки по сигналу. Мы пока решили через volume для критичных компонентов.
Что убрали из кластера
Итог на стороне заказчика: из манифестов и ConfigMap ушли все статические ключи. AWS IAM ключи теперь краткоживущие - даже если утекут, через час бесполезны. Пароль к PostgreSQL ротируется автоматически. GCP service account tokens живут 1 час.
Vault сам по себе требует надёжного хранилища - у нас Consul кластером из трёх нод. Это дополнительная инфраструктура, которую нужно сопровождать. Managed-сопровождение здесь окупается: Consul и Vault мониторятся наравне с остальной инфраструктурой, alert-ы настроены, процедура рестора задокументирована.
Где сейчас
Схема работает несколько недель, острых проблем не было. Периодически видим в логах ESO предупреждения о том, что Vault вернул lease раньше ожидаемого - это поведение при нагрузке на Consul, разбираемся. В остальном - тихо.
Следующий шаг, который пока в планах: audit log Vault. Сейчас мы знаем что ESO ходит в Vault за credentials, но детальной картины кто, когда и за чем запрашивал - нет. Vault умеет писать audit log в файл или syslog, это не сложно, просто руки не дошли.