# Управление секретами - Vault, k8s Secrets, sealed-secrets _Безопасность · LinuxLab Knowledge Base_ **TL;DR:** Секреты не в git, не в env-vars в коде. Опции: HashiCorp Vault (универсал, dynamic creds), k8s Secrets (base64, нужен encryption- at-rest), sealed-secrets (commit-friendly), external-secrets (sync из cloud-vault). ## Зачем отдельная инфраструктура для секретов «Секрет» = значение, которое нельзя видеть никому, кроме тех, кто должен. Пароли БД, 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 покажет Что **действительно** нужно: 1. Секрет хранится **зашифрованным** 2. Доступ **аутентифицирован** (известно кто запросил) 3. Доступ **авторизован** (этому identity положено) 4. Каждый запрос **залогирован** (audit-trail) 5. Возможна **ротация** без перезапуска приложения 6. Секреты **не лежат в git** (даже зашифрованные если возможно) ## Уровень 0: env-vars из orchestrator'а Простейший подход: задать env-var через docker-compose / systemd / k8s, без хранилища. ```yaml # 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](/kb/kerberos.md), k8s SA, AWS IAM, OIDC) - **Policy**, кому что разрешено (HCL-policy на пути) - **Audit device**, куда писать audit-log ```bash # Запись секрета vault kv put secret/app/db password="s3cret" username="myapp" # Чтение vault kv get -field=password secret/app/db ``` ### Dynamic secrets, киллер-фича Не хранить пароль БД, а **выпускать на лету** короткоживущий: ```bash 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: ```bash 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, нативно, но осторожно ```yaml apiVersion: v1 kind: Secret metadata: { name: app-db } type: Opaque stringData: password: s3cret # k8s сам base64 закодирует ``` Pod использует: ```yaml 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 secrets` permission - По умолчанию **в etcd plain**! Включить [`encryption-at-rest`](https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/) через `EncryptionConfiguration` (KMS-ключ или в-файле AES) - RBAC на secrets, отдельно. **Не давать `cluster-reader`** автоматически (часто включает secrets) - Mount как файл vs env-var, **файл предпочтительнее** (меньше leak через `/proc//environ`) ## sealed-secrets, секреты в git Если у вас GitOps (ArgoCD/Flux), хочется хранить всё в git, включая Secrets. Но Secret base64-наружу, лажа. Решение **sealed-secrets** (Bitnami): - В кластере controller с парой ключей - CLI `kubeseal` шифрует Secret публичным ключом → SealedSecret CRD - SealedSecret коммитится в git - Controller дешифрует и создаёт реальный Secret в кластере ```bash 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-операторы: ```yaml 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 промежуточно: ```yaml 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 помнит навсегда. ## Команды ```bash vault kv put secret/app/db password=s3cret ``` Положить секрет в KV-engine - простейший use-case Vault ```bash vault read database/creds/readonly ``` Получить dynamic-credentials с TTL - короткоживущий пароль БД ```bash kubectl create secret generic app-db --from-literal=pwd=s3cret -n prod ``` Создать k8s Secret из CLI - быстро, но без encryption если etcd-at-rest off ```bash kubeseal -o yaml < secret.yaml > sealed.yaml ``` Зашифровать Secret в SealedSecret для commit в git ```bash kubectl get secrets -A -o json | jq '.items[] | select(.type=="Opaque") | .metadata.name' ``` Аудит - все Opaque-секреты в кластере, для review кто что хранит ```bash git secrets --scan ``` Сканер git-репо на случайно закоммиченные API-keys, AWS credentials ```bash vault audit enable file file_path=/var/log/vault/audit.log ``` Включить audit-log Vault - обязательно для прода и compliance ## См. также - [PAM - Pluggable Authentication Modules](/kb/pam.md) - [SSH hardening - закрытие сервера](/kb/ssh-hardening.md) - [Kubernetes pod lifecycle - от Pending до Terminated](/kb/kubernetes-pod-lifecycle.md) - [auditd - syscall и file audit](/kb/auditd.md) - [bash-скрипты - основы и идиомы](/kb/bash-scripting.md) - [GPG/PGP - подпись, шифрование, web of trust](/kb/gpg-pgp.md)