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 — количество активных (лидерских) узлов, должно быть 1
  • vault_runtime_total_gc_pause_ns — высокие паузы GC указывают на нехватку памяти