Управление секретами
Объект Secret в Kubernetes по умолчанию не шифрует данные — он лишь кодирует их в base64.
Secret — ресурс Kubernetes для хранения чувствительных данных (паролей, токенов, ключей), который отделяет их от манифеста пода. По умолчанию значения хранятся в etcd закодированными в base64, но не зашифрованными.
Самое опасное заблуждение новичка: «положил пароль в Secret — значит, он защищён». На деле base64 — это просто кодировка, она разворачивается одной командой кем угодно. Безопасность секретов в Kubernetes складывается из нескольких слоёв, и объект Secret — лишь первый из них. Этот урок про то, как закрыть остальные.
Зачем это на практике
Секреты утекают тремя путями: их случайно коммитят в Git, их читают через слишком широкий RBAC, или их достают прямо из etcd на скомпрометированной ноде. На каждый путь есть своя оборона: не коммитить (и сканировать репозиторий), ограничить доступ ролями (см. урок про RBAC) и включить шифрование etcd at-rest. Понимание, что именно делает Secret, а что нет, — отправная точка.
base64 — это не шифрование
Создадим секрет и убедимся, что «защита» снимается мгновенно:
kubectl create secret generic db-cred \
--from-literal=password='S3cr3t!'
# Достаём значение и декодируем его одной строкой
kubectl get secret db-cred -o jsonpath='{.data.password}' | base64 -d
Вывод:
S3cr3t!
Любой, у кого есть право get secrets в этом namespace, видит пароль открытым текстом. Вывод прямой: контроль доступа к секретам — это RBAC, а не сама кодировка. В манифесте секрет выглядит так:
apiVersion: v1
kind: Secret
metadata:
name: db-cred
type: Opaque
data:
password: UzNjcjN0IQ== # это base64, а не шифр
Шифрование etcd at-rest
Все объекты кластера, включая секреты, лежат в базе etcd. Если злоумышленник получит дамп etcd или диск ноды control-plane, он прочитает все секреты — base64 ему не помеха. Лекарство — encryption at rest: API-сервер шифрует значения секретов перед записью в etcd. Настраивается это через EncryptionConfiguration на control-plane:
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc: # шифруем ключом AES-CBC
keys:
- name: key1
secret: <base64-32-байтного-ключа>
- identity: {} # fallback для чтения старых записей
Порядок провайдеров важен: первым стоит шифрующий (он используется для записи), identity — в конце, чтобы читать ещё не перешифрованные записи. В managed-кластерах (EKS, GKE, AKS) шифрование секретов внешним KMS обычно включается одной галочкой — это рекомендованный путь.
External secrets: хранить снаружи
Более зрелый подход — вообще не держать «настоящие» секреты в кластере, а подтягивать их из внешнего менеджера (HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager). Оператор External Secrets синхронизирует значение из хранилища в обычный Secret по описанию:
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: db-cred
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: db-cred # какой k8s-Secret создать
data:
- secretKey: password
remoteRef:
key: prod/db
property: password
Плюсы такого подхода: секрет ротируется централизованно, в Git хранится только ссылка на него, а не значение, и аудит доступа ведётся во внешнем менеджере.
Монтирование: переменные или файлы
Секрет отдают поду двумя способами. Через переменные окружения это удобно, но env легко утекает в логи и виден в kubectl describe pod. Через том — каждое значение становится файлом, что безопаснее и поддерживает обновление без перезапуска:
spec:
containers:
- name: app
image: my-app:2.0
volumeMounts:
- name: creds
mountPath: /etc/secrets
readOnly: true
volumes:
- name: creds
secret:
secretName: db-cred
defaultMode: 0400 # файл читает только владелец
Приложение читает пароль из /etc/secrets/password. Параметр defaultMode: 0400 ограничивает права на файл, а readOnly: true не даёт его перезаписать.
Не коммитьте секреты
Манифест Secret с заполненным data в репозитории — это публикация пароля для всех, у кого есть доступ к Git (и навсегда, в истории). Защита:
- В Git попадают только шаблоны без значений; реальные секреты — из CI-переменных или внешнего менеджера.
- Если нужен GitOps, шифруйте файлы (Sealed Secrets, SOPS) — в репозиторий ложится шифртекст, расшифровать может только кластер.
- Включите сканер секретов в pre-commit и CI, чтобы случайный токен не дошёл до коммита.
Как это работает под капотом
API-сервер хранит секреты в etcd. С включённым EncryptionConfiguration он прогоняет значение через провайдера (AES-CBC, AES-GCM или KMS) перед записью и расшифровывает при чтении — для клиента это прозрачно. При монтировании секрета томом kubelet кладёт файлы в tmpfs (память ноды, не диск), а при обновлении секрета периодически обновляет содержимое смонтированных файлов. Поэтому файловое монтирование умеет «подхватывать» новые значения, а переменные окружения — нет, они фиксируются на момент старта контейнера.
Частые ошибки
- «base64 = безопасно». Это кодировка; доступ к секрету решает только RBAC и шифрование etcd.
- Секрет в Git с реальным значением. Даже удалённый, он остаётся в истории — считайте его скомпрометированным и ротируйте.
- Секреты в env и в логах. Приложение или фреймворк часто печатает окружение при старте — пароль уходит в логи.
- etcd без шифрования at-rest. Бэкап или украденный диск control-plane = все секреты открытым текстом.
- Слишком широкий доступ к секретам. Роли вроде edit/admin включают чтение секретов; выдавайте get secrets точечно.
Итоги
- Объект Secret кодирует данные в base64, но не шифрует — это не защита, а лишь отделение от манифеста.
- Контроль доступа к секретам обеспечивает RBAC; ограничивайте get secrets.
- Включайте шифрование etcd at-rest (EncryptionConfiguration или KMS managed-кластера), иначе дамп etcd раскроет всё.
- External secrets и шифрованный GitOps (Sealed Secrets/SOPS) убирают «настоящие» значения из кластера и репозитория.
- Монтируйте секреты файлами с ограниченными правами, а не через env, и никогда не коммитьте значения.