Управление секретами

Объект 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, и никогда не коммитьте значения.
Проверьте себя
1. Что на самом деле делает Kubernetes со значениями обычного Secret по умолчанию?
AШифрует их алгоритмом AES перед записью
BКодирует в base64 — это не шифрование и легко декодируется
CХранит их в зашифрованном виде во внешнем KMS
DХеширует их необратимо
2. Зачем настраивать шифрование etcd at-rest для секретов?
AЧтобы скрыть секреты от пользователей с правом get secrets
BЧтобы дамп etcd или украденный диск control-plane не раскрыл секреты открытым текстом
CЧтобы секреты автоматически попадали в Git
DЧтобы ускорить чтение секретов из etcd
3. Почему монтирование секрета томом часто безопаснее, чем через переменные окружения?
AПеременные окружения шифруются, а файлы нет
Benv легко утекает в логи и виден в kubectl describe pod, а файл с правами 0400 — нет, плюс файлы обновляются без рестарта
CЧерез том секрет автоматически шифруется
DПеременные окружения вообще не поддерживают секреты