Перейти к содержимому
Hogin Hogin
Назад

External Secrets Operator: секреты из Vault в Kubernetes без копипасты

8 мин чтения

Секрет в Kubernetes — это Secret, лежащий в etcd рядом с деплойментами. Кто-то должен положить его туда: вручную через kubectl create secret, через kubectl apply из зашифрованного файла в git, через шаблон в Helm-чарте. В любом из этих сценариев есть человек, который однажды скопировал значение из Vault в манифест — и с этого момента у секрета появилась вторая копия, живущая своей жизнью. External Secrets Operator убирает человека из этой цепочки: кластер сам тянет секрет из внешнего менеджера и держит его синхронизированным, без ручных копий и без секретов в git вообще.

Содержание

Открыть содержание

SOPS решает не ту половину проблемы

Самый популярный ответ на «секреты в Kubernetes» — SOPS: файл с секретом шифруется перед коммитом, GitOps-контроллер расшифровывает его при apply. Это рабочая схема, и она честно закрывает один риск — секрет в открытом виде в истории git.

Но она ничего не говорит про источник истины. Секрет всё ещё живёт в репозитории, просто в зашифрованном виде. Если у вас уже есть Vault или облачный Secret Manager — а у большинства команд с более чем одним кластером он есть, — вы получаете два независимых места, где нужно менять значение при ротации: сначала в менеджере, потом руками пересобрать и закоммитить зашифрованный файл. Рано или поздно они разъезжаются: кто-то обновил пароль в Vault и забыл про SOPS-файл, или наоборот.

External Secrets Operator (ESO) заходит с другой стороны. Вместо «зашифровать секрет и положить в git» — «секрет живёт только в менеджере, а кластер сам его оттуда забирает». Git при этом хранит не секрет, а ссылку на него: путь в Vault, имя параметра в AWS Secrets Manager. Ссылка не секретна — её утечка ничего не даёт без доступа к самому менеджеру.

Модель: SecretStore, ClusterSecretStore и ExternalSecret

ESO построен вокруг трёх CRD, и разобраться в них — весь необходимый минимум.

Оператор работает циклом: читает ExternalSecret, идёт в менеджер через SecretStore, забирает значение и создаёт (или обновляет) обычный Secret в том же namespace. Для пода, который монтирует этот Secret как volume или env, разницы с ручным kubectl create secret нет вообще — вся магия происходит на уровне контроллера, а не на уровне API, которое видит workload.

Vault остаётся источником истины, ESO синхронизирует значение в кластер по ссылке из ExternalSecret

Провайдеры: не только Vault

ESO — не Vault-специфичный инструмент, это общий слой абстракции с десятками провайдеров под одним API. Меняется только SecretStore.spec.provider, форма ExternalSecret остаётся той же:

Практическое следствие: если завтра компания мигрирует с самостоятельно захостенного Vault на облачный Secret Manager, меняется один ClusterSecretStore, а не сотни манифестов ExternalSecret по всем сервисам — они ссылаются на стор по имени, а не на конкретного провайдера напрямую.

Ещё одна деталь, которая экономит время на большом количестве ключей: не обязательно перечислять каждый ключ в data вручную. dataFrom.extract забирает все пары ключ-значение по указанному пути одним блоком — удобно, когда в Vault под одним путём лежит сразу пачка связанных значений (например, весь набор credentials для одной базы) и вы не хотите держать список полей синхронизированным в двух местах.

Ротация: refreshInterval и что происходит при смене секрета

Ключевое отличие от «скопировали один раз» — ExternalSecret.spec.refreshInterval. Оператор не тянет секрет один раз при создании объекта, а опрашивает менеджер с заданным интервалом (по умолчанию час, но обычно ставят от одной до пятнадцати минут в зависимости от чувствительности) и обновляет k8s Secret, если значение в источнике изменилось.

Важный нюанс, о который спотыкаются в первый раз: обновление Secret не перезапускает под автоматически. Kubernetes сам по себе не следит за изменением Secret и не рестартит потребителей — это касается и ESO, и любого другого способа обновить Secret. Если приложение читает переменные окружения только при старте процесса, новое значение приедет в под как файл (при монтировании как volume) почти сразу, но в env — только после следующего рестарта. Практика — либо монтировать секрет как volume и явно перечитывать файл в приложении, либо ставить Reloader или аналог, который следит за хэшем Secret и триггерит rollout при изменении.

