Пароль в обычном 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:
- Сгенерировать 32-байтовый ключ шифрования.
- Создать файл EncryptionConfiguration и положить его на все control plane узлы.
- Добавить флаг --encryption-provider-config в манифест kube-apiserver и смонтировать каталог с ключом.
- Перезапустить API-сервер, по одному узлу, начиная с standby.
- Перезаписать существующие 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 rest | etcd, зашифрован | нет | нет | низкая |
| RBAC как дополнение | etcd | - | нет | низкая |
| Sealed Secrets | etcd, источник в Git зашифрован | да | нет | низкая |
| SOPS + age или KMS | etcd, источник в Git зашифрован | да | нет | средняя |
| External Secrets Operator | внешнее хранилище | да | частично, через Vault | средняя |
| Vault + Agent Injector | Vault | аннотации в манифесте | да | высокая |
Рекомендации по профилю нагрузки:
- Один небольшой кластер, 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 при требованиях к ротации и аудиту.