Secret rotation и external secret managers
Базовые K8s Secrets — base64-кодированные данные в etcd с RBAC-доступом. Для development этого достаточно. Для production — нет. Production-grade secret management требует encryption at rest, audit log, automated rotation, short-lived credentials. Эти свойства K8s native Secrets не обеспечивает out-of-the-box.
Поэтому в production K8s + secrets — это всегда integration с external secret manager: HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault. И bridge между ними и K8s — это External Secrets Operator (ESO), CSI Secret Store Driver или Vault Agent Injector. CKAD-scope — знать, что эти инструменты существуют и зачем; глубокая настройка — CKS-scope. Но без понимания паттернов rotation production-приложение получит ту же боль, что и без graceful shutdown.
Секреты и образы: как не слить пароль в Docker Hub
Проблемы native K8s Secrets
Secret объект в K8s — это data: { key: base64(value) }, хранится в etcd, доступ через RBAC. Что не даёт:
1. base64 ≠ encryption
apiVersion: v1
kind: Secret
type: Opaque
data:
password: cGFzc3dvcmQxMjM= # это просто base64(password123)
echo cGFzc3dvcmQxMjM= | base64 -d → password123. Никакой защиты — обфускация только.
Encryption at rest в etcd — это отдельная настройка кластера (--encryption-provider-config на kube-apiserver). По умолчанию выключена. Даже если включена — encryption keys управляются вне K8s (KMS, key file), и часто настройка делается некорректно.
2. RBAC часто misconfigured
# Слишком много разрешений
kind: Role
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["*"] # включает get, list — можно прочитать все секреты в ns
Многие developers ставят verbs: ["*"] “на всякий случай” — это даёт read access ко всем secrets в namespace. ServiceAccount, у которого get secrets — потенциальная утечка.
3. Rotation — manual
# Native подход: вручную
kubectl create secret generic db --from-literal=password=newpass --dry-run=client -o yaml | kubectl apply -f -
kubectl rollout restart deploy/web # чтобы Pod подхватил
Никакой автоматизации. Если 50 микросервисов используют один Secret — все 50 нужно вручную restart. Если pod hot-reload secret из mounted volume — это другой механизм, который app должна явно реализовать (см. ниже).
4. Audit — не auto
Кто прочитал Secret? Когда? Из какого Pod? — K8s audit log это записывает, но только если включён --audit-policy-file с правильной policy. По умолчанию — ничего.
External Secrets ecosystem
Три основных подхода интеграции с external secret manager:
1. External Secrets Operator (ESO)
ESO — community operator, который добавляет CRDs SecretStore / ExternalSecret. Pattern:
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-backend
spec:
provider:
vault:
server: "https://vault.example.com"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "web-role"
serviceAccountRef:
name: web-sa
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: web-db-credentials
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: web-db # имя K8s Secret, который будет создан/обновлён
data:
- secretKey: password
remoteRef:
key: secret/data/web/db
property: password
Как работает:
- ESO controller watch ExternalSecret CRs.
- Каждые
refreshInterval(1h) — fetch значение из Vault (или AWS SM, GCP SM, Azure KV — настраивается в SecretStore). - Создаёт / обновляет K8s Secret object
web-dbс актуальными данными. - Pod монтирует обычный K8s Secret.
ESO выступает как cache layer между внешним storage и K8s. Приложение использует обычный K8s Secret API — никаких изменений.
2. CSI Secret Store Driver
Альтернатива — не создавать K8s Secret вообще. Mount секреты как volume напрямую из external store через CSI driver.
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: web-vault-secrets
spec:
provider: vault
parameters:
vaultAddress: "https://vault.example.com"
roleName: "web-role"
objects: |
- objectName: "db-password"
secretPath: "secret/data/web/db"
secretKey: "password"
---
apiVersion: v1
kind: Pod
spec:
containers:
- name: web
volumeMounts:
- name: secrets
mountPath: "/mnt/secrets"
readOnly: true
volumes:
- name: secrets
csi:
driver: secrets-store.csi.x-k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: web-vault-secrets
Pod видит /mnt/secrets/db-password — это файл с актуальным значением. CSI driver делает fetch при mount и (опционально) periodic refresh.
Плюс: секреты не попадают в etcd (если не включить syncSecret: true для backward-compat). Меньше attack surface.
Минус: app должна читать из файла, не из env. Не все апликации это умеют (часто envFrom удобнее).
3. HashiCorp Vault Agent Injector
Specific для Vault. Mutating admission webhook, который inject sidecar в Pod при annotation:
metadata:
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "web-role"
vault.hashicorp.com/agent-inject-secret-db.txt: "secret/data/web/db"
vault.hashicorp.com/agent-inject-template-db.txt: |
{{ with secret "secret/data/web/db" -}}
DB_PASSWORD={{ .Data.data.password }}
{{- end }}
Vault Agent (sidecar) делает:
- Authenticate в Vault через ServiceAccount token.
- Fetch secret.
- Render template в
/vault/secrets/db.txt. - Periodic refresh при изменении.
App читает /vault/secrets/db.txt — формат можно настроить (env-style, JSON, custom).
Secret rotation patterns
После того как value в Vault изменился — как app узнает? Три подхода.
Pattern 1: Restart на rotation (Reloader)
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
annotations:
reloader.stakater.com/auto: "true" # Reloader следит
Stakater Reloader — controller, который watch ConfigMap/Secret changes и patch Deployment (изменяет annotation), что триггерит rolling restart.
Flow:
- Vault rotation → ESO refresh → K8s Secret updated.
- Reloader замечает изменение → patch Deployment annotation.
- Deployment rolling restart → новые Pods со свежим Secret.
Простой, но требует rolling update (downtime если плохо настроен). Хорош для infrequent rotation (раз в неделю).
Pattern 2: Hot reload — app watches file
App монтирует Secret как volume (не env):
spec:
containers:
- name: web
volumeMounts:
- name: db-secret
mountPath: /etc/secrets
volumes:
- name: db-secret
secret:
secretName: web-db
K8s kubelet automatically обновляет mounted Secret в Pod, когда Secret меняется. App watch файл через inotify и hot-reload connection pool без restart:
// pseudocode
watcher.Add("/etc/secrets/password")
for event := range watcher.Events {
if event.Op&fsnotify.Write == fsnotify.Write {
newPass := readFile("/etc/secrets/password")
db.UpdatePassword(newPass) // reload без restart
}
}
Env injection не обновляется! Если используешь envFrom: secretRef — Pod получает значения один раз при старте. Изменение Secret не reflected в env — только при restart. Hot reload требует volume mount.
Плюсы: no downtime, no restart. Минусы: app должна реализовать watch logic. Для legacy apps часто проще restart.
Pattern 3: Short-lived tokens
Самый production-grade. App не использует long-lived password — каждые N минут запрашивает свежий token:
apiVersion: v1
kind: ServiceAccount
metadata:
name: web-sa
---
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
serviceAccountName: web-sa
containers:
- name: web
volumeMounts:
- name: token
mountPath: /var/run/secrets/tokens
volumes:
- name: token
projected:
sources:
- serviceAccountToken:
audience: vault # bound to Vault
expirationSeconds: 3600 # 1 hour
path: vault-token
K8s проектирует ServiceAccount token с audience и expiration. App использует этот token для authentication в Vault (или AWS via IRSA, GCP via Workload Identity). Vault возвращает short-lived DB credentials. Когда они истекают — app перезапрашивает.
Никаких persistent secrets в Pod. Compromise app → credentials истекают через час → blast radius минимален.
Vault dynamic secrets
В Vault: вместо хранения static DB password, Vault генерирует credentials on-demand:
vault read database/creds/web-role
# username: v-token-web-role-rkpQk-...
# password: A1a-XYZ-random-...
# lease_id: database/creds/web-role/12345
# lease_duration: 1h
Vault создаёт unique user в Postgres, выдаёт креды, после 1h автоматически revoke (DROP USER). Никакой shared credentials — каждый app/pod получает свои.
Когда что использовать
| Сценарий | Решение |
|---|---|
| Dev / learning K8s | Native Secret + base64 |
| Production, infrequent rotation | ESO + Reloader (restart на rotation) |
| Production, no-downtime rotation | Volume mount + inotify watch в app |
| High security, ephemeral creds | ServiceAccount token + Vault dynamic secrets |
| Mandatory: secrets не в etcd | CSI Secret Store Driver |
| Vault-shop с deep integration | Vault Agent Injector |
CKAD scope vs CKS scope
CKAD должен знать:
- Native K8s Secrets имеют ограничения (base64 ≠ encryption, manual rotation).
- Существуют external secret managers (Vault, AWS SM, GCP SM) и интеграции (ESO, CSI Secret Store, Vault Agent).
- Volume mount vs env — критичное различие для hot reload.
- Reloader / annotation-based restart — общая идея.
CKS-scope (не нужно глубоко знать на CKAD):
- Конфигурация ESO / Vault Agent Injector в деталях.
- Encryption at rest в etcd (
encryption-provider-config). - Audit policy configuration.
- KMS provider plugins.
Но в production-context CKAD-инженер должен понимать зачем эти инструменты — чтобы не предлагать “положите password в Secret” решения для high-security workloads.
Killer-моменты
- base64 ≠ encryption. Любой с
get secretspermission читает в plain text. RBAC must be strict. - Encryption at rest в etcd — отдельная настройка, не включена по умолчанию даже на managed K8s часто.
- envFrom: secretRef инжектит один раз при старте Pod. Изменение Secret не reflected — только restart. Для hot reload — volume mount.
- Volume mount Secret hot-reloaded kubelet’ом автоматически (примерно за 1 minute). App может реализовать inotify watch.
- Reloader — третий-party controller (не часть K8s), но стандарт для restart-on-rotation pattern.
- Vault dynamic secrets — gold standard для credentials к external services. Никаких shared passwords.
- ServiceAccount token с audience — bridge между K8s identity и external auth (Vault Kubernetes auth, AWS IRSA, GCP Workload Identity). Production должна использовать.