ADG Оставить заявку
Блог DevOps 5 мин чтения

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, это не сложно, просто руки не дошли.

Контакт

Нужна такая же инженерная работа?

Опишите задачу и контекст. Ответим в течение рабочего дня, при необходимости подпишем NDA.