refreshInterval опрашивает Vault по расписанию; Secret обновляется, но под подхватывает новое значение только через volume-перечитывание или явный rollout

Отдельно стоит ExternalSecret.spec.target.deletionPolicy — что делать с k8s Secret, если сам ExternalSecret удалили. По умолчанию Retain (секрет остаётся), но для honest-cleanup сценариев есть Delete.

RBAC: у кого есть доступ к менеджеру

Оператор — это единственная точка, у которой есть креды к Vault или AWS Secrets Manager, и это одновременно и удобство, и риск, который нужно осознанно ограничивать:

Сравнение в одной таблице

Ручные Secret / Helm-valuesSOPS + gitExternal Secrets Operator
Источник истиныкопия в кластерезашифрованная копия в gitвнешний менеджер (Vault и т.п.)
Ротацияруками пересоздать Secretруками пересобрать и закоммититьпо refreshInterval, автоматически
Что утечёт при компрометации gitсекрет в открытом видезашифрованный блоб (нужен ключ)только ссылка на путь, не значение
Доступ к секретукто угодно с kubectl get secretкто угодно с ключом дешифровкиограничен политикой менеджера + K8s RBAC
Смена провайдерапереписать все манифестыпереписать пайплайн шифрованияпоменять один SecretStore

Что нужно, чтобы завести ESO с Vault

Предполагается уже работающий Vault с включённым KV v2 и Kubernetes auth method, настроенным на кластер. Дальше — три шага: поставить оператор, завести ClusterSecretStore, объявить ExternalSecret.

helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets \
  -n external-secrets --create-namespace

ClusterSecretStore на Vault через Kubernetes auth:

apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
  name: vault-backend
spec:
  provider:
    vault:
      server: "https://vault.internal:8200"
      path: "secret"
      version: "v2"
      auth:
        kubernetes:
          mountPath: "kubernetes"
          role: "eso-reader"
          serviceAccountRef:
            name: "external-secrets"
            namespace: "external-secrets"

ExternalSecret, который материализует значение из secret/data/prod/api в нативный Secret api-credentials:

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: api-credentials
  namespace: prod
spec:
  refreshInterval: 5m
  secretStoreRef:
    name: vault-backend
    kind: ClusterSecretStore
  target:
    name: api-credentials
    creationPolicy: Owner
  data:
    - secretKey: DB_PASSWORD
      remoteRef:
        key: prod/api
        property: db_password
    - secretKey: API_TOKEN
      remoteRef:
        key: prod/api
        property: api_token

Под подключает результат как обычный Secret — никакого специального API для потребителя:

envFrom:
  - secretRef:
      name: api-credentials

Как убедиться, что работает

Проверьте, что объект синхронизировался и что значение в кластере действительно совпадает с Vault:

kubectl get externalsecret api-credentials -n prod
# READY   True   — синхронизация прошла успешно

kubectl get secret api-credentials -n prod -o jsonpath='{.data.DB_PASSWORD}' | base64 -d

Проверьте ротацию: измените значение в Vault, подождите refreshInterval, убедитесь, что Secret обновился (по resourceVersion или напрямую по значению), и что настроен механизм рестарта пода — иначе новое значение приедет в кластер, но не дойдёт до процесса, который его использует.

Если ExternalSecret завис в состоянии, отличном от READY True, kubectl describe externalsecret покажет причину в status.conditions — чаще всего это протухший токен ServiceAccount, неверный путь в Vault или política, не дающая доступ к нему. Для мониторинга на масштабе оператор отдаёт метрики Prometheus (externalsecret_sync_calls_total, externalsecret_sync_calls_error) — на них имеет смысл повесить алерт: рост ошибок синхронизации значит, что часть подов в кластере скоро увидит устаревший секрет, даже если сейчас всё ещё выглядит зелёным.

Итог

External Secrets Operator не добавляет ещё одно место, где хранится секрет, — он убирает лишние копии. Единственный источник истины остаётся в Vault или облачном Secret Manager, а Kubernetes получает актуальное значение по подписке через SecretStore/ExternalSecret, с ротацией по расписанию и RBAC, который живёт на стороне самого менеджера. Цена — установить оператор и один раз завести стор; выигрыш — секрет физически не может «разъехаться» между git, кластером и менеджером, потому что копия в кластере всегда производная, а не независимая.


Поделиться:

Следующая статья
Trivy в CI: ловим уязвимости и собираем SBOM до прода