HashiCorp Vault: bootstrap, unseal и Kubernetes auth
Published: 2026-02-10
Настройка Vault впервые в Kubernetes-кластере — это не просто helm install. Есть церемония инициализации, стратегия распечатывания (unseal) и настройка Kubernetes auth, без которых External Secrets Operator ничего не получит.
Этот пост охватывает полный процесс начальной загрузки: деплой HA Raft, церемонию unseal, настройку Kubernetes auth, политики, привязки ServiceAccount и операционные паттерны для day-2 эксплуатации.
Почему Vault, а не Kubernetes Secrets + SealedSecrets
Kubernetes Secrets закодированы в base64 и по умолчанию не зашифрованы при хранении (если вы не настроили шифрование etcd). SealedSecrets решают задачу «зашифровать перед коммитом в Git», но не решают:
- Ротацию секретов — при каждой смене пароля нужно перезапечатать и развернуть заново
- Audit trail — кто какой секрет читал и когда
- Динамические секреты — учётные данные БД, действительные 1 час, автоматически отзываемые после использования
- Детальное управление доступом — какой под может читать какой путь секрета
Vault решает всё это. Эксплуатация Vault требует заметных усилий (unseal, управление ключами), но для production-сред с несколькими командами и требованиями compliance это оправданно.
Деплой
Vault работает как StatefulSet через официальный Helm-чарт HashiCorp. HA-режим с интегрированным Raft-хранилищем — без зависимости от внешнего Consul:
yamlserver:
ha:
enabled: true
replicas: 3
raft:
enabled: true
config: |
ui = true
listener "tcp" {
tls_disable = 1
address = "[::]:8200"
cluster_address = "[::]:8201"
}
storage "raft" {
path = "/vault/data"
retry_join {
leader_api_addr = "http://vault-0.vault-internal:8200"
}
retry_join {
leader_api_addr = "http://vault-1.vault-internal:8200"
}
retry_join {
leader_api_addr = "http://vault-2.vault-internal:8200"
}
}
service_registration "kubernetes" {}
dataStorage:
storageClass: local-path
size: 10Gi
tls_disable = 1 в listener — TLS-терминация происходит на ingress (Traefik/APISIX), не в самом Vault. В production-деплое с прямым взаимодействием между подами TLS нужно включить здесь.
service_registration "kubernetes" {} позволяет Vault регистрироваться как Kubernetes-сервис. При смене активного узла Vault (выборы лидера) Kubernetes корректно маршрутизирует трафик.
retry_join — все три Raft-узла пытаются объединиться при запуске. Только один станет лидером, остальные присоединятся как follower. Если vault-0 перезапустится, он автоматически переприсоединится.
Размер хранилища
10Gi достаточно для хранения секретов, но при включённом audit logging объём быстро растёт. Включите audit в отдельный том:
yaml# В конфиге Vault
audit_backend "file" {
file_path = "/vault/audit/audit.log"
}
Инициализация
После запуска подов инициализировать нужно только один узел. Остальные поды автоматически присоединятся к Raft-кластеру:
bashkubectl exec -n vault vault-0 -- vault operator init \
-key-shares=5 \
-key-threshold=3 \
-format=json > vault-init.json
Это создаёт 5 ключей unseal и root-токен. Порог 3 означает: любые 3 из 5 ключей могут распечатать Vault.
Безопасность ключей: держите root-токен офлайн — он нужен только для начальной настройки и экстренного доступа. Ключи unseal нужно распределить: в серьёзном деплое каждый ключ отдаётся отдельному человеку/команде, чтобы распечатывание требовало консенсуса.
Варианты хранения ключей
- Менеджер паролей с 5 разными держателями — наиболее безопасно для многокомандных сред
- SealedSecret в infra-репо — удобно для команд из одного человека; ключ Sealed Secrets infra-кластера защищает ключи unseal Vault (рекурсивная защита)
- YC KMS / AWS KMS auto-unseal — полностью автоматизировано, убирает человеческую церемонию при каждом перезапуске
Для homelab и стартапов с одной командой хранение в SealedSecret прагматично. Для регулируемых сред (SOC2, PCI, аудиты GDPR) нужна распределённая церемония ключей.
Unseal
Каждый под Vault нужно распечатывать отдельно после каждого перезапуска. Vault запускается в запечатанном состоянии и отказывает в выполнении всех операций до распечатывания:
bashfor pod in vault-0 vault-1 vault-2; do
for key in $(cat vault-init.json | jq -r '.unseal_keys_b64[:3][]'); do
kubectl exec -n vault $pod -- vault operator unseal $key
done
done
Проверка статуса:
bashkubectl exec -n vault vault-0 -- vault status
Ищите Sealed: false и HA Enabled: true. Активный узел показывает HA Mode: active, остальные — HA Mode: standby.
Auto-unseal с Yandex Cloud KMS
Для автоматического распечатывания (устраняет ручную церемонию после каждого перезапуска):
hclseal "transit" {
address = "https://kms.yandex"
key_name = "vault-unseal-key"
# Использует метаданные виртуальной машины для аутентификации в YC
}
С auto-unseal поды Vault распечатываются автоматически при перезапуске. Компромисс: если KMS-ключ удалить или ротировать неправильно, все данные Vault станут навсегда недоступны. Держите migration seal для аварийного восстановления.
Auto-unseal с AWS KMS
hclseal "awskms" {
region = "us-east-1"
kms_key_id = "alias/vault-unseal"
}
Настройка Kubernetes auth
После распечатывания войдите с root-токеном и настройте Kubernetes auth, чтобы External Secrets Operator мог аутентифицироваться:
bashexport VAULT_ADDR="http://localhost:8200" # port-forward или ingress
export VAULT_TOKEN=$(cat vault-init.json | jq -r '.root_token')
vault auth enable kubernetes
vault write auth/kubernetes/config \
kubernetes_host="https://$KUBE_HOST" \
token_reviewer_jwt="$(kubectl get secret -n vault vault-sa-token -o jsonpath='{.data.token}' | base64 -d)" \
kubernetes_ca_cert="$(kubectl config view --raw -o jsonpath='{.clusters[0].cluster.certificate-authority-data}' | base64 -d)"
kubernetes_host — URL API-сервера. Внутри кластера: https://kubernetes.default.svc.
token_reviewer_jwt — токен ServiceAccount, который Vault использует для вызова Kubernetes TokenReview API при проверке входящих JWT-токенов от подов. Создайте выделенный ServiceAccount:
bashkubectl create serviceaccount vault-auth -n vault
kubectl create clusterrolebinding vault-auth-binding \
--clusterrole=system:auth-delegator \
--serviceaccount=vault:vault-auth
ClusterRole system:auth-delegator даёт ServiceAccount право вызывать TokenReview — именно это нужно Vault.
Проверка настройки Kubernetes auth
bashvault auth list
# Должен показать kubernetes/ в списке
vault read auth/kubernetes/config
KV-движок и политики
Включите KV v2 (поддерживает версионирование, мягкое удаление, метаданные):
bashvault secrets enable -path=secret kv-v2
Создайте политику для окружения dev:
hcl# dev-read.hcl
path "secret/data/dev/*" {
capabilities = ["read"]
}
path "secret/metadata/dev/*" {
capabilities = ["read", "list"]
}
bashvault policy write dev-read dev-read.hcl
Паттерн secret/data/dev/* разрешает чтение любого секрета под dev/. Для изоляции по сервисам используйте более конкретные пути:
hcl# eso-postgres.hcl — только секреты postgres для команды payments
path "secret/data/dev/postgres" {
capabilities = ["read"]
}
Политика для нескольких namespace
Если разные команды используют разные Kubernetes namespace, создайте отдельные политики и отдельные роли:
bash# Политика prod — те же пути, строже (без list)
vault policy write prod-read - <<EOF
path "secret/data/prod/*" {
capabilities = ["read"]
}
EOF
Привязка Kubernetes ServiceAccount к политике
bashvault write auth/kubernetes/role/eso-dev \
bound_service_account_names=external-secrets \
bound_service_account_namespaces=external-secrets \
policies=dev-read \
ttl=1h
ServiceAccount external-secrets в namespace external-secrets (создаётся чартом ESO) получает токен на 1 час с правами dev-read. ESO использует его для получения секретов для всех объектов ExternalSecret.
ttl=1h означает, что Vault-токен, полученный ESO, истекает через один час. ESO автоматически обновляет его до истечения. Если токен истёк (например, Vault был недоступен во время обновления), ESO пишет в лог ошибку аутентификации и прекращает синхронизацию до повторной аутентификации.
Несколько ролей для нескольких кластеров
В hub-and-spoke настройке ESO на hub-кластере может получать секреты для нескольких окружений. Создайте отдельные роли:
bashvault write auth/kubernetes/role/eso-infra \
bound_service_account_names=external-secrets \
bound_service_account_namespaces=external-secrets \
policies=infra-read \
ttl=1h
vault write auth/kubernetes/role/eso-dev \
bound_service_account_names=external-secrets \
bound_service_account_namespaces=external-secrets \
policies=dev-read \
ttl=1h
Роли различаются по policies, а не по ServiceAccount (это один и тот же ESO SA в обоих случаях).
Добавление секретов
bashvault kv put secret/dev/postgres \
username=app \
password=s3cr3t
vault kv put secret/dev/gitlab-registry \
dockerconfig='{"auths":{"registry.example.com":{"auth":"..."}}}'
Версионирование
KV v2 по умолчанию хранит до 10 версий. Откат к предыдущей версии:
bash# Просмотр версий
vault kv metadata get secret/dev/postgres
# Откат к версии 2
vault kv rollback -version=2 secret/dev/postgres
Проверка того, что ESO реально получает
Если ESO получает секрет и результат выглядит неправильным, проверьте значение напрямую:
bashvault kv get -format=json secret/dev/postgres | jq '.data.data'
Обратите внимание на двойное .data.data — в KV v2 реальные значения вложены под .data.data в JSON-ответе.
Day-2 эксплуатация
Ротация root-токена
После начальной настройки отзовите root-токен и генерируйте новый только при необходимости:
bashvault token revoke $ROOT_TOKEN
# Генерация нового root-токена при необходимости (требует кворум ключей unseal)
vault operator generate-root -init
# ... церемония с держателями ключей unseal ...
Audit logging
Включите audit log перед любым production-использованием:
bashvault audit enable file file_path=/vault/audit/audit.log
Audit log фиксирует каждый запрос и ответ (значения секретов маскируются). Обязателен для compliance и расследования инцидентов.
Мониторинг Vault с Prometheus
Официальный Helm-чарт Vault включает serviceMonitor для Prometheus:
yamlserver:
serviceAccount:
create: true
metrics:
enabled: true
serviceMonitor:
enabled: true
labels:
release: kube-prom-stack
Ключевые метрики для алертов:
vault_core_unsealed— 0 означает, что Vault запечатан, все чтения секретов завершатся ошибкойvault_core_active— количество активных (лидерских) узлов, должно быть 1vault_runtime_total_gc_pause_ns— высокие паузы GC указывают на нехватку памяти