Sealed Secrets: полный рабочий процесс

Published: 2026-02-11

SealedSecrets позволяет коммитить зашифрованные секреты в Git. Расшифровать их может только контроллер в кластере. Этот пост охватывает полный рабочий процесс: начальную установку, офлайн-запечатывание, резервное копирование сертификата, что происходит при миграции кластеров, и операционные грабли, на которые наступают команды после первоначальной настройки.


Как это работает

Контроллер генерирует RSA-пару ключей при первом запуске. Публичный ключ доступен любому, кто может обратиться к кластеру (или у кого есть копия сертификата). Приватный ключ никогда не покидает кластер.

Вы шифруете манифест Kubernetes Secret публичным ключом → получаете манифест SealedSecret → коммитите его → контроллер расшифровывает и создаёт настоящий Secret.

Шифрование по умолчанию привязано к namespace и имени. SealedSecret, созданный для my-secret в namespace app, не расшифруется при переименовании секрета или перемещении в другой namespace. Это предотвращает replay-атаки — кража SealedSecret из одного кластера и деплой в другой не сработают без приватного ключа.

Режимы scope

Три режима через флаг --scope:

Scope Привязка
strict (по умолчанию) namespace + имя
namespace-wide только namespace
cluster-wide без привязки

cluster-wide полезен для общих секретов (учётные данные container registry), которые должны работать в нескольких namespace без повторного запечатывания. Используйте с осторожностью — cluster-wide SealedSecret может быть расшифрован любым namespace.


Установка

Контроллер устанавливается через Helm как часть bootstrap-плейбука, до запуска FluxCD:

bashhelm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
helm upgrade --install sealed-secrets sealed-secrets/sealed-secrets \
  --namespace kube-system \
  --set fullnameOverride=sealed-secrets-controller \
  --wait --timeout 120s

fullnameOverride=sealed-secrets-controller важен: kubeseal по умолчанию ищет контроллер с именем sealed-secrets-controller. Если ваш релиз называется иначе (например, Helm-релиз sealed-secrets приведёт к sealed-secrets-sealed-secrets), вы получите непонятные ошибки controller not found при запуске kubeseal без явного --controller-name.

После установки убедитесь, что контроллер запущен и сгенерировал ключ:

bashkubectl get pods -n kube-system | grep sealed-secrets
kubectl get secret -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key

Селектор по лейблу показывает активный ключ контроллера. Если вывод пуст — контроллер ещё не сгенерировал ключ. Подождите несколько секунд и повторите.


Получение и сохранение публичного сертификата

После запуска контроллера получите публичный сертификат и сохраните его локально:

bashkubeseal --fetch-cert \
  --controller-name=sealed-secrets-controller \
  --controller-namespace=kube-system \
  > ~/.kube/sealed-secrets-infra.pem

Храните его в менеджере паролей или защищённом S3-бакете. Он понадобится для офлайн-запечатывания на машинах без kubeconfig-доступа к кластеру.

Сертификат не является конфиденциальным — это публичный ключ. Его безопасно коммитить в репо в директорию .kubeconfigs/ рядом с SealedSecret, для которых он используется. Это упрощает запечатывание в CI без специальной обработки.

Ротация сертификата

По умолчанию контроллер генерирует новый ключ каждые 30 дней, но сохраняет старые ключи активными для расшифровки. Секреты, запечатанные старыми ключами, продолжают работать — контроллер перебирает каждый ключ до успешного результата.

Принудительная ротация:

bashkubectl annotate secret \
  -n kube-system \
  -l sealedsecrets.bitnami.com/sealed-secrets-key \
  sealedsecrets.bitnami.com/rotate-key=force

После ротации обновите локальный файл .pem повторным получением сертификата. Старые SealedSecret продолжают расшифровываться; новые секреты будут запечатаны новым ключом.


Онлайн-запечатывание (простейший вариант)

При наличии доступа к кластеру:

bash# Создать манифест обычного секрета
kubectl create secret generic my-secret \
  --namespace=app \
  --from-literal=api-key=supersecret \
  --dry-run=client -o yaml \
| kubeseal \
  --controller-name=sealed-secrets-controller \
  --controller-namespace=kube-system \
  --format yaml \
  > my-sealed-secret.yaml

kubeseal автоматически запрашивает сертификат у кластера. Предварительно сохранённый .pem не нужен.


Офлайн-запечатывание (из CI или air-gapped)

Используйте сохранённый сертификат:

bashkubectl create secret generic my-secret \
  --namespace=app \
  --from-literal=api-key=supersecret \
  --dry-run=client -o yaml \
| kubeseal \
  --cert ~/.kube/sealed-secrets-infra.pem \
  --format yaml \
  > my-sealed-secret.yaml

Именно так работает наш GitLab CI при запечатывании секретов в пайплайнах деплоя — сертификат хранится как файловая переменная GitLab CI, kubeconfig не нужен.

Запечатывание для нескольких кластеров из CI

При наличии нескольких кластеров (infra, dev, test), каждый со своим контроллером Sealed Secrets, нужны отдельные .pem-файлы:

bash# Запечатать для infra
kubeseal --cert .kubeconfigs/sealed-secrets-infra.pem ...

# Запечатать для dev
kubeseal --cert .kubeconfigs/sealed-secrets-dev.pem ...

Результирующие манифесты SealedSecret разные, даже если plaintext одинаковый — каждый зашифрован другим публичным ключом.


Запечатывание kubeconfig для hub-and-spoke

Kubeconfig — это просто Secret с ключом файла. Запечатываем так же:

bashkubectl create secret generic dev-kubeconfig \
  --namespace=flux-system \
  --from-file=value=.kubeconfigs/dev.yaml \
  --dry-run=client -o yaml \
| kubeseal \
  --cert ~/.kube/sealed-secrets-infra.pem \
  --format yaml \
  > fluxcd/dev-kubeconfig-sealed.yaml

Контроллер на infra расшифровывает его, Flux использует для обращения к dev. Обратите внимание: namespace — flux-system, что должно совпадать с namespace, где работает Kustomization, ссылающаяся на этот секрет.


Запечатывание учётных данных container registry

Image pull secrets в JSON-формате:

bashkubectl create secret docker-registry registry-creds \
  --namespace=app \
  --docker-server=registry.example.com \
  --docker-username=myuser \
  --docker-password=mypass \
  --dry-run=client -o yaml \
| kubeseal \
  --cert ~/.kube/sealed-secrets-infra.pem \
  --format yaml \
  > registry-creds-sealed.yaml

Для cluster-wide registry credentials (чтобы каждый под мог делать pull без указания imagePullSecrets в каждом namespace):

bashkubectl create secret docker-registry registry-creds \
  --namespace=kube-system \
  --docker-server=registry.example.com \
  --docker-username=myuser \
  --docker-password=mypass \
  --dry-run=client -o yaml \
| kubeseal \
  --cert ~/.kube/sealed-secrets-infra.pem \
  --scope cluster-wide \
  --format yaml \
  > registry-creds-sealed.yaml

Нельзя частично обновить SealedSecret

Это главная ловушка: операции «обновить поле X» не существует. Если нужно изменить любое значение — даже одно поле — нужно перезапечатать целый Secret с новыми значениями и заменить весь манифест SealedSecret.

Рабочий процесс:

bash# Получить текущие значения (если они хранятся отдельно)
kubectl get secret my-secret -n app -o yaml | kubectl neat > /tmp/my-secret.yaml
# ИЛИ восстановить с нуля с новыми значениями

# Отредактировать значения в /tmp/my-secret.yaml

# Перезапечатать всё целиком
kubeseal --cert ~/.kube/sealed-secrets-infra.pem --format yaml \
  < /tmp/my-secret.yaml > my-sealed-secret.yaml

git add my-sealed-secret.yaml && git commit -m "rotate my-secret" && git push

Поэтому plaintext-значения нужно хранить отдельно (Vault, менеджер паролей) — иначе при обновлении без доступа к кластеру окажется, что восстанавливать значения не из чего.


Миграция кластера: перезапечатывание всего

При пересборке кластера с нуля контроллер генерирует новую пару ключей. Все существующие SealedSecret бесполезны — новый контроллер не может их расшифровать.

Варианты:

  1. Восстановить пару ключей — экспортировать ключ из старого кластера до его уничтожения, импортировать в новый
  2. Перезапечатать все секреты — трудоёмко, но чисто, если plaintext хранится в Vault

Вариант 1: Восстановление пары ключей

Экспортируйте старый ключ до уничтожения кластера:

bashkubectl get secret \
  -n kube-system \
  -l sealedsecrets.bitnami.com/sealed-secrets-key=active \
  -o yaml > sealed-secrets-key-backup.yaml

Храните в менеджере паролей. Это самый чувствительный файл в вашей инфраструктуре — у кого есть этот файл, тот может расшифровать все SealedSecret в репо.

Импортируйте ключ в новый кластер до того, как Flux начнёт реконсиляцию:

bashkubectl apply -f sealed-secrets-key-backup.yaml
kubectl rollout restart deployment sealed-secrets-controller -n kube-system

Контроллер подхватывает восстановленный ключ и может расшифровать все существующие SealedSecret.

Вариант 2: Перезапечатать все секреты

Если все plaintext-значения хранятся в Vault (а так и должно быть), перезапечатывание несложно:

  1. Получить публичный сертификат нового кластера
  2. Пройтись по всем манифестам SealedSecret
  3. Восстановить plaintext из Vault
  4. Запечатать новым сертификатом
  5. Закоммитить и запушить

Больше работы, но результат чище — нет зависимости от сохранения старого ключевого материала.


Проверка статуса расшифровки

После деплоя SealedSecret:

bash# Проверить, синхронизировался ли SealedSecret в настоящий Secret
kubectl get sealedsecret my-secret -n app
kubectl get secret my-secret -n app

# Если Secret не существует, проверить events
kubectl describe sealedsecret my-secret -n app

Типичные сообщения об ошибках:

Ошибка Причина
no key could decrypt secret Неправильный кластер — запечатан для другого контроллера
illegal base64 data Сертификат или секрет был повреждён при копировании
namespace/name mismatch Секрет был перемещён или переименован после запечатывания

no key could decrypt secret — частая ошибка при запечатывании для infra, но случайном коммите в путь dev.