Перевод 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 у ребёнка ничего не значит, пока родитель на старой ревизии.
- Принимайте изменение только после того, как под, перезапущенный без присмотра, распечатался сам.