Автоматическое распечатывание (auto-unseal) через статический ключ — кластер восстанавливается после перезапусков без ручного вмешательства.
Компания Kubermatic только что выпустила SecureGuard — платформу управления секретами с открытым исходным кодом, построенную на OpenBao и External Secrets Operator. Я провёл последние два дня, разворачивая именно этот стек на собственном кластере под управлением FluxCD. Ниже — практическое руководство по всей установке, включая каждую ловушку, на которую я наткнулся.
Проблема
Kubernetes Secrets кодируются в base64, а не шифруются. Любой, у кого есть доступ к кластеру, может их декодировать. Если вы работаете с GitOps, у вас есть два пути: шифровать секреты до того, как они попадут в Git, или вообще не хранить их в Git.
Sealed Secrets следует первому подходу — шифрует секреты для конкретного кластера и сохраняет шифротекст в репозитории. Работает нормально, но управление десятками секретов означает десятки зашифрованных файлов в Git, а их ротация и обновление — то ещё удовольствие.
OpenBao + External Secrets реализует второй подход: секреты хранятся в центральном хранилище с веб-интерфейсом, полностью вне Git. External Secrets Operator следит за OpenBao и синхронизирует секреты в Kubernetes во время выполнения. Добавляете или обновляете секрет в одном месте — ESO берёт на себя всё остальное.
Почему OpenBao?
OpenBao — это форк HashiCorp Vault с открытым исходным кодом. Когда в 2023 году HashiCorp перевёл Vault на лицензию Business Source License (BSL), сообщество создало форк под эгидой Linux Foundation. Если вы раньше работали с Vault, OpenBao покажется идентичным — тот же API, те же концепции, тот же рабочий процесс. Разница — в лицензии: MPL 2.0, без каких-либо ограничений.
На этом кластере я прошёл полный путь эволюции: начал с HashiCorp Vault, перешёл на Sealed Secrets после смены лицензии и в итоге остановился на OpenBao + External Secrets. Каждый шаг чему-то учил, но этот стек победил, потому что повседневное управление с ним просто проще.
Архитектура
Схема работы прямолинейна:
-
OpenBao хранит все секреты — API-токены, пароли от баз данных и всё остальное.
-
External Secrets Operator читает данные из OpenBao и создаёт нативные Kubernetes Secrets.
-
Ваши рабочие нагрузки используют стандартные K8s Secrets — им не важно и незачем знать, откуда те взялись.
Благодаря этому манифесты приложений остаются чистыми. Никаких файлов с зашифрованными секретами, никаких значений, зашифрованных SOPS, никаких ссылок на секреты в Git-репозитории.
Установка OpenBao на Kubernetes
Это руководство предполагает, что OpenBao и External Secrets Operator уже развёрнуты в кластере. Установку через Helm я не рассматриваю — она стандартна и хорошо задокументирована. Все манифесты можно найти в моём репозитории: github.com/dmuiX/fluxcd.k8sdev.cloud.
То, что задокументировано плохо, — это всё, что начинается после установки.
OpenBao запускается как StatefulSet из трёх реплик, использующий Raft для консенсуса. Helm-чарт берёт на себя большую часть работы, но в конфигурации есть острые углы.
Автоматическое распечатывание через статический ключ
По умолчанию Vault/OpenBao использует для распечатывания схему разделения секрета Шамира — при каждом перезапуске пода нужно вручную вводить части ключа. Для промышленного хранилища с командой дежурных это приемлемо, но в домашней лаборатории или небольшой команде без автоматического распечатывания не обойтись.
OpenBao поддерживает конфигурацию seal "static", использующую заранее сгенерированный ключ для автоматического распечатывания. Кластер восстанавливается после перезапусков без какого-либо ручного вмешательства.
Шаг 1: сгенерировать статический ключ и сохранить его как Kubernetes Secret
static_key=$(openssl rand -base64 32 | tee /dev/tty)
kubectl create secret generic openbao-unseal-key \
-n openbao \
--from-literal=unseal-key="${static_key}" \
--dry-run=client -o yaml | kubectl apply -f -
Этот ключ должен существовать до инициализации OpenBao. Обязательно сделайте резервную копию — без него кластер не сможет распечататься.
Шаг 2: инициализировать кластер
kubectl exec -n openbao openbao-0 -- bao operator init \
-recovery-shares=1 \
-recovery-threshold=1 | tee openbao-keys.txt
При активном seal "static" команда создаёт ключ восстановления (recovery key), а не ключи распечатывания (unseal keys). Ключ восстановления предназначен только для экстренных операций — при штатной работе распечатывание происходит автоматически через статический ключ.
После инициализации лидер распечатывается самостоятельно. Остальные узлы автоматически присоединяются через retry_join и также распечатываются без ручного вмешательства.
Шаг 3: убедиться, что все узлы работают
kubectl exec -n openbao openbao-0 -- bao status
kubectl exec -n openbao openbao-1 -- bao status
kubectl exec -n openbao openbao-2 -- bao status
Все три должны показывать Sealed: false.
bao status: «Все три узла распечатаны — автоматическое распечатывание через статический ключ, ручное вмешательство не требуется.»
Шаг 4: проверить raft-пиров (требуется root-токен)
BAO_TOKEN=<root-token-here>
kubectl exec -n openbao openbao-0 -- sh -c "BAO_TOKEN=$BAO_TOKEN bao operator raft list-peers"
raft list-peers: «Три здоровых raft-пира — выбор лидера и автоматическое присоединение выполнены автоматически.»
Понимание трёх ключей
Процесс инициализации создаёт три разных ключа, которые выполняют совершенно разные функции. Поначалу это меня запутало, поэтому поясню подробнее.
Статический ключ (Static Key) — ключ автоматического распечатывания, сгенерированный на шаге 1 с помощью openssl rand. Именно он шифрует мастер-ключ OpenBao. OpenBao считывает его при каждом запуске через блок конфигурации seal "static" — именно это обеспечивает автоматическое распечатывание. Без него кластер не запустится. Сохраните его в менеджере паролей.
Ключ восстановления (Recovery Key) — генерируется OpenBao во время bao operator init. Используется только в экстренных ситуациях. Если вы потеряете root-токен, ключ восстановления позволит сгенерировать новый. Он не распечатывает хранилище. Тоже сделайте резервную копию, но понадобится он редко.
Root-токен (Root Token) — также генерируется при инициализации. Это ваш административный доступ к API и веб-интерфейсу OpenBao. Используйте его для первоначальной настройки: включения методов аутентификации, создания политик. В промышленной среде после настройки его следует отозвать и перейти на нормальные методы аутентификации, но в домашней лаборатории оставить его — допустимо.
Главное, что нужно понять: при seal "static" ключи Shamir не существуют. Статический ключ полностью их заменяет. В этом весь смысл — никакого ручного распечатывания, никогда.
Подключение External Secrets
После запуска OpenBao нужно разрешить External Secrets Operator аутентифицироваться в нём. Самый чистый способ — Kubernetes auth: ESO использует свой ServiceAccount для аутентификации, никаких статических токенов.
В OpenBao:
-
Включите KV-хранилище секретов (назовите его
kv). -
Создайте ACL-политику, предоставляющую доступ на чтение:
path "kv/data/*" {
capabilities = ["read", "list"]
}
path "kv/metadata/*" {
capabilities = ["list"]
}
-
Включите метод аутентификации Kubernetes и создайте роль, связывающую ServiceAccount ESO с этой политикой.
В кластере:
Разверните ClusterSecretStore, указывающий на OpenBao и использующий Kubernetes auth. После этого любой создаваемый ресурс ExternalSecret будет автоматически получать значения из OpenBao и создавать нативный Kubernetes Secret.
Практический пример: API-токен Cloudflare
Теория — хорошо, но вот как это выглядит на практике. И cert-manager (для TLS-сертификатов через DNS-01 challenge), и External DNS (для автоматического управления DNS-записями) нуждаются в API-токене Cloudflare. Вместо того чтобы создавать этот секрет вручную в каждом пространстве имён, достаточно один раз определить его в OpenBao и позволить ESO распространить его:
apiVersion: external-secrets.io/v1
kind: ClusterExternalSecret
metadata:
name: cloudflare-token
spec:
refreshTime: 24h # как часто ESO проверяет изменения в OpenBao
externalSecretName: "cloudflare-token"
externalSecretSpec:
target:
name: cloudflare-token # имя создаваемого K8s Secret
data:
- secretKey: token # ключ внутри K8s Secret
remoteRef:
key: cloudflare-token # путь в KV-хранилище OpenBao
version: v1
property: token # поле внутри секрета OpenBao
decodingStrategy: None
secretStoreRef:
kind: ClusterSecretStore
name: openbao-backend # ссылается на ваш ClusterSecretStore
namespaceSelectors: # в каких пространствах имён создавать секрет
- matchExpressions:
- key: kubernetes.io/metadata.name
operator: In
values:
- cert-manager
- external-dns
Что здесь происходит:
-
ClusterExternalSecret (а не просто ExternalSecret) — кластерный вариант, автоматически создающий ExternalSecret в каждом совпадающем пространстве имён.
-
remoteRef указывает на секрет в OpenBao:
key— путь в KV-хранилище,property— конкретное поле внутри этого секрета. -
target.name — имя итогового Kubernetes Secret, на которое ссылаются cert-manager и External DNS в своих конфигурациях.
-
namespaceSelectors определяет, какие пространства имён получат секрет. Токен нужен только cert-manager и external-dns — только они его и получают. Принцип минимальных привилегий по умолчанию.
-
refreshTime: 24h означает, что ESO проверяет OpenBao каждые 24 часа. Ротируйте токен через веб-интерфейс OpenBao — в течение суток он обновится везде, без коммитов в Git и без повторного развёртывания.
Один секрет в OpenBao, автоматически синхронизированный именно в те пространства имён, которым он нужен. Никаких секретов в Git.
Именно здесь подход полностью себя оправдывает: один секрет в OpenBao, автоматически доставляемый ровно туда, где он нужен. Появился новый сервис, которому нужен токен? Добавьте его пространство имён в селектор. Нужно ротировать токен? Обновите его в одном месте.
Подводные камни
Установка OpenBao на Kubernetes таит в себе больше острых углов, чем можно ожидать. Вот всё, что у меня ломалось, и почему.
ha.config против ha.raft.config
Когда в значениях Helm установлено ha.raft.enabled: true, чарт использует ha.raft.config, а не ha.config. Положите конфигурацию не в тот блок — и Helm-чарт молча отрендерит ConfigMap по умолчанию. Никакой ошибки, никакого предупреждения. Всегда проверяйте, что отрендеренный ConfigMap соответствует ожидаемому.
Не передавайте ключ распечатывания через переменную окружения
extraSecretEnvironmentVars выглядит как подходящее место для передачи статического ключа. Это не так. OpenBao не запустится. Ключ необходимо подключать через монтирование тома (volume mount).
Несоответствие HTTP и HTTPS в retry_join
Если в конфигурации listener установлено tls_disable = 1, адреса в retry_join должны использовать http://, а не https://. Ошибитесь здесь — и получите: «http: server gave HTTP response to HTTPS client» — сбивающее с толку сообщение, когда вы разбираетесь, почему raft-пиры не присоединяются.
Запечатанные узлы не слушают порт 8201
Порт Raft-кластера (8201) открывается только после распечатывания узла. На запечатанном узле порт 8200 (API) открыт, а порт 8201 (кластер) — закрыт. Это вызывает ошибку «failed to make requestVote RPC: connection refused» между узлами и при первоначальной настройке может сильно сбить с толку.
Raft требует минимум 3 реплики
2 реплики означают отсутствие отказоустойчивости: если один узел упадёт, кворум будет потерян и весь кластер заблокируется. Всегда используйте 3 (или 5). Helm-чарт по умолчанию задаёт 3 — не меняйте на 2.
UI-сервис отправляет запросы на распечатывание в случайном порядке
Если вам когда-нибудь понадобится распечатать вручную (например, при первоначальной отладке), не используйте веб-интерфейс — сервис отправляет запросы случайным подам. Вместо этого используйте kubectl exec, явно указывая нужный под.
Тонкости jq и base64
Статический ключ в Kubernetes кодируется в base64 дважды (один раз — openssl, второй — сам K8s). А jq требует флага -r для вывода без кавычек и экранирования имён с дефисами:
# Правильный способ:
kubectl get secret openbao-unseal-key -n openbao -o json \
| jq -r '.data."unseal-key"' | base64 -d
Что я вынес из этого опыта
Это уже третья итерация управления секретами на данном кластере. HashiCorp Vault → Sealed Secrets → OpenBao + External Secrets. Каждый подход работает, но они оптимизированы под разные задачи.
Хотите минимальную сложность установки — берите Sealed Secrets. Зашифровал, закоммитил, готово.
Хотите центральный интерфейс, удобную ротацию и полное отсутствие секретов в Git — берите OpenBao + External Secrets. Больше работы на старте, но в долгосрочной перспективе эксплуатация проще.
То, что Kubermatic теперь упаковывает именно этот стек в готовый продукт (SecureGuard), говорит о том, что сообщество сходится на этом паттерне. Компоненты зрелые — документация просто не поспевает за ними.
Полная установка со всеми манифестами — на GitHub: 🔗 github.com/dmuiX/fluxcd.k8sdev.cloud
Я фриланс-инженер по инфраструктуре. Большинство моих проектов — Docker Compose: простые, стабильные установки, которые клиенты могут обслуживать самостоятельно. Когда проект действительно требует высокой доступности и масштабируемости, я подключаю Kubernetes. Правильный инструмент под задачу, а не самый сложный из доступных. Не стесняйтесь писать.