Хранение паролей в Kubernetes: Secrets, External Secrets и Vault - практическое руководство | AdminWiki

Хранение паролей в Kubernetes: Secrets, External Secrets и Vault - практическое руководство

15 сентября 2026 16 мин. чтения

Пароль в обычном Kubernetes Secret лежит в etcd в кодировке base64. Кодирование обратимо без ключа: одна команда kubectl или прямой запрос к etcd возвращает пароль в читаемом виде. Ответ на главный вопрос зависит от среды. В dev-кластере на одной машине встроенного Secret достаточно. В production, где к API-серверу есть доступ у нескольких команд и внешних сетей, нужны шифрование etcd, строгий RBAC и вынос секретов во внешнее хранилище.

Рабочая схема защиты собирается из трёх слоёв. Шифрование etcd (encryption at rest) закрывает диски и снапшоты резервных копий, RBAC ограничивает чтение через API. Внешнее хранилище, HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager или Azure Key Vault, убирает долгоживущие пароли из кластера и даёт ротацию. Шифрование на стороне Git, Sealed Secrets или SOPS, позволяет держать манифесты в репозитории и раскатывать их через ArgoCD или Flux.

Ниже порядок действий с примерами: устройство хранения в etcd, включение encryption at rest, рабочие Role и RoleBinding, установка Vault Agent Injector, динамические секреты для PostgreSQL, манифесты SecretStore и ExternalSecret, настройка Sealed Secrets и SOPS с age, таблица сравнения и разбор типовых ошибок. Базовые операции с Secret (создание, монтирование, TLS-сертификаты) разобраны в полном руководстве по Kubernetes Secrets.

Почему стандартные Kubernetes Secrets небезопасны

Secret в Kubernetes - обычный объект API с полем data, где значения закодированы в base64. Отдельного хранилища, отдельного шифрования и отдельной модели доступа у него нет: объект лежит в etcd рядом с ConfigMap и описаниями подов. Ограничения следуют из этого напрямую.

  • Чтение из etcd. Без encryption at rest пароли доступны каждому, у кого есть доступ к файлам etcd или к снапшотам бэкапов.
  • Доступ через API. Роль с verbs: [get, list] на ресурс secrets в namespace раскрывает все секреты этого namespace.
  • Право создавать поды. Возможность запустить под в namespace равна доступу ко всем Secret namespace: секрет монтируется в контейнер за минуту.
  • Утечка через Git. Сохранённый в репозиторий вывод kubectl get secret -o yaml оставляет пароль в истории коммитов.
  • Переменные окружения. Значения видны в описании пода, в /proc/1/environ контейнера, в дампах и логах.
  • Редкая ротация. Один пароль живёт годами и используется всеми сервисами сразу.

Отсюда вырастают четыре практических подхода: шифрование etcd плюс RBAC, внешнее хранилище с инжекцией секретов в под, внешнее хранилище с синхронизацией через оператор, шифрование секретов для Git. Подходы комбинируются, и об этом дальше.

Как хранятся Secrets в etcd и что такое base64

При создании Secret через kubectl значение кодируется автоматически, без участия администратора:

kubectl create secret generic db-cred -n payments \
  --from-literal=username=app \
  --from-literal=password=S3cr3tP@ss

В etcd уходит объект такого вида:

apiVersion: v1
kind: Secret
metadata:
  name: db-cred
  namespace: payments
type: Opaque
data:
  username: YXBw
  password: UzNjcjN0UEBzcw==

Обратная операция выполняется одной строкой и не требует ни ключа, ни пароля:

kubectl get secret db-cred -n payments -o jsonpath='{.data.password}' | base64 -d

base64 - это представление бинарных данных текстом по общеизвестному алфавиту. Декодирует значение любая стандартная утилита, включая base64 из coreutils. Именно поэтому фраза «секрет зашифрован в base64» неверна технически и опасна на практике: она создаёт ложное чувство защиты.

Проверить, включено ли шифрование etcd, можно по флагам API-сервера:

kubectl -n kube-system get pod kube-apiserver-node1 -o yaml | grep encryption-provider-config
ps -ef | grep kube-apiserver | grep encryption-provider-config

Пустой вывод означает, что все Secret лежат в etcd открытым текстом после декодирования base64. Так выглядит состояние по умолчанию у кластеров, развёрнутых kubeadm, k3s и большинством managed-провайдеров, если шифрование не включили отдельно.

Шифрование etcd: включаем encryption at rest

Encryption at rest шифрует объекты на уровне API-сервера перед записью в etcd. Ключ хранится вне etcd: в файле на control plane или в облачном KMS. Порядок шагов для кластера kubeadm:

  1. Сгенерировать 32-байтовый ключ шифрования.
  2. Создать файл EncryptionConfiguration и положить его на все control plane узлы.
  3. Добавить флаг --encryption-provider-config в манифест kube-apiserver и смонтировать каталог с ключом.
  4. Перезапустить API-сервер, по одному узлу, начиная с standby.
  5. Перезаписать существующие Secret, чтобы они зашифровались.
head -c 32 /dev/urandom | base64

Файл /etc/kubernetes/enc/encryption-config.yaml:

apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
      - configmaps
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: ключ-из-первого-шага
      - identity: {}

Провайдер, указанный первым, используется для записи; остальные участвуют в чтении. Это позволяет ротировать ключи без простоя: новый ключ добавляется наверх, старый остаётся ниже для чтения уже зашифрованных объектов. Провайдер identity в конце списка оставляют только на время миграции, иначе часть объектов продолжит писаться в открытом виде.

Флаг добавляется в /etc/kubernetes/manifests/kube-apiserver.yaml вместе с монтированием каталога через hostPath /etc/kubernetes/enc:

- --encryption-provider-config=/etc/kubernetes/enc/encryption-config.yaml

После перезапуска API-сервера нужно перезаписать существующие Secret, иначе они останутся в прежнем виде:

kubectl get secrets --all-namespaces -o json | kubectl replace -f -

Проверка: префикс значения в etcd должен содержать метку шифрования k8s:enc:aescbc:v1:key1. Для управляемых кластеров эквивалент - включить KMS-провайдер (kms v2), тогда мастер-ключ живёт в облачном KMS и ротируется автоматически.

Что даёт encryption at rest и чего не даёт. Защищены файлы etcd, снапшоты и резервные копии. Не защищены секреты, которые кто-то прочитал через API или выгрузил в Git: там шифрование etcd не участвует. Ключ шифрования нужно хранить отдельно от бэкапов etcd, иначе шифрование теряет смысл. Потеря ключа делает все Secret нечитаемыми, восстановление кластера придётся делать вместе с ключом. Подробные примеры объектов и их подключения к подам есть в материале про ConfigMap и Secret.

Настройка RBAC для ограничения доступа к секретам

RBAC решает вторую половину задачи: даже если шифрование etcd включено, право list secrets отдаёт все пароли namespace одному сервисному аккаунту. Три типовые дыры встречаются чаще всего.

  • Роль с list и watch на ресурс secrets: она не ограничивается списком имён и показывает содержимое всех объектов.
  • ClusterRole с широким доступом, выданная CI-системе: компромисс раннера равен компромиссу всего кластера.
  • Право create pods в namespace: под можно создать с монтированием любого существующего Secret, поэтому доступ к секретам фактически есть.

Минимальная рабочая конструкция даёт доступ к конкретному Secret по имени:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: db-cred-reader
  namespace: payments
rules:
  - apiGroups: ['']
    resources: ['secrets']
    resourceNames: ['db-cred']
    verbs: ['get']
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: db-cred-reader
  namespace: payments
subjects:
  - kind: ServiceAccount
    name: payments-api
    namespace: payments
roleRef:
  kind: Role
  name: db-cred-reader
  apiGroup: rbac.authorization.k8s.io

Ограничение resourceNames работает только для get и delete. Глаголы list и watch по имени не ограничиваются, поэтому приложение должно запрашивать Secret по конкретному имени, а не перечислять их. Для сервисов, которым секреты не нужны вовсе, отключите автоматическое монтирование токена: automountServiceAccountToken: false в spec пода или в самом ServiceAccount.

Проверка прав выполняется без запуска пода:

kubectl auth can-i get secrets -n payments --as=system:serviceaccount:payments:payments-api
kubectl auth can-i list secrets -n payments --as=system:serviceaccount:payments:payments-api
kubectl auth can-i create pods -n payments --as=system:serviceaccount:payments:payments-api

Второй и третий вызовы должны возвращать no для прикладных сервисных аккаунтов. Для аудита доступа включается audit policy с уровнем Metadata: тела запросов с секретами в журнал не пишутся, а факт обращения фиксируется.

apiVersion: audit.k8s.io/v1
kind: Policy
rules:
  - level: Metadata
    resources:
      - group: ''
        resources: ['secrets']

Уровень RequestResponse для ресурса secrets использовать нельзя: в лог попадут сами значения. Общая модель ролей и сетевых политик в production-кластере разобрана в статье про оркестрацию Kubernetes.

HashiCorp Vault и Kubernetes: интеграция через sidecar-инжектор

Vault хранит секреты вне кластера и вне Git, выдаёт их по короткоживущим токенам и умеет генерировать временные учётные данные для баз данных. Аутентификация строится на ServiceAccount: под предъявляет свой JWT, Vault проверяет его через TokenReview API и возвращает токен Vault с привязанной политикой. Vault Agent Injector работает как mutating admission webhook: при создании пода он добавляет init-контейнер и sidecar vault-agent, которые забирают секрет и кладут его в общий том emptyDir.

Альтернатива инжектору - Secrets Store CSI Driver с драйвером Vault, монтирующий секреты как файлы без дополнительных контейнеров. Инжектор удобнее для шаблонов и обновляемых динамических секретов; CSI-драйвер удобнее там, где admission-вебхукам не разрешено менять спецификации подов.

Установка и настройка Vault Agent Injector

Для тестов подходит dev-режим с одним подом и автоматическим unseal, для production нужен HA-кластер с хранилищем Raft и TLS-сертификатом от внутреннего CA. Установка через Helm:

helm repo add hashicorp repo-url-hashicorp
helm repo update
helm install vault hashicorp/vault -n vault --create-namespace \
  --set server.ha.enabled=true \
  --set server.ha.replicas=3 \
  --set injector.enabled=true

Точный адрес Helm-репозитория и актуальные версии чартов смотрите в официальной документации проекта: они меняются, а последовательность действий остаётся такой же. После запуска Vault распечатывается (unseal), затем включается Kubernetes-аутентификация и создаётся политика:

vault auth enable kubernetes
vault write auth/kubernetes/config kubernetes_host=https://kubernetes.default.svc

vault kv put secret/myapp username=app password=s3cr3t
vault policy write myapp myapp-policy.hcl

vault write auth/kubernetes/role/myapp \
  bound_service_account_names=payments-api \
  bound_service_account_namespaces=payments \
  policies=myapp ttl=1h

Файл политики myapp-policy.hcl ограничивает доступ одним путём:

path 'secret/data/myapp' {
  capabilities = ['read']
}

path 'secret/metadata/myapp' {
  capabilities = ['read']
}

Дальше секрет в под доставляется аннотациями, без изменения образа приложения:

apiVersion: v1
kind: Pod
metadata:
  name: payments-api
  namespace: payments
  annotations:
    vault.hashicorp.com/agent-inject: 'true'
    vault.hashicorp.com/role: 'myapp'
    vault.hashicorp.com/agent-inject-secret-password: 'secret/data/myapp'
    vault.hashicorp.com/agent-inject-template-password: |
      {{- with secret 'secret/data/myapp' -}}
      {{ .Data.data.password }}
      {{- end }}
spec:
  serviceAccountName: payments-api
  containers:
    - name: api
      image: registry.example/payments-api:1.4.2
      volumeMounts:
        - name: vault-secrets
          mountPath: /vault/secrets
          readOnly: true

Пароль появится в файле /vault/secrets/password внутри контейнера. Vault Agent обновляет токен и перезаписывает файл при ротации, поэтому приложение должно перечитывать значение, а не запоминать его при старте: ориентируйтесь на изменение mtime файла или используйте аннотацию agent-inject-command для отправки сигнала процессу. Практические грабли: аннотации не работают, если serviceAccountName не совпадает с ролью или namespace в аннотации не тот; для нескольких секретов используйте суффиксы agent-inject-secret-имя и agent-inject-template-имя.

Динамические секреты Vault для баз данных

Database secrets engine создаёт пользователя в PostgreSQL по запросу и удаляет его по истечении TTL. Пароля, который можно унести и переиспользовать, не существует дольше часа. Настройка:

vault secrets enable database

vault write database/config/postgres \
  plugin_name=postgresql-database-plugin \
  allowed_roles='app-readonly' \
  connection_url='postgresql://{{username}}:{{password}}@postgres.payments.svc:5432/appdb?sslmode=disable' \
  username=vault_admin \
  password=vault-admin-password

vault write database/roles/app-readonly \
  db_name=postgres \
  default_ttl=1h \
  max_ttl=24h \
  creation_statements=@role.sql

vault read database/creds/app-readonly

Содержимое role.sql:

CREATE ROLE '{{name}}' WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';
GRANT SELECT ON ALL TABLES IN SCHEMA public TO '{{name}}';

Ответ vault read содержит lease_id, username и password. Тот же путь подставляется в аннотацию пода: vault.hashicorp.com/agent-inject-secret-db: database/creds/app-readonly. Приложение получает новые креды каждый раз, когда agent перезаписывает файл, а предыдущий пользователь БД отзывается по lease. Требование к приложению: уметь переподключаться к БД без перезапуска, иначе ротация превратится в простой.

Vault становится критичной зависимостью: при его недоступности новые поды с инжектором не стартуют, если не выставлена аннотация vault.hashicorp.com/agent-pre-populate-only: 'true'. Для production нужны HA-кластер, автоматический unseal через KMS, снапшоты Raft и мониторинг времени ответа. Сравнение Vault с облачными Secret Manager по критериям безопасности, стоимости и удобства собрано в отдельном материале.

External Secrets Operator: синхронизация секретов из внешних хранилищ

External Secrets Operator (ESO) работает иначе: он не меняет поды, а создаёт обычный Kubernetes Secret по данным из внешнего хранилища и обновляет его по таймеру. Приложения читают Secret привычным способом. Один оператор закрывает Vault, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault, Yandex Lockbox и десятки других провайдеров через единый API.

helm repo add external-secrets repo-url-external-secrets
helm repo update
helm install external-secrets external-secrets/external-secrets \
  -n external-secrets --create-namespace \
  --set installCRDs=true

Схема работы: SecretStore или ClusterSecretStore описывает доступ к хранилищу, ExternalSecret описывает, какой ключ превратить в какой Secret. Права в Vault выдаются через Kubernetes-аутентификацию, как и для инжектора.

Пример манифеста ExternalSecret для Vault

apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: vault-backend
  namespace: payments
spec:
  provider:
    vault:
      server: http://vault.vault.svc:8200
      path: secret
      version: v2
      auth:
        kubernetes:
          mountPath: kubernetes
          role: eso-payments
          serviceAccountRef:
            name: eso-payments
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: db-cred
  namespace: payments
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: vault-backend
    kind: SecretStore
  target:
    name: db-cred
    creationPolicy: Owner
  data:
    - secretKey: password
      remoteRef:
        key: myapp
        property: password
    - secretKey: username
      remoteRef:
        key: myapp
        property: username

После применения ESO создаст Secret db-cred в namespace payments и будет обновлять его каждый час. Диагностика:

kubectl get externalsecret -n payments
kubectl describe externalsecret db-cred -n payments
kubectl get secret db-cred -n payments -o jsonpath='{.metadata.annotations}'

Ключевое ограничение Kubernetes: Secret, смонтированный как том, обновляется в контейнере автоматически (обычно в пределах минуты), но приложение должно само перечитать файл. Для переменных окружения обновление не работает вообще: env подставляется один раз при старте контейнера. Если приложение не умеет перечитывать файл, ставьте reloader или добавляйте rollout restart в CI. Разбор типичных ошибок синхронизации и настройка прав для ESO есть в руководстве по External Secrets Operator.

Использование ClusterSecretStore для нескольких namespace

SecretStore привязан к namespace: удобно для изоляции команд, но требует отдельного объекта на каждую команду. ClusterSecretStore описывается один раз на кластер, а ExternalSecret ссылается на него из любого namespace:

apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
  name: vault-global
spec:
  provider:
    vault:
      server: http://vault.vault.svc:8200
      path: secret
      version: v2
      auth:
        kubernetes:
          mountPath: kubernetes
          role: eso-global
          serviceAccountRef:
            name: eso-global
            namespace: external-secrets
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: smtp-cred
  namespace: notifications
spec:
  refreshInterval: 30m
  secretStoreRef:
    name: vault-global
    kind: ClusterSecretStore
  target:
    name: smtp-cred
  data:
    - secretKey: password
      remoteRef:
        key: smtp
        property: password

Обратная сторона: ClusterSecretStore открывает всем namespace одни и те же пути Vault, и разделение остаётся только на уровне политики Vault. Практика: держите в ClusterSecretStore общие ключи (SMTP, ключи внешних API, сертификаты), а прикладные секреты храните в namespace-scoped SecretStore с отдельной ролью на каждый namespace. Права сервисного аккаунта eso-global тоже ограничиваются: отдельный ServiceAccount, Role на чтение только тех Secret, которые оператор создаёт.

Sealed Secrets и SOPS для GitOps-сценариев

GitOps требует, чтобы состояние кластера целиком лежало в Git. Обычный Secret в репозиторий положить нельзя, поэтому применяют шифрование на стороне Git: расшифровка происходит уже внутри кластера. Два инструмента решают задачу разными способами, и выбор между ними определяет, насколько просто будет перенести конфигурацию в другой кластер.

Установка и использование Sealed Secrets

Sealed Secrets состоит из контроллера в кластере и утилиты kubeseal. Контроллер держит приватный ключ расшифровки в Secret в namespace kube-system, а в Git попадает объект SealedSecret, который читается только этим кластером.

kubectl apply -f controller-manifest-sealed-secrets.yaml
kubectl get pods -n kube-system -l name=sealed-secrets-controller

Манифест контроллера берётся из раздела Releases репозитория проекта; для продакшена фиксируйте версию, а не плавающий latest. Шифрование существующего Secret:

kubectl create secret generic db-cred -n payments \
  --from-literal=username=app \
  --from-literal=password=S3cr3tP@ss \
  --dry-run=client -o yaml > secret.yaml

kubeseal --format yaml --namespace payments secret.yaml > sealedsecret.yaml

Результат коммитится в Git и спокойно проходит код-ревью, потому что значения нечитаемы:

apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: db-cred
  namespace: payments
spec:
  encryptedData:
    username: AgBy8hC9...
    password: AgCzTn7Q...
  template:
    metadata:
      name: db-cred
      namespace: payments
    type: Opaque

Что учитывать: объект привязан к namespace и имени, поэтому переименование или перенос требует повторного шифрования. Приватный ключ контроллера нужно забэкапить: после пересоздания кластера без ключа все SealedSecret в Git станут бесполезны. Контроллер хранит предыдущие ключи и продолжает расшифровывать старые объекты, поэтому ротация ключа не ломает совместимость. Для нескольких кластеров либо используйте один общий ключ, либо заводите отдельные ветки Git и свой ключ на кластер.

Шифрование секретов с помощью SOPS и age

SOPS шифрует значения внутри YAML, оставляя ключи и структуру читаемыми, и поддерживает age, GPG и KMS облачных провайдеров. В диффе видно, какой ключ изменился, а само значение остаётся зашифрованным.

apt install age
age-keygen -o age.key

# в выводе команды будет публичный ключ вида age1..., приватный останется в age.key

Правила задаются в .sops.yaml в корне репозитория:

creation_rules:
  - path_regex: clusters/prod/.*\.yaml$
    encrypted_regex: '^(data|stringData)$'
    age: age1q7x2v...публичный-ключ

Шифрование и правка файла:

sops -e secret.yaml > secret.enc.yaml
sops secret.enc.yaml

Внутри secret.enc.yaml значения выглядят так:

apiVersion: v1
kind: Secret
metadata:
  name: db-cred
  namespace: payments
type: Opaque
stringData:
  username: ENC[AES256_GCM,data:YXBw,iv:...,tag:...,type:str]
  password: ENC[AES256_GCM,data:c4Fk...,iv:...,tag:...,type:str]
sops:
  age:
    - recipient: age1q7x2v...
  encrypted_regex: ^(data|stringData)$
  version: 3.8.1

Flux расшифрует файл, если в Kustomization указан ключ:

apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: payments
  namespace: flux-system
spec:
  interval: 10m
  path: ./clusters/prod/payments
  prune: true
  sourceRef:
    kind: GitRepository
    name: flux-system
  decryption:
    provider: sops
    secretRef:
      name: sops-age

Приватный ключ age кладётся в Secret sops-age в namespace flux-system и создаётся вручную при bootstrap. ArgoCD работает с SOPS через плагин или боковой контейнер, тогда как Sealed Secrets в ArgoCD и Flux раскатывается без доработок: в Git лежит обычный Kubernetes-объект. Сравнение по сути: Sealed Secrets проще и не требует внешних ключей при чтении, но привязан к кластеру и namespace; SOPS требует управления ключами age или GPG, зато одинаково работает с любым кластером, с Ansible и с обычным CI.

Сравнение подходов и рекомендации по выбору

ПодходГде хранится секретGitOpsДинамические секретыСложность
Secret + encryption at restetcd, зашифрованнетнетнизкая
RBAC как дополнениеetcd-нетнизкая
Sealed Secretsetcd, источник в Git зашифрованданетнизкая
SOPS + age или KMSetcd, источник в Git зашифрованданетсредняя
External Secrets Operatorвнешнее хранилищедачастично, через Vaultсредняя
Vault + Agent InjectorVaultаннотации в манифестедавысокая

Рекомендации по профилю нагрузки:

  • Один небольшой кластер, 2-5 разработчиков, нет требований PCI DSS или ГОСТ: encryption at rest плюс Sealed Secrets. Минимум компонентов, которые могут упасть.
  • Несколько кластеров и общий репозиторий: SOPS с age или облачным KMS, расшифровка на стороне Flux или ArgoCD.
  • Команда от 10 человек и уже используемое облачное хранилище (AWS, GCP, Azure): External Secrets Operator. Секрет живёт в хранилище, кластер получает только копию.
  • Требования к короткоживущим учётным данным, аудит каждого обращения, работа с БД и PKI: Vault с Agent Injector и динамическими секретами.
  • Банк, финтех, аудит по ISO 27001: комбинация, где Vault служит источником истины, ESO синхронизирует секреты в кластер, а SOPS шифрует bootstrap-секреты самого ESO и Vault.

Если control plane не хочется обслуживать своими силами, managed Kubernetes снимает часть работы по узлам, обновлениям версии и интеграции с сетями, а модель доступа к секретам остаётся на вашей стороне. Например, Timeweb Cloud даёт кластеры Kubernetes рядом с VDS, базами данных и объектным хранилищем, что удобно для схем, где Vault и ESO разворачиваются в том же облаке.

Типичные ошибки и лучшие практики

Ниже список ошибок, которые чаще всего приводят к утечке паролей в кластерах Kubernetes, и способ их закрыть.

  • base64 вместо шифрования. Считается, что Secret защищён, потому что значение нечитаемо на глаз. Решение: включить encryption at rest и не полагаться на кодирование.
  • Секрет в Git открытым текстом. Даже после удаления файла значение остаётся в истории коммитов. Решение: сканеры утечек в CI (gitleaks, trufflehog), плюс перевыпуск всех засветившихся паролей, потому что история уже разошлась по клонам.
  • Переменные окружения вместо томов. Значения видны в /proc/1/environ, в kubectl describe pod и в дампах процесса. Решение: монтировать Secret как том с readOnly и defaultMode 0400.
  • Один Secret на всё приложение. Компромисс одного компонента раскрывает доступ к БД, к внешним API и к внутренним сервисам сразу. Решение: отдельный Secret на каждую группу кредов.
  • Широкий RBAC. Роль с list secrets или кластерная роль для CI выдаёт всё содержимое namespace. Решение: get по resourceNames, отдельные ServiceAccount на приложение, проверка через kubectl auth can-i.
  • Отсутствие ротации. Пароль живёт годами, уволенный сотрудник сохраняет доступ. Решение: TTL и динамические секреты Vault, либо ротация по расписанию с перезапуском потребителей.
  • Отсутствие аудита. Невозможно понять, кто читал секрет и когда. Решение: audit policy уровня Metadata для ресурса secrets и централизованный сбор логов.
  • Секреты в ConfigMap. Ключи API, включая ключи к внешним сервисам вроде агрегаторов моделей вроде AiTunnel, попадают в ConfigMap, который читают все. Решение: хранить их тем же способом, что и пароли БД: в Vault или облачном Secret Manager.
  • Отсутствие шифрования бэкапов etcd. Снапшоты уезжают в объектное хранилище без защиты. Решение: шифрование etcd плюс шифрование самих снапшотов и хранение ключа отдельно.

Перед выкаткой в production проверьте конфигурацию в тестовом кластере, а не на живом: смена провайдера шифрования или перевод приложения с env на тома требует перезапуска подов и часто правки кода. Начните с трёх проверок текущего кластера: есть ли флаг encryption-provider-config у API-сервера, возвращает ли kubectl auth can-i list secrets значение no для прикладных сервисных аккаунтов, остались ли в репозиториях base64-строки в манифестах Secret. По результатам выбирайте схему из таблицы выше и двигайтесь от простого к сложному: Sealed Secrets или SOPS на старте, External Secrets Operator при росте команды, Vault при требованиях к ротации и аудиту.

Поделиться:
Сохранить гайд? В закладки браузера