Секрет в Kubernetes — это Secret, лежащий в etcd рядом с деплойментами. Кто-то должен положить его туда: вручную через kubectl create secret, через kubectl apply из зашифрованного файла в git, через шаблон в Helm-чарте. В любом из этих сценариев есть человек, который однажды скопировал значение из Vault в манифест — и с этого момента у секрета появилась вторая копия, живущая своей жизнью. External Secrets Operator убирает человека из этой цепочки: кластер сам тянет секрет из внешнего менеджера и держит его синхронизированным, без ручных копий и без секретов в git вообще.
Содержание
Открыть содержание
- SOPS решает не ту половину проблемы
- Модель: SecretStore, ClusterSecretStore и ExternalSecret
- Провайдеры: не только Vault
- Ротация: refreshInterval и что происходит при смене секрета
- RBAC: у кого есть доступ к менеджеру
- Сравнение в одной таблице
- Что нужно, чтобы завести ESO с Vault
- Как убедиться, что работает
- Итог
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, и разобраться в них — весь необходимый минимум.
SecretStore— описывает подключение к одному менеджеру секретов в рамках одного namespace: адрес Vault, метод аутентификации, регион для AWS. Namespaced — то есть команда в своём namespace может завести свой стор, не трогая чужие.ClusterSecretStore— то же самое, но на уровне кластера: один стор, доступныйExternalSecret-ам из любого namespace (с ограничениями черезnamespaceSelector, если нужно). Обычно это и есть рабочий вариант — платформенная команда заводит одинClusterSecretStoreна Vault, а дальше все просто на него ссылаются.ExternalSecret— декларация «хочу вот этот путь из стора материализовать как k8sSecretс вот таким именем». Именно этот объект видит разработчик в своём манифесте — он выглядит как обычная зависимость, а не как что-то особенное.
Оператор работает циклом: читает ExternalSecret, идёт в менеджер через SecretStore, забирает значение и создаёт (или обновляет) обычный Secret в том же namespace. Для пода, который монтирует этот Secret как volume или env, разницы с ручным kubectl create secret нет вообще — вся магия происходит на уровне контроллера, а не на уровне API, которое видит workload.
Провайдеры: не только Vault
ESO — не Vault-специфичный инструмент, это общий слой абстракции с десятками провайдеров под одним API. Меняется только SecretStore.spec.provider, форма ExternalSecret остаётся той же:
- HashiCorp Vault — KV v2, аутентификация через Kubernetes ServiceAccount token, AppRole или JWT.
- AWS Secrets Manager / SSM Parameter Store — аутентификация через IRSA (IAM Roles for Service Accounts), без статических ключей в кластере.
- GCP Secret Manager и Azure Key Vault — то же самое через Workload Identity.
- 1Password, Doppler, Bitwarden и ещё около трёх десятков провайдеров — для команд, у которых источник правды не корпоративный Vault, а SaaS-менеджер.
Практическое следствие: если завтра компания мигрирует с самостоятельно захостенного 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 при изменении.
Отдельно стоит ExternalSecret.spec.target.deletionPolicy — что делать с k8s Secret, если сам ExternalSecret удалили. По умолчанию Retain (секрет остаётся), но для honest-cleanup сценариев есть Delete.
RBAC: у кого есть доступ к менеджеру
Оператор — это единственная точка, у которой есть креды к Vault или AWS Secrets Manager, и это одновременно и удобство, и риск, который нужно осознанно ограничивать:
- Аутентификация ESO к Vault обычно идёт через Kubernetes auth method — Vault доверяет ServiceAccount-токену пода ESO, а не статическому токену в Secret. Токен короткоживущий, ничего долгоживущего в кластере не хранится.
- Vault-политика, привязанная к этому ServiceAccount, должна давать доступ только к тем путям, что реально нужны кластеру — не
secret/*, а конкретные префиксы по окружениям и командам. - Если несколько команд шарят один
ClusterSecretStore,ExternalSecretв чужом namespace физически не сможет прочитать чужой путь — не потому что Kubernetes это запрещает, а потому что Vault-политика для этого ServiceAccount такой путь не разрешает. Least privilege держится на стороне менеджера секретов, а не на стороне Kubernetes RBAC.
Сравнение в одной таблице
| Ручные Secret / Helm-values | SOPS + git | External 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, кластером и менеджером, потому что копия в кластере всегда производная, а не независимая.