← Все статьи

Перевод Vault с Shamir на transit auto-unseal и четыре ловушки по дороге

Каждый рестарт пода Vault заканчивался запечатанным хранилищем и замороженной синхронизацией секретов. Как мы добавили transit-провайдер, мигрировали raft-кластер из трёх нод и на чём споткнулись.

В Optimacros наш Vault в Kubernetes работал как HA-кластер на raft из трёх нод — на Shamir и без auto-unseal. Это значило, что любой рестарт пода возвращал его запечатанным, и нужно было руками вводить три доли ключа. За две недели это случилось пять раз без присмотра и ещё четыре раза во время патчинга нод. Каждый seal замораживал Vault Secrets Operator: до 53 из 55 синхронизируемых секретов висели днями, однажды — 32 часа незамеченными.

Это блокировало и другую работу. Патчинг нод каждый раз стоил трёх-четырёх ручных unseal’ов, о перезагрузках без присмотра не могло быть и речи, а один под Vault нельзя было даже снять со старой ревизии без человека с долями ключа наготове. Бага не было: в инфраструктуре просто не существовало KMS или transit-провайдера, о который можно распечатываться.

Устройство

Небольшая выделенная VM теперь держит второй Vault в Docker, на raft, в роли transit-провайдера. Секретов приложений в нём нет — только transit-ключ, который разворачивает мастер-ключ кластера.

  • TLS на самоподписанном CA, который смонтирован в поды Vault и закреплён через tls_ca_cert. Никакого tls_skip_verify: путь распечатки не должен быть доступен для MITM.
  • Аутентификация — периодический orphan-токен, чья политика разрешает ровно два пути: encrypt и decrypt на одном transit-ключе. В поды он попадает как Kubernetes Secret, созданный вне Git. Vault продлевает его сам.
  • Transit-VM распечатывает себя сама при загрузке из локального файла с ключами — иначе перезагрузка одного этого хоста стала бы аварией кластера.

Изменения на стороне кластера заехали через GitOps, затем — разовая миграция seal. Перед миграцией мы сняли raft-снапшот и сохранили исходные доли Shamir.

Ловушка 1: -config дважды

Entrypoint официального образа сам дописывает -config=/vault/config, если первый аргумент — server. Я добавил свой -config=<file>, Vault загрузил конфиг дважды и упал с bind: address already in use на заведомо свободном порту. Лечится просто command: server.

Ловушка 2: переменная токена

Токен передаётся стандартной VAULT_TOKEN. Существует семейство переменных VAULT_TRANSIT_SEAL_* для имени ключа и пути монтирования, поэтому VAULT_TRANSIT_SEAL_TOKEN выглядит правдоподобно. Но Vault читает не её.

Ловушка 3: app-of-apps спрятал изменение

Argo CD показывал приложение Vault как Synced на новой ревизии, а в живом конфиге не было секции seal. Сам объект Application рендерится родительским приложением, а родитель всё ещё стоял на старой ревизии. Сначала рефрешим родителя, потом ребёнка.

Ловушка 4: -migrate на каждом пире

Это была главная. После рестарта подов с новой секцией seal Vault пишет в лог, что вошёл в режим миграции seal и не будет распечатываться автоматически, даже если autoseal настроен. Миграция завершается, только когда каждый пир распечатан с -migrate своими долями Shamir. Одна смигрированная нода оставляет raft без кворума, а кластер — без лидера. Доли нужно скормить всем трём, каждому — пороговое число раз.

Приёмочный тест

Весь смысл был в одной проверке: удалить под Vault и не давать никакой команды unseal. Новый под написал unsealed with stored key и сам вошёл в кластер. Это был первый рестарт без присмотра, которому не понадобился человек.

Чем платим

  • Transit-VM теперь жёсткая зависимость для старта Vault. Работающие поды продолжают обслуживать, если она лежит, но любой перезапущенный под останется запечатанным. Её самораспечатка, снапшоты и мониторинг важны так же, как у самого Vault.
  • Синхронизация секретов во время миграции упала в ноль и сама не восстановилась. Разбудило её изменение refreshAfter у синхронизируемых объектов.
  • Доли Shamir храните. Они остаются действительными для миграции обратно.

Что взять с собой

  • Shamir без auto-unseal превращает каждый рестарт в ручной инцидент. Если вы патчите ноды — нужен auto-unseal.
  • Transit-провайдеру нужен минимум прав: один ключ, два пути, закреплённый TLS.
  • Миграция seal — общекластерная. Запустите -migrate на каждом пире, иначе лидер не выберется.
  • В app-of-apps Synced у ребёнка ничего не значит, пока родитель на старой ревизии.
  • Принимайте изменение только после того, как под, перезапущенный без присмотра, распечатался сам.