Секреты в Helm: helm-secrets, sops, внешние
Самая частая дыра в безопасности Helm — пароли в values под git. Разбираем, как делать правильно.
Стандартный Helm-Secret в кластере хранится в base64, а не зашифрованным. А пароли в
values.yamlпод git — это утечка. Решения: шифровать values (sops/helm-secrets) или вообще не хранить секреты в Helm (внешние сторы).
В чём проблема
Соблазн прост: положить пароль в values/prod.yaml и закоммитить. Последствия: (1) пароль виден всем с доступом к репозиторию и в истории git навсегда; (2) даже Kubernetes-объект Secret кодируется base64, а не шифруется — kubectl get secret -o yaml | base64 -d вернёт пароль. Helm сам по себе шифрования секретов не предоставляет.
Особенно коварен пункт про историю git. Многие думают, что достаточно удалить пароль из файла следующим коммитом — но git хранит каждую ревизию, и старое значение остаётся доступным через git log -p навсегда, пока историю не перепишут force-push'ем (что в общем репозитории почти нереально). Поэтому единственный по-настоящему правильный ответ на «закоммиченный пароль» — считать его скомпрометированным и ротировать, а не «убрать из файла». Это и объясняет, почему вопрос секретов решают до первого коммита, а не после инцидента.
Стоит уточнить и природу base64 в Kubernetes Secret. Это не «слабое шифрование», а вообще не шифрование — лишь кодировка для безопасной передачи бинарных данных в YAML. Любой, у кого есть права get secret в namespace, или доступ к снапшоту etcd, читает значение открыто. Защита здесь — это RBAC (кому вообще можно читать Secret'ы), encryption-at-rest для etcd (чтобы дамп диска не выдал секреты) и подходы из этого урока, которые не дают паролю попасть в git в принципе.
Подход 1: шифровать values через sops + helm-secrets
sops (Secrets OPerationS) шифрует значения в YAML/JSON, оставляя ключи читаемыми, ключами из KMS/age/PGP. Плагин helm-secrets интегрирует sops в Helm: зашифрованный файл лежит в git, а Helm расшифровывает его на лету при деплое.
helm plugin install https://github.com/jkroepke/helm-secrets
# зашифровать файл с секретами (значения станут шифротекстом)
sops --encrypt secrets.prod.yaml > secrets.prod.enc.yaml
# деплой: helm-secrets расшифрует на лету
helm secrets upgrade --install web ./webapp -n prod -f values/prod.yaml -f secrets.prod.enc.yaml
В git попадает только secrets.prod.enc.yaml с зашифрованными значениями — ключи видны, пароли нет. Расшифровать может лишь тот, у кого есть доступ к KMS-ключу. Это «GitOps-дружественный» способ: секреты версионируются, но безопасно.
Почему именно «значения, а не файл целиком»
Ключевое инженерное решение sops — шифровать каждое значение по отдельности, оставляя структуру и ключи карты открытыми. Это даёт два важных свойства. Во-первых, осмысленный git diff: вы видите, что менялся именно db.password, а не «весь файл стал другим бинарным блоком», что бесценно при ревью pull request'ов. Во-вторых, sops дописывает в конец файла служебный блок sops: с метаданными — каким ключом (age, PGP, AWS/GCP KMS) зашифровано и контрольными суммами. Благодаря MAC-сумме sops замечает, если кто-то вручную подправил зашифрованный файл, и отказывается его расшифровывать — это защита от тихой порчи секретов.
Управление доступом здесь сводится к управлению ключами расшифровки, а не к правам на файл. В .sops.yaml описывают правила: какие файлы каким получателям (recipients) шифровать. Добавить нового инженера в команду — значит перешифровать файлы на его публичный ключ (одной командой sops updatekeys); отозвать доступ уволенному — убрать его ключ и перешифровать. Сам пароль при этом менять не обязательно, потому что в git он всегда лежал зашифрованным.
Подход 2: внешние секрет-сторы
Более зрелый путь — не держать секреты в Helm вообще. Секреты живут в специализированном хранилище (HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager), а в кластер их доставляет оператор. Самый популярный — External Secrets Operator: вы описываете ExternalSecret, который ссылается на ключ во внешнем сторе, а оператор создаёт из него обычный Kubernetes Secret.
# ExternalSecret в чарте: значение тянется из Vault, не из values
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
spec:
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: db-credentials
data:
- secretKey: password
remoteRef:
key: prod/db
property: password
Чарт не содержит пароля — только ссылку. Это убирает секрет из git и из values полностью.
У этого подхода есть свойства, которых нет у sops. Главное — ротация без передеплоя: оператор периодически синхронизирует значение из стора, поэтому при смене пароля в Vault обновится и Kubernetes Secret, а вам не нужно перешифровывать файл и катить новый релиз. Это особенно ценно для динамических секретов (например, временные креды БД, которые Vault выдаёт на час). Минус — цена входа: нужно поднять и сопровождать сам стор (Vault — нетривиальная инфраструктура с собственными unseal-ключами и HA), настроить аутентификацию кластера в нём (обычно через ServiceAccount-токены) и развернуть оператор. Поэтому маленьким проектам sops чаще выгоднее, а External Secrets раскрывается на зрелой инфраструктуре с десятками сервисов и требованием аудита доступа к секретам.
Важно не путать слои: ExternalSecret в итоге всё равно создаёт обычный Kubernetes Secret в кластере. То есть от base64 в etcd этот подход сам по себе не спасает — encryption-at-rest и RBAC по-прежнему нужны. Его ценность в другом: секрет никогда не лежит в git и в values, источником истины служит защищённый стор, а в кластер значение попадает контролируемо и обновляемо.
Сравнение подходов
| Подход | Где живёт секрет | Когда |
| sops/helm-secrets | зашифрован в git | GitOps, нет внешнего стора |
| External Secrets / Vault | во внешнем хранилище | зрелая инфраструктура, много секретов |
Как helm-secrets работает под капотом
Плагин оборачивает обычные команды Helm. Перед рендером он находит файлы значений, помеченные как зашифрованные, вызывает sops для их расшифровки во временный файл в памяти/на диске, подставляет расшифрованные значения в обычный -f, запускает Helm, а затем удаляет временные данные. Ключевой момент: расшифрованные секреты существуют лишь на время команды и в git не попадают. sops, в свою очередь, шифрует только значения, а ключи карты оставляет открытыми — поэтому diff зашифрованного файла читаем (видно, какие ключи менялись), а сами секреты защищены KMS-ключом, к которому привязан файл.
Частые ошибки
- Пароль в открытом values под git. Главная ошибка; даже удалив его потом, он остаётся в истории git.
- Считать Kubernetes Secret шифрованием. Это base64; включайте encryption-at-rest в etcd и/или внешние сторы.
- Коммитить расшифрованный временный файл. Держите
*.dec.yamlв.gitignore.
Итог
- Helm не шифрует секреты; values под git и base64-Secret — не защита.
- sops + helm-secrets шифруют значения в git (ключи открыты, пароли — нет) — GitOps-дружественно.
- External Secrets/Vault убирают секрет из Helm вовсе, оставляя в чарте лишь ссылку.