Публикация политик под двойным контролем с HashiCorp Vault Transit
Подписанный набор политик — единственный артефакт, которому прокси управления должен доверять. Разбираем, как мы публикуем такие наборы под двойным контролем, ротируем ключи без простоя и подтверждаем целостность на каждом запросе, используя HashiCorp Vault Transit и подписи Ed25519.
Коротко. Если один инженер может выкатить изменение политики, ослабляющее маскирование, открывающее новый эндпоинт или добавляющее модель в белый список, то прокси управления — театр. Реальное применение держится на подписанных наборах, двойном контроле при каждой публикации и ротации ключей без переразвёртывания. Криптографию берёт на себя HashiCorp Vault Transit, Ed25519 держит проверку дешёвой, а связывает всё небольшой набор правил публикации.
Прокси управления на каждом запросе решает, может ли промпт покинуть периметр, маскируются ли персональные данные, допустим ли код в ответе и какие модели вообще доступны. Эту границу задаёт набор политик: версионированный машиночитаемый артефакт, собранный из декларативных правил. Кто может изменить этот набор, тот управляет периметром.
В материале разбирается модель, по которой мы публикуем наборы политик в Raigate: как выпускаются подписи Ed25519 через HashiCorp Vault Transit, как двойной контроль обеспечивается на шаге публикации (а не в редакторе), как прокси проверяет наборы при загрузке и как мы ротируем ключи подписи без окна обслуживания.
Содержание
- Почему двойной контроль нужен на уровне политик
- Анатомия подписанного набора политик
- Vault Transit как оракул подписи
- Сквозной процесс публикации
- Проверка на стороне прокси
- Ротация ключей и шаблон переподписи
- Со стороны комплаенса: что на самом деле спрашивают аудиторы
1. Почему двойной контроль нужен на уровне политик
Большинство команд уже где-то применяет двойной контроль: продуктивные выкатки, миграции баз, изменения в IAM. Наборы политик для прокси управления ИИ заслуживают того же по простой причине: набор и есть политика.
Изменение одной строки в JSON-предикате может:
- молча отключить маскирование персональных данных для арендатора,
- разрешить новый исходящий эндпоинт, через который утекают промпты,
- добавить в белый список неутверждённую модель со слабее настроенной безопасностью,
- превратить умолчания с отказом в закрытое состояние в отказ в открытое.
Одного код-ревью мало. Ревьюер, одобривший пул-реквест, — не то же самое, что криптографический со-подписант, свидетельствующий об артефакте, который поедет в продуктив. Эти двое расходятся при ребейзе, конфликте слияния, «крошечной» правке после одобрения или при компрометации CI-раннера.
Решение — перенести границу доверия с репозитория на подписанный артефакт. Каждый работающий прокси проверяет подпись до загрузки набора. Набор загружается, только если несёт действительную подпись ключа, которому прокси уже доверяет; отсутствующая, неверная или корректная подпись неизвестного ключа оставляют его лежать на диске.
2. Анатомия подписанного набора политик
Набор — детерминированный архив. На вход идут исходники, на выходе — канонический поток байтов, который хешируется и подписывается. Детерминированность важна, потому что одна и та же логическая политика должна давать один и тот же хеш на любой машине. Иначе проверка подписи превращается из гарантии в лотерею.
bundle/
├── manifest.json # version, created_at, source commit, framework refs
├── policies/
│ ├── redaction.json # PII handling rules, fail-closed defaults
│ ├── routing.json # which models, which endpoints, per tenant
│ └── content.json # blocked categories, output constraints
├── checksums.txt # SHA-256 of every file above, sorted by path
└── signatures/
├── primary.sig # Ed25519 over checksums.txt
└── secondary.sig # Ed25519 over checksums.txt, different signer
- redaction.json: правила по ПДн, умолчания с отказом в закрытое состояние
- routing.json: модели и эндпоинты по арендаторам
- content.json: запрещённые категории, ограничения вывода
- primary.sig: Ed25519, личность руководителя по политикам
- secondary.sig: Ed25519, личность безопасности
- Нужны обе. Разные люди, разные ключи Vault.
checksums.txt — точка канонизации. Файлы перечислены по алфавиту; каждая строка — <sha256> <path>. Подписанты никогда не подписывают сам архив; они подписывают checksums.txt. Это делает подписи устойчивыми к форматам архивов и версиям инструментов и даёт проверяющему быстрый путь: захешировать файлы, пересобрать checksums.txt, сравнить с подписанным.
Поверхность двойного контроля именно такова: две подписи, сделанные двумя разными ключами, которыми владеют два разных человека.
3. Vault Transit как оракул подписи
Мы никогда не держим приватные ключи на машинах разработчиков или CI-раннерах. Vault Transit отдаёт подпись как API; ключевой материал остаётся внутри Vault, и его можно ротировать, версионировать или отзывать, не трогая клиентов.
В бэкенде Transit живут два именованных ключа:
policy-bundle-primary, у роли руководителя инженерии политикpolicy-bundle-secondary, у роли безопасности
Оба ключа — Ed25519, неэкспортируемые, с включённым версионированием.
Выпуск подписи — один вызов API к Vault: на вход идёт SHA-256 файла checksums.txt в base64, на выходе — подпись в base64 и версия ключа, которая её произвела:
vault write transit/sign/policy-bundle-primary/sha2-256 \
input="$(sha256sum checksums.txt | awk '{print $1}' | xxd -r -p | base64)" \
prehashed=true \
signature_algorithm=pkcs1v15
Вызов подписи закрыт политикой Vault, которая требует одновременно аутентифицированную человеческую личность и активный тикет согласования. Ни один сервисный аккаунт не может вызвать transit/sign/policy-bundle-primary напрямую. То же самое, с другой политикой и другой группой идентичностей, действует для вторичного ключа.
Именно это превращает двойной контроль из командной договорённости в свойство системы: один человек не может произвести обе подписи, потому что ни у одной идентичности нет прав Vault на оба эндпоинта подписи.
4. Сквозной процесс публикации
Оракул подписи — центральный элемент, но безопасной эксплуатацию делает процесс вокруг него. Путь изменения политики от черновика до продуктива:
Конкретно:
- Автор открывает изменение политики обычным пул-реквестом. CI собирает кандидатный набор и публикует его SHA-256 как статус-чек. Пул-реквест нельзя слить, пока хеш не совпадает с последним коммитом ветки.
- Ревьюер открывает кандидат в студии политик, прогоняет сухую оценку на регрессионном корпусе (переигранный исторический трафик плюс подобранный набор red-team промптов) и либо отклоняет, либо помечает набор готовым к публикации. Ревьюер — не автор.
- Основная подпись запрашивается. Система забирает кандидат, пересчитывает
checksums.txtи вызывает Vault Transit под идентичностью автора с основной политикой подписи. Подпись прикрепляется. - Вторичная подпись запрашивается под идентичностью ревьюера, вторичным ключом. Если хоть одна подпись отсутствует или недействительна, набор не покидает сборочную среду.
- Дистрибутор публикует дважды подписанный набор в хранилище артефактов по адресуемому содержимым пути:
bundles/<sha256>.tar.gz. Флот тянет оттуда. - Флот прокси проверяет обе подписи по закреплённым публичным ключам текущих версий и атомарно переключает активную политику. Ошибка загрузки оставляет прежний набор, отказ в закрытое состояние на всех уровнях.
Никто не может утвердить собственное изменение, и ревьюер не может опубликовать без основной подписи автора. Дистрибутор, в свою очередь, принимает только наборы, обе подписи которых проходят проверку по текущему набору публичных ключей. Каждый рубеж — жёсткая криптографическая проверка, а не элемент интерфейса.
5. Проверка на стороне прокси
Прокси держит в конфигурации небольшой набор публичных ключей, закреплённых по версиям:
{
"trusted_signers": [
{
"name": "policy-bundle-primary",
"key_versions": {
"v2": "MCowBQYDK2VwAyEA<...base64 ed25519 public key...>",
"v3": "MCowBQYDK2VwAyEA<...base64 ed25519 public key...>"
}
},
{
"name": "policy-bundle-secondary",
"key_versions": {
"v1": "MCowBQYDK2VwAyEA<...base64 ed25519 public key...>",
"v2": "MCowBQYDK2VwAyEA<...base64 ed25519 public key...>"
}
}
],
"require_both": true,
"min_key_versions": { "primary": 2, "secondary": 1 }
}
Процедура проверки по шагам:
- Распаковать архив во временный каталог.
- Пересобрать
checksums.txtиз распакованных файлов в отсортированном порядке. - Захешировать его SHA-256.
- Для каждого файла
.sigпрочитать встроенные имя и версию ключа, найти их вtrusted_signers, отказать при отсутствии. - Проверить подпись Ed25519 против пересчитанного хеша.
- Если установлен
require_both, должны пройти обе. Иначе отклонить.
Ed25519 выбран за стоимость проверки. Проверка одной подписи занимает меньше миллисекунды на обычном железе — достаточно дёшево, чтобы выполнять её при каждой перезагрузке набора, в health-check прокси и в стартовой пробе, которая не помечает под готовым, пока активный набор не перепроверен.
Прокси никогда не доверяет ключу, о котором ему не сказали. Новые версии ключей требуют обновления конфигурации, а оно само проходит тот же путь ревью и выкатки, что и любое продуктивное изменение.
6. Ротация ключей и шаблон переподписи
Ключи Ed25519 не слабеют со временем, но операционный принцип остаётся: ключ, подписывавший достаточно долго, следует ротировать. Риск не в алгоритме. Он в хранении, текучке персонала и медленном накоплении идентичностей, которые за несколько лет могли коснуться эндпоинта подписи.
Больно не сгенерировать новый ключ. Vault Transit делает это одним вызовом:
vault write -f transit/keys/policy-bundle-primary/rotate
Больно то, что происходит с уже развёрнутыми наборами, подписанными старой версией ключа. Есть два варианта:
- Заставить перепубликовать каждый набор для каждого арендатора при каждой ротации. Шумно, разрушительно и соблазнительно пропустить в следующий раз.
- Переподписать существующие наборы на месте контролируемым пакетным процессом.
Мы используем второй. Переподписчик — короткоживущая задача, работающая с хранилищем артефактов: она загружает каждый развёрнутый набор, перепроверяет существующие подписи (старые версии ключей остаются доверенными на время перекрытия), затем запрашивает новую основную подпись под ротированной версией ключа и записывает набор обратно с обеими подписями.
- Vault Transit создаёт новую версию ключа (v_new)
- Старая версия (v_old) остаётся действительной для проверки
- Конфигурация доверия прокси обновляется под обе версии
СостояниеВ обороте две версии ключа; для развёрнутых наборов пока ничего не изменилось
- Задача переподписи перечисляет все наборы в хранилище
- Для каждого: проверка(v_old) + проверка(вторичной), затем подпись(v_new)
- Набор перезаписывается с новой основной подписью, вторичная сохраняется
- Идентичность переподписчика ограничена только этим процессом
СостояниеКаждый развёрнутый набор несёт основную подпись версии v_new
- Удалить v_old из конфигурации доверия прокси
- Удалить v_old из Vault Transit
- Журнал аудита сохраняет оба события подписи для исторического набора
Состояниеv_old больше нигде не доверен; ротация завершена без простоя
Два замечания:
- Вторичная подпись сохраняется без изменений. Одновременная ротация обоих ключей стёрла бы свидетельство двойного контроля на всех исторических наборах. Мы ротируем их со сдвигом по графику.
- Переподписчик работает под собственной выделенной идентичностью, которой разрешён только процесс переподписи: он не может создавать новые наборы, публиковать новые изменения политик или убирать подписи. Он может лишь перепривязать набор, уже подписанный двумя людьми, к свежей версии основного ключа. Именно эта узость и делает безопасной его автоматизацию.
7. Со стороны комплаенса: что на самом деле спрашивают аудиторы
Статья 12 EU AI Act (ведение записей), ISO 27001 Приложение A.12.4 (журналирование), SOC 2 CC7.2 (управление изменениями). Применительно к политике управления ИИ все они сводятся к трём вопросам:
- Кто санкционировал это изменение и как вы это докажете?
- Можно ли подменить изменение после санкционирования?
- Можете ли вы восстановить, какая политика действовала в момент любого прошлого решения?
Подписанные наборы отвечают на все три:
- Кто санкционировал — две разные идентичности Vault, у каждой своё событие подписи в журнале аудита Vault. Идентичности привязаны к людям через ваш провайдер идентификации, а событие подписи фиксирует время, версию ключа и хеш входа.
- Устойчивость к подмене следует из того, что любое изменение любого файла внутри набора меняет
checksums.txt, что делает обе подписи недействительными, и прокси отказывается его загружать. - Историческое восстановление обеспечивает поле
policy_bundle_sha256, которое каждый запрос может нести в журнале аудита. Сопоставьте этот хеш с хранилищем артефактов — и у вас тот самый набор с теми самыми подписями, который принял решение по запросу.
Аудиторская цепочка идёт насквозь: журнал запроса указывает на набор, набор — на события подписи, а те — на людей за ними, поэтому след держится на записях, а не на устных заверениях.
Заключение
Двойной контроль публикации политик — из тех инженерных вложений, которые выглядят накладными расходами ровно до первого раза, когда они останавливают плохое изменение, и с этого момента более-менее себя окупают. Криптография хорошо изучена, инструменты есть, а проверка Ed25519 достаточно дёшева, чтобы аргумента о производительности против её выполнения при каждой загрузке набора просто не было.
Сложнее операционная часть: определить две роли подписантов, поставить перед ключами правильные политики Vault, построить переподписчика, которому доверяешь работать без присмотра, и не поддаться соблазну добавить «аварийный» обход, который обнуляет всю модель.
Следующим мы опубликуем чек-лист соответствия EU AI Act: та же поверхность контролей, разложенная по статьям, с явными указателями, какой артефакт (подписанный набор, журнал аудита, запись о ротации ключей) закрывает какое требование.
Дополнительное чтение
- HashiCorp Vault: документация Transit Secrets Engine
- RFC 8032: Edwards-Curve Digital Signature Algorithm (EdDSA)
- ISO/IEC 27001:2022, Приложение A.12.4 (журналирование и мониторинг)
- Регламент (ЕС) 2024/1689, статьи 12, 15, 26
Готовы защитить
корпоративную инфраструктуру?
Запишитесь на технический брифинг. Без продаж, только архитекторы и ваша команда.