Главное заблуждение: Secret в Kubernetes не зашифрован
Самая дорогая ошибка новичка - думать, что объект Secret что-то скрывает. Он не скрывает. Он кодирует. base64 - это не шифрование, это просто другой способ записать те же байты, обратимый одной командой без всякого ключа. Давай посмотрим живьём. Создаём секрет:
Код: Выделить всё
kubectl create secret generic db-creds \
--from-literal=username=app \
--from-literal=password='S3cr3t!Pass'Код: Выделить всё
kubectl get secret db-creds -o jsonpath='{.data.password}'
UzNjcjN0IVBhc3M=Код: Выделить всё
kubectl get secret db-creds -o jsonpath='{.data.password}' | base64 -d
S3cr3t!PassЧем Secret реально отличается от ConfigMap, если оба читаются легко? Отличий три, и они не про криптографию. Первое: Secret хранится в API-сервере и kubelet в памяти типа tmpfs (в RAM, не на диске ноды), а не в обычном файле. Второе: значения не светятся в kubectl describe и в логах вывода. Третье: на Secret отдельно навешивается RBAC и можно включить шифрование at-rest. То есть Secret - это правильное МЕСТО для пароля и набор крючков для защиты, но защиту надо включить руками. Из коробки её нет.

Шифрование секретов Kubernetes at-rest: EncryptionConfiguration и KMS
Первый рубеж обороны - шифрование в etcd. Делается это через EncryptionConfiguration - файл, который ты передаёшь kube-apiserver флагом --encryption-provider-config. Управляемые кластеры (Yandex Managed Kubernetes, VK Cloud, EKS, GKE) обычно дают эту опцию в настройках, в self-hosted и k3s ты правишь манифест apiserver сам.
Минимальный конфиг с локальным ключом выглядит так:
Код: Выделить всё
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <BASE64_32_BYTE_KEY>
- identity: {}Почему kms лучше локального ключа? При aescbc/aesgcm ключ шифрования лежит на диске control-plane рядом с тем, что он шифрует. Украл диск - украл и замок, и ключ. KMS-провайдер выносит мастер-ключ во внешнюю систему (облачный KMS, Vault через плагин), а в etcd попадает только зашифрованный data encryption key. На 2026 актуален KMS v2: начиная с Kubernetes 1.29 KMS v1 выключен по умолчанию (deprecated с 1.28), и включать его обратно флагом --feature-gates=KMSv1=true без причины не стоит. KMS v2 заметно производительнее: он генерирует DEK через KDF из seed плюс случайные данные, не дёргая внешний KMS на каждый секрет.
Важнейшая деталь, на которой все спотыкаются: включение шифрования НЕ перешифровывает то, что уже лежит в etcd. Старые секреты остаются открытыми, пока их не перезапишут. После настройки конфига обязательно прогони перешифровку:
Код: Выделить всё
kubectl get secrets --all-namespaces -o json | kubectl replace -f -Код: Выделить всё
ETCDCTL_API=3 etcdctl get /registry/secrets/default/db-creds | hexdump -CRBAC на секреты и дефолтный токен ServiceAccount
Шифрование at-rest спасает от кражи диска, но не от того, кто имеет права в кластере. Второй рубеж - RBAC. Право get/list/watch на secrets надо раздавать точечно. Особенно опасен list по всему кластеру: он отдаёт значения всех секретов разом. Правило простое - давай доступ к конкретным секретам по имени через resourceNames, а не к ресурсу secrets целиком:
Код: Выделить всё
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: read-db-creds
namespace: prod
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["db-creds"]
verbs: ["get"]Код: Выделить всё
apiVersion: v1
kind: ServiceAccount
metadata:
name: web
automountServiceAccountToken: falseExternal Secrets: как не держать пароли в кластере вообще
Шифрование и RBAC закрывают дыры, но не решают корневую проблему: секрет всё равно живёт в кластере, его надо где-то хранить, ротировать, не закоммитить в git. Зрелый подход - вынести источник правды наружу, в специализированное хранилище (HashiCorp Vault, Yandex Lockbox, AWS Secrets Manager, GCP Secret Manager), а в кластер их только синхронизировать. Здесь на сцену выходит External Secrets Operator (ESO).
Идея ESO: ты описываешь в git не сам секрет, а ССЫЛКУ на него во внешнем хранилище. Оператор читает эту ссылку, ходит в хранилище, забирает значение и САМ создаёт обычный Kubernetes Secret в кластере. В git при этом нет ни одного пароля - только адрес. Две основные сущности (на 2026 API стабилизировался до external-secrets.io/v1, v1beta1 объявлен устаревшим и в свежих релизах удалён - смотри версию своего оператора):
SecretStore описывает, КУДА и КАК ходить (адрес Vault, способ аутентификации). ExternalSecret описывает, ЧТО забрать и в какой Secret положить. Пример связки с Vault через Kubernetes-аутентификацию:
Код: Выделить всё
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: vault-backend
namespace: prod
spec:
provider:
vault:
server: "https://vault.internal:8200"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "prod-app"
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: db-creds
namespace: prod
spec:
refreshInterval: "1h"
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: db-creds
creationPolicy: Owner
data:
- secretKey: password
remoteRef:
key: prod/database
property: passwordКод: Выделить всё
kubectl get externalsecret db-creds -n prod
NAME STORE REFRESH STATUS READY
db-creds vault-backend 1h SecretSynced TrueSealed Secrets и SOPS: секреты прямо в git, но безопасно
ESO требует внешнее хранилище. А если его нет и хочется GitOps, чтобы вообще всё лежало в репозитории? Тогда секрет надо положить в git ЗАШИФРОВАННЫМ так, чтобы расшифровать его мог только кластер. Два рабочих инструмента.
Sealed Secrets (от Bitnami). В кластере живёт контроллер с приватным ключом. Ты утилитой kubeseal шифруешь свой Secret публичным ключом - получается ресурс SealedSecret, который безопасно коммитить: расшифровать его может ТОЛЬКО этот контроллер этим приватным ключом, больше никто.
Код: Выделить всё
kubectl create secret generic db-creds \
--from-literal=password='S3cr3t!Pass' \
--dry-run=client -o yaml \
| kubeseal --format yaml > sealed-db-creds.yamlКод: Выделить всё
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: db-creds
namespace: prod
spec:
encryptedData:
password: AgBy3i4OJSWK+PiTySYZZA9rO43cGDEq...SOPS (Secrets OPerationS) от Mozilla - другой подход: шифрует только ЗНАЧЕНИЯ в YAML/JSON, оставляя ключи и структуру читаемыми, через age, PGP или облачный KMS. Удобно для код-ревью: видно, какие поля изменились, без раскрытия самих значений. В GitOps SOPS дружит с Flux (нативно) и с Argo CD (через плагин). Выбор между ними простой: Sealed Secrets - проще, заточен под Kubernetes Secret; SOPS - гибче, шифрует любые файлы и красиво ложится в diff.
CSI Secrets Store: монтировать, а не хранить
Есть и четвёртый путь - вообще не создавать объект Secret, а монтировать секрет из Vault прямо в файловую систему пода как том. Это Secrets Store CSI Driver плюс провайдер (Vault, облачные KMS). Под получает секрет файлом в /mnt/secrets, в etcd при этом не пишется ничего. Описывается через SecretProviderClass, а в поде объявляется CSI-том. Полезные опции драйвера: enableSecretRotation периодически опрашивает хранилище (по умолчанию раз в 2 минуты) и обновляет смонтированный том, а syncSecret при желании дополнительно зеркалит данные в обычный Kubernetes Secret. Краевой случай, про который забывают: приложение читает секреты ОДИН раз на старте. Ротация обновит файл, но процесс продолжит работать со старым значением в памяти - под обновлённые значения часто нужен rolling restart деплоймента.
Типичные грабли и антипаттерны
- Секрет в git открытым текстом. Самая частая утечка. Закоммитил Secret с base64 в values.yaml - считай, выложил пароль. base64 не считается за защиту. Лечится Sealed Secrets/SOPS/ESO.
- Секрет в образе или в ENV логах. ARG/ENV в Dockerfile с паролем остаётся в слоях образа навсегда. А printenv в стартовом скрипте или дамп окружения при краше утащит секрет в логи и в систему сбора логов.
- Секрет через args, а не env/файл. Аргументы команды видны в ps и в kubectl describe pod. Передавай чувствительное через переменные окружения из секрета или через файл-том.
- Дефолтный токен SA в каждом поде. Прокинутый токен default SA - готовый плацдарм для повышения привилегий из взломанного контейнера. Отключай automountServiceAccountToken там, где он не нужен.
- Включили шифрование, не перешифровали старое. EncryptionConfiguration не трогает уже лежащие в etcd секреты. Без kubectl get secrets ... | kubectl replace -f - половина данных так и останется открытой.
- identity первым провайдером. Перепутал порядок в providers - и всё пишется открытым текстом, хотя конфиг вроде есть.
- Создай Secret через kubectl create secret generic и докажи себе, что это не шифрование: вытащи значение через base64 -d.
- В тестовом self-hosted кластере или k3s включи EncryptionConfiguration с провайдером aescbc, прогони перешифровку через kubectl replace и проверь шифротекст в etcd через etcdctl + hexdump (ищи префикс k8s:enc:aescbc:v1:).
- Поставь Sealed Secrets контроллер, через kubeseal заверни секрет в SealedSecret и убедись, что encryptedData нечитаем без кластера.
- Разверни External Secrets Operator, подними dev-Vault, опиши SecretStore + ExternalSecret и проверь, что ESO сам создал Secret (kubectl get externalsecret, статус SecretSynced/Ready).
- Почему объект Secret в Kubernetes по умолчанию нельзя считать защищённым и что конкретно происходит с его данными в etcd?
- Что делает EncryptionConfiguration, почему порядок провайдеров критичен и почему после его включения обязательно перешифровать существующие секреты?
- В чём разница подходов External Secrets Operator, Sealed Secrets/SOPS и CSI Secrets Store - и когда какой выбрать?
- Зачем отключать automountServiceAccountToken и чем рискует под с лишним токеном SA?
Secret в Kubernetes - это удобное место и набор крючков, но не защита: base64 читается мгновенно, в etcd по умолчанию открытый текст. Настоящая безопасность собирается слоями: шифрование at-rest через EncryptionConfiguration/KMS, жёсткий RBAC по resourceNames, отключение лишних токенов SA, и вынос источника правды наружу - External Secrets из Vault/Lockbox, Sealed Secrets или SOPS для GitOps, CSI Secrets Store для монтирования без хранения. И помни про ротацию и rolling restart: положить секрет мало, его ещё надо вовремя менять.