Secrets management: Vault, KMS, External Secrets Operator, Sealed Secrets, SOPS. Five scenarios: app fetches static secret via Vault (k8s SA JWT auth → token → KV read with KMS-backed envelope encryption + audit log), Vault dynamic DB credentials (auto-generated postgres user with TTL=1h, auto-revoke makes leaked password useless), External Secrets Operator (GitOps sync from Vault into K8s Secret, plus Sealed Secrets alternative), SOPS encrypted YAML in git (Mozilla SOPS with KMS/age, no runtime Vault dependency), and ADR comparing Vault vs cloud-native Secrets Manager vs ESO+Sealed Secrets with hybrid recommendation.
Секрет в .env под git — это пароль на стикере, приклеенный к монитору. Он
живёт вечно (git log -p найдёт его через 5 лет), копируется во все forks и
CI-логи, и при увольнении инженера остаётся валидным. Uber 2016 (57M users,
$148M штраф), Toyota 2022 (296K записей, 5 лет в публичном репо), Twitch 2021
(125GB кода + AWS keys) — все начинались с «временно положу в config».
Secrets management даёт три вещи, которых нет у статических секретов: ротацию (короткий TTL → leaked password бесполезен через час), аудит (кто читал какой секрет когда), и least privilege (каждый сервис видит только свои). Это не «ещё одна тулза» — это инфраструктурная контрольная точка, без которой compliance (SOC2 CC6.1, PCI DSS 3.5/3.6, HIPAA 164.312) проваливается на первом аудите.
Думайте о секрете не как о строке, а как о lease — аренде с истечением.
v-token-api-AbC123 с TTL 1h, через час
DROP USER автоматически. Blast radius = TTL.Второй мысленный приём — envelope encryption. Не шифруем данные мастер- ключом (он бы тогда читал всё). Генерируем уникальный DEK на каждый кусок данных, шифруем им данные, а сам DEK шифруем KEK из HSM. Утечка одного DEK компрометирует один объект, не всю базу. KMS-сервисы (AWS KMS, GCP KMS) — это именно про управление KEK; они никогда не отдают мастер-ключ наружу, только расшифровывают DEK по запросу с audit log.
Четыре зоны, каждая со своей ролью:
Связи между зонами — это plane separation: identity flow (sa → vault-auth), secret read flow (app → vault-api → kv/db), и declarative GitOps flow (developer → git → eso/sealed-ctl). Никаких прямых линий «app → postgres с жёстко прописанным паролем» — все credentials проходят через Vault.
Базовый поток. Pod стартует, kubelet монтирует projected ServiceAccount JWT,
sidecar (vault-agent) меняет его на Vault token через k8s auth method, читает
Stripe API key из KV. Ключевое: token живёт 1h, secret держится в памяти (не
пишем в файл), каждый шаг попадает в audit log с полем {who, what, when, ip}.
Самый сильный аргумент за Vault. App не имеет статического Postgres-пароля
вообще. По запросу Vault выполняет CREATE USER v-token-api-AbC123 WITH PASSWORD ...,
выдаёт credentials с lease, через 1h автоматически DROP USER. Если heap dump
утечёт через debug endpoint и атакующий попробует зайти через 65 минут — user
уже не существует. Blast radius схлопывается с «до ручной ротации (часто
никогда)» до TTL.
GitOps-альтернатива sidecar-подходу. ExternalSecret CRD в git содержит ссылку на секрет в Vault, не сам секрет. ESO controller непрерывно ресинкает Vault → обычный K8s Secret, pod монтирует env как раньше и ничего не знает о Vault. Это decoupling: при миграции backend (Vault → AWS Secrets Manager) меняется только CRD, не код приложения. Sealed Secrets — родственник: зашифрованный манифест в git, controller расшифровывает приватным ключом кластера.
Mozilla SOPS — для случаев, когда runtime-зависимость от Vault не оправдана
(bootstrap, конфиги, миграции). Шифруем YAML/JSON прямо в git: keys plain
(api_key:), values как ENC[AES256_GCM,data:abc...]. PR diff читается («что
изменилось» видно, «на что» — нет). DEK уникален на файл, зашифрован KMS-
ключом (или age pubkey каждого инженера). Offboarding: sops updatekeys без
получателя — старые commits он расшифровать не сможет.
Архитектурное решение: Vault vs cloud-native vs ESO+SealedSecrets. Решение обычно гибрид: Vault как backend (dynamic creds, audit, multi-cloud- ready) + ESO как K8s-интеграция (поды декаплены от Vault) + SOPS для bootstrap (решает chicken-and-egg «как ESO получит свой Vault token»).
| Опция | Что хорошо | Что больно |
|---|---|---|
| HashiCorp Vault (self-hosted) | Dynamic secrets (DB/AWS/SSH), богатые auth methods, multi-cloud | HA-кластер 3-5 нод Raft, unsealing, upgrades; Enterprise-лицензия для namespaces ($); outage = новые pods без secrets |
| AWS/GCP Secrets Manager | Zero ops, multi-AZ, native rotation для RDS, CloudTrail из коробки | Vendor lock-in (миграция в multi-cloud дороже), $0.40/secret/mo × 1000 = $400+/mo, dynamic creds только через свою lambda |
| ESO + Sealed Secrets | GitOps-friendly, декларативно, дёшево (только controller-поды) | Sealed rotation = re-encrypt + PR на каждый пароль, нет dynamic creds, ESO всё равно нужен backend |
| SOPS | Нет runtime-зависимости, PR diff читается, multi-recipient (KMS + age + PGP) | Git history содержит старые версии (утечка KMS = расшифровка истории), нет audit «кто читал в CI» |
Decision: Vault (backend) + ESO (k8s sync) + SOPS (bootstrap config). Consequences: +1 компонент сложности, но developer experience и audit выигрывают. Re-evaluate при переходе на managed K8s + single-cloud — тогда managed Secrets Manager + ESO будет дешевле и проще.
Pitfall to watch: secret zero — как app получает первый Vault token? Ответ: projected ServiceAccount JWT, не статический seed. Если в ответе проскакивает «положим Vault token в env» — решение неверное.
git log -p навсегда; GitHub
secret scanning ловит, но утечка уже произошла..env без encryption — рано или поздно случайно закоммитят
(git add . всё включит).strings найдёт.console.log(req.body) где body содержит
password или token; попадает в Datadog, Loki, Sentry, S3 backup.echo $SECRET в workflow попадает в публичный
build log (для public repos — катастрофа).EncryptionConfiguration с KMS provider etcd хранит секреты в plain.Secrets manager — не молоток для всех гвоздей.
direnv + локальный .env.local в .gitignore + pre-commit
hook (gitleaks) покрывает 95% риска.