Зачем отдельная инфраструктура для секретов
«Секрет» = значение, которое нельзя видеть никому, кроме тех, кто должен. Пароли БД, API-keys, TLS-private-keys, OAuth-client-secret.
Способы как НЕ надо:
- В коде (
API_KEY = "sk-..."), git история, code review, утечки навсегда (даже после revert виден в history) - В .env коммичен в git, то же самое, плюс случайно попадает в Docker image
- В Dockerfile через ENV, виден в
docker history, попадает в image - В CI как plain переменная, лог CI, артефакты build'а
- В k8s ConfigMap, base64 не шифрование, kubectl get cm покажет
Что действительно нужно:
- Секрет хранится зашифрованным
- Доступ аутентифицирован (известно кто запросил)
- Доступ авторизован (этому identity положено)
- Каждый запрос залогирован (audit-trail)
- Возможна ротация без перезапуска приложения
- Секреты не лежат в git (даже зашифрованные если возможно)
Уровень 0: env-vars из orchestrator'а
Простейший подход: задать env-var через docker-compose / systemd / k8s, без хранилища.
# docker-compose.yml
services:
app:
image: myapp
environment:
DB_PASSWORD: ${DB_PASSWORD} # из .env (НЕ в git!)Минусы: ротация = restart, no audit, secret виден в docker inspect.
Подход для local-dev и небольших pet-проектов. Для прода, мало.
HashiCorp Vault, универсал
Самый зрелый менеджер секретов. Работает как REST-сервис + encrypted backend (etcd, raft, S3).
Ключевые концепции:
- Secrets engine, что хранить (KV, database, AWS, PKI, transit)
- Auth method, как клиент аутентифицируется (token, AppRole, kerberos, k8s SA, AWS IAM, OIDC)
- Policy, кому что разрешено (HCL-policy на пути)
- Audit device, куда писать audit-log
# Запись секрета
vault kv put secret/app/db password="s3cret" username="myapp"
# Чтение
vault kv get -field=password secret/app/db
Dynamic secrets, киллер-фича
Не хранить пароль БД, а выпускать на лету короткоживущий:
vault write database/config/postgres-prod \
plugin_name=postgresql-database-plugin \
connection_url="postgresql://{{username}}:{{password}}@db:5432/" \allowed_roles=readonly \
username=vault-admin password=...
vault write database/roles/readonly \
db_name=postgres-prod \
creation_statements="CREATE USER \"{{name}}\" WITH PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \ GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \default_ttl=1h max_ttl=24h
# клиент:
vault read database/creds/readonly
▸username=v-app-..., password=..., lease 1h
Нет долгоживущих credentials → нечего ротировать. У каждого pod'а, свои уникальные. При утечке, ttl истекает за час.
Transit engine, encryption as a service
Vault шифрует/дешифрует, ключ никогда не покидает Vault:
vault write transit/encrypt/app plaintext=$(echo "secret" | base64)
▸ciphertext: vault:v1:...
vault write transit/decrypt/app ciphertext="vault:v1:..."
▸plaintext: c2VjcmV0
Полезно для encryption-at-rest без управления KMS-ключами в коде.
k8s Secrets, нативно, но осторожно
apiVersion: v1
kind: Secret
metadata: { name: app-db }type: Opaque
stringData:
password: s3cret # k8s сам base64 закодирует
Pod использует:
spec:
containers:
- name: app
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef: { name: app-db, key: password }Подводные:
- base64 ≠ encryption,
kubectl get secret app-db -o yaml | base64 -dвидно всем сget secretspermission - По умолчанию в etcd plain! Включить
encryption-at-restчерезEncryptionConfiguration(KMS-ключ или в-файле AES) - RBAC на secrets, отдельно. Не давать
cluster-readerавтоматически (часто включает secrets) - Mount как файл vs env-var, файл предпочтительнее (меньше
leak через
/proc/<pid>/environ)
sealed-secrets, секреты в git
Если у вас GitOps (ArgoCD/Flux), хочется хранить всё в git, включая Secrets. Но Secret base64-наружу, лажа.
Решение sealed-secrets (Bitnami):
- В кластере controller с парой ключей
- CLI
kubesealшифрует Secret публичным ключом → SealedSecret CRD - SealedSecret коммитится в git
- Controller дешифрует и создаёт реальный Secret в кластере
kubectl create secret generic db -n prod --from-literal=pwd=s3cret \
--dry-run=client -o yaml | \
kubeseal -o yaml > db-sealedsecret.yaml
git add db-sealedsecret.yaml && git commit
Только этот конкретный кластер может расшифровать (private key только там). Утечка git → secret защищён.
Минус: при потере private key cluster'а, все sealed-secrets невосстановимы. Бэкап ключа критичен.
external-secrets, мост к cloud-vault'у
Если используете AWS Secrets Manager / GCP Secret Manager / Azure Key Vault / Vault, стандартный CRD-операторы:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata: { name: app-db }spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets
kind: ClusterSecretStore
target: { name: app-db }data:
- secretKey: password
remoteRef:
key: prod/app/db
property: password
Оператор каждый час идёт в AWS Secrets Manager, тянет значение, обновляет k8s Secret. Источник истины, облачный vault, k8s Secret, кэш.
Бонус: ротация секрета в AWS → автоматически обновляется в k8s.
CSI driver для секретов (Secrets Store CSI Driver)
Альтернатива external-secrets: монтировать секрет прямо в pod как файл, без k8s Secret промежуточно:
volumes:
- name: secrets
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: app-secrets
Плюсы: никогда не лежит в etcd, ротация без restart
(с enableSecretRotation).
Ротация, главная боль
Хранение секрета, простая часть. Сложная, обновить везде, где используется, без даунтайма.
Подходы:
- Restart pod при изменении Secret, крайне нежелательно для stateful
- App watch'ит файл секрета, паттерн с CSI driver
- Sidecar перезагружает (Vault Agent injector + sigtemplate)
- Dynamic secrets, обновление автоматическое через TTL
Никогда не предполагайте, что секрет вечен. Минимум раз в год ротация (compliance: SOC 2, PCI требуют).
Audit и compliance
Любой production-vault должен логировать каждый read/write с:
- identity клиента
- какой secret path
- timestamp
- source IP
Vault: vault audit enable file file_path=/var/log/vault/audit.log.
AWS Secrets Manager: CloudTrail. k8s Secrets: [[auditd|audit-policy]] на
apiserver.
При инциденте audit-log = единственное что покажет, кто и когда читал секрет.
Что НЕ хранить в secrets
- Hash паролей пользователей, это БД, не secret
- Сессии / JWT-токены, отдельный store (Redis)
- Public-данные (URLs, env-flags), ConfigMap, не Secret
Слишком всё в Secret = больше attack surface, тяжелее audit.
Когда что-то пошло не так
Error: secret not foundв pod'е, Secret в другом namespace, или ServiceAccount без RBAC на чтение этого Secret.- Восстановление из бэкапа etcd показывает старый secret encryption-at-rest с KMS-ключом, но KMS ключ потерян. Бэкап бесполезен. Бэкапить и KMS-ключ.
pod cannot read secret fileпосле rotation, приложение запомнило старый файл-handle (cached open). Решение: SIGHUP-handler или sidecar-watch.- sealed-secret invalid, controller перерегенерировал ключ (новый кластер). Расшифровать старый невозможно, regenerate с нуля.
- Vault sealed после рестарта, Vault стартует sealed (HA
feature). Нужен
vault operator unsealс N из M shamir-keys (или auto-unseal через cloud KMS). - Secret видится в логах приложения, приложение печатает env
при старте (debug). Никогда не делать. Линтить через CI:
grep -i "password\|secret" app-logs. .envзакоммичен в git,git filter-repo --invert-paths --path .env, потом ROTATE all secrets, git history помнит навсегда.