Learning Platform
Глоссарий Troubleshooting
Урок 20.04 · 22 мин
Продвинутый
Secret rotationExternal Secrets OperatorESOCSI Secret StoreHashiCorp VaultVault Agent InjectorReloaderdynamic secretsencryption at rest

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 -dpassword123. Никакой защиты — обфускация только.

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

Как работает:

  1. ESO controller watch ExternalSecret CRs.
  2. Каждые refreshInterval (1h) — fetch значение из Vault (или AWS SM, GCP SM, Azure KV — настраивается в SecretStore).
  3. Создаёт / обновляет K8s Secret object web-db с актуальными данными.
  4. Pod монтирует обычный K8s Secret.

ESO выступает как cache layer между внешним storage и K8s. Приложение использует обычный K8s Secret API — никаких изменений.

External Secrets Operator: Vault → K8s Secret → Pod
HashiCorp VaultSource of truth для секретов. Поддерживает encryption, audit, ACL, dynamic secrets, lease/renewal. ESO с Vault — самая частая комбинация в enterprise.
ESO controllerDeployment в namespace external-secrets. Watch ExternalSecret CRs, делает fetch из настроенного backend (Vault, AWS SM, GCP SM, ...) по interval. Аутентифицируется через ServiceAccount или static creds в SecretStore.
create K8s Secret
K8s SecretESO создаёт обычный K8s Secret object. Тот же base64, тот же etcd. Но содержимое — actual из Vault. Refresh при rotation в Vault.
PodPod монтирует Secret как env или volume. Через обычный mechanism — никакой ESO awareness в app. Если хочешь чтобы Pod restart при rotation — Reloader/Stakater watch annotation.

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) делает:

  1. Authenticate в Vault через ServiceAccount token.
  2. Fetch secret.
  3. Render template в /vault/secrets/db.txt.
  4. 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:

  1. Vault rotation → ESO refresh → K8s Secret updated.
  2. Reloader замечает изменение → patch Deployment annotation.
  3. 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
    }
}
WARNING

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 K8sNative Secret + base64
Production, infrequent rotationESO + Reloader (restart на rotation)
Production, no-downtime rotationVolume mount + inotify watch в app
High security, ephemeral credsServiceAccount token + Vault dynamic secrets
Mandatory: secrets не в etcdCSI Secret Store Driver
Vault-shop с deep integrationVault 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 secrets permission читает в 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 должна использовать.

Проверка знанийKnowledge check
Production app использует envFrom: secretRef для DB password. Команда обновила Secret через kubectl apply. App продолжает использовать старый password. Почему и как починить?
ОтветAnswer
envFrom инжектит Secret в env переменные Pod-а ОДНИН РАЗ при старте контейнера. Изменение Secret после старта НЕ reflected в env — это immutable копия. Чтобы Pod подхватил новое значение — нужен restart (Pod re-creation). Решения: (1) kubectl rollout restart deploy/web — manual restart. (2) Reloader / Stakater — controller watch Secret и автоматически patch Deployment annotation, что триггерит rolling restart. (3) Альтернатива — volume mount: volumes.secret.secretName → volumeMounts. kubelet автоматически обновляет mounted Secret примерно за минуту. App может реализовать inotify watch и hot-reload connection pool без restart. (4) Самое production-grade — short-lived tokens из Vault (dynamic secrets), app сам refresh каждые 1h. envFrom hot reload не поддерживает — это фундаментальное ограничение API.
Проверка знанийKnowledge check
Почему External Secrets Operator (ESO) часто предпочитают над CSI Secret Store Driver, даже если оба решают похожую задачу?
ОтветAnswer
Главное отличие: ESO создаёт обычный K8s Secret object из external store (Vault, AWS SM, ...), а CSI Secret Store mount-ит секреты как volume НАПРЯМУЮ без создания K8s Secret. Преимущества ESO: (1) Backward compat — app использует обычный envFrom/secretRef, никаких изменений кода. (2) Доступно для всех K8s features, ожидающих Secret (ImagePullSecrets, TLS Secrets для Ingress). (3) Кэш — если external store недоступен, K8s Secret продолжает работать (последнее закэшированное значение). Преимущества CSI Secret Store: (1) Секреты НЕ в etcd — меньше attack surface. (2) Меньше RBAC complexity (нет K8s Secret object). Недостатки CSI: app должна читать из файла, не env (некоторые legacy apps это не умеют). Multi-replica Pods все делают fetch к external store параллельно (load). В практике: ESO выбирают чаще из-за compat. CSI — когда security policy требует 'no secrets in etcd'.
Проверка знанийKnowledge check
Что такое Vault dynamic secrets и почему они принципиально безопаснее static credentials?
ОтветAnswer
Static creds (password в Secret) — long-lived, shared, manually rotated. При утечке — compromise до следующей ручной rotation. Vault dynamic secrets: Vault генерирует УНИКАЛЬНЫЕ credentials on-demand с коротким TTL (1h типично). API call: vault read database/creds/web-role → возвращает username v-token-web-role-XYZ-... и random password. Vault внутри Postgres делает CREATE USER ... GRANT и через 1h автоматически DROP USER (revoke lease). Преимущества: (1) Каждый Pod получает СВОИ креды — не shared, можно точно сказать кто что сделал в audit. (2) Compromise — blast radius максимум 1 час до auto-revoke. (3) Никакой rotation problem — нет 'старого password', он истёк. (4) Forensic friendly: ID пользователя в DB соответствует Vault lease ID, который привязан к Pod identity. Альтернатива: AWS IAM Roles for Service Accounts (IRSA) с STS — концептуально аналог dynamic secrets для AWS API.
Проверка знанийKnowledge check
Как обеспечить zero-downtime secret rotation для приложения с persistent connections к БД (pool of 50 connections)?
ОтветAnswer
Полностью zero-downtime требует hot reload в самом приложении. Шаги: (1) Mount Secret как volume (НЕ envFrom): volumes.secret + volumeMounts.mountPath /etc/secrets. kubelet автоматически обновляет mounted Secret примерно за минуту после изменения. (2) В app — fsnotify/inotify watch на /etc/secrets/password. При изменении: (3) Создать НОВЫЙ connection pool с новыми credentials, оставив старый. (4) Постепенно мигрировать active queries с старого pool на новый (или просто отдавать новые queries новому pool, ждать пока старые завершатся). (5) Drain старого pool — close все idle connections, дождаться in-flight. (6) Удалить старый pool. БД-сторона: pgbouncer / RDS Proxy упрощают это — там pool management отдельно от app. Альтернатива (proxy-based): Vault Agent с template + signal — Vault Agent при rotation шлёт SIGHUP в app, app обрабатывает как 'reload config'. Без hot reload в app — придётся rolling restart, что НЕ zero-downtime в строгом смысле (короткий период reduced capacity).

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. Production app использует 'envFrom: secretRef: name: db-secret'. Команда обновила Secret через kubectl apply -f. App продолжает использовать старый password. Почему?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5