Инженерия

Публикация политик под двойным контролем с HashiCorp Vault Transit

Подписанный набор политик — единственный артефакт, которому прокси управления должен доверять. Разбираем, как мы публикуем такие наборы под двойным контролем, ротируем ключи без простоя и подтверждаем целостность на каждом запросе, используя HashiCorp Vault Transit и подписи Ed25519.

Автор Krasper Engineering 27 Май 2026 8 мин чтения
Dual-Control Policy Publishing — hero

Коротко. Если один инженер может выкатить изменение политики, ослабляющее маскирование, открывающее новый эндпоинт или добавляющее модель в белый список, то прокси управления — театр. Реальное применение держится на подписанных наборах, двойном контроле при каждой публикации и ротации ключей без переразвёртывания. Криптографию берёт на себя HashiCorp Vault Transit, Ed25519 держит проверку дешёвой, а связывает всё небольшой набор правил публикации.

Прокси управления на каждом запросе решает, может ли промпт покинуть периметр, маскируются ли персональные данные, допустим ли код в ответе и какие модели вообще доступны. Эту границу задаёт набор политик: версионированный машиночитаемый артефакт, собранный из декларативных правил. Кто может изменить этот набор, тот управляет периметром.

В материале разбирается модель, по которой мы публикуем наборы политик в Raigate: как выпускаются подписи Ed25519 через HashiCorp Vault Transit, как двойной контроль обеспечивается на шаге публикации (а не в редакторе), как прокси проверяет наборы при загрузке и как мы ротируем ключи подписи без окна обслуживания.

Содержание

  1. Почему двойной контроль нужен на уровне политик
  2. Анатомия подписанного набора политик
  3. Vault Transit как оракул подписи
  4. Сквозной процесс публикации
  5. Проверка на стороне прокси
  6. Ротация ключей и шаблон переподписи
  7. Со стороны комплаенса: что на самом деле спрашивают аудиторы

1. Почему двойной контроль нужен на уровне политик

Большинство команд уже где-то применяет двойной контроль: продуктивные выкатки, миграции баз, изменения в IAM. Наборы политик для прокси управления ИИ заслуживают того же по простой причине: набор и есть политика.

Изменение одной строки в JSON-предикате может:

  • молча отключить маскирование персональных данных для арендатора,
  • разрешить новый исходящий эндпоинт, через который утекают промпты,
  • добавить в белый список неутверждённую модель со слабее настроенной безопасностью,
  • превратить умолчания с отказом в закрытое состояние в отказ в открытое.

Одного код-ревью мало. Ревьюер, одобривший пул-реквест, — не то же самое, что криптографический со-подписант, свидетельствующий об артефакте, который поедет в продуктив. Эти двое расходятся при ребейзе, конфликте слияния, «крошечной» правке после одобрения или при компрометации CI-раннера.

Решение — перенести границу доверия с репозитория на подписанный артефакт. Каждый работающий прокси проверяет подпись до загрузки набора. Набор загружается, только если несёт действительную подпись ключа, которому прокси уже доверяет; отсутствующая, неверная или корректная подпись неизвестного ключа оставляют его лежать на диске.

2. Анатомия подписанного набора политик

Набор — детерминированный архив. На вход идут исходники, на выходе — канонический поток байтов, который хешируется и подписывается. Детерминированность важна, потому что одна и та же логическая политика должна давать один и тот же хеш на любой машине. Иначе проверка подписи превращается из гарантии в лотерею.

text
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
Детерминированная структура набора
Исходники
policies/
  • redaction.json: правила по ПДн, умолчания с отказом в закрытое состояние
  • routing.json: модели и эндпоинты по арендаторам
  • content.json: запрещённые категории, ограничения вывода
Канонизация
checksums.txt
SHA-256 каждого файла, отсортировано по пути. Именно эти байты подписывают.
Двойной контроль
signatures/
  • 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 и версия ключа, которая её произвела:

bash
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 (основной ключ)

Вызов подписи закрыт политикой Vault, которая требует одновременно аутентифицированную человеческую личность и активный тикет согласования. Ни один сервисный аккаунт не может вызвать transit/sign/policy-bundle-primary напрямую. То же самое, с другой политикой и другой группой идентичностей, действует для вторичного ключа.

Именно это превращает двойной контроль из командной договорённости в свойство системы: один человек не может произвести обе подписи, потому что ни у одной идентичности нет прав Vault на оба эндпоинта подписи.

4. Сквозной процесс публикации

Оракул подписи — центральный элемент, но безопасной эксплуатацию делает процесс вокруг него. Путь изменения политики от черновика до продуктива:

AUTHOR REVIEWER VAULT TRANSIT DISTRIBUTOR PROXY FLEET open draft PR build candidate bundle request review + sign dry-eval on corpus sign(primary) Ed25519/v3 sign(secondary) Ed25519/v2 publish doubly-signed bundle distribute by content-hash verify both signatures Ed25519 × 2 atomic policy swap fail-closed if verify fails

Конкретно:

  1. Автор открывает изменение политики обычным пул-реквестом. CI собирает кандидатный набор и публикует его SHA-256 как статус-чек. Пул-реквест нельзя слить, пока хеш не совпадает с последним коммитом ветки.
  2. Ревьюер открывает кандидат в студии политик, прогоняет сухую оценку на регрессионном корпусе (переигранный исторический трафик плюс подобранный набор red-team промптов) и либо отклоняет, либо помечает набор готовым к публикации. Ревьюер — не автор.
  3. Основная подпись запрашивается. Система забирает кандидат, пересчитывает checksums.txt и вызывает Vault Transit под идентичностью автора с основной политикой подписи. Подпись прикрепляется.
  4. Вторичная подпись запрашивается под идентичностью ревьюера, вторичным ключом. Если хоть одна подпись отсутствует или недействительна, набор не покидает сборочную среду.
  5. Дистрибутор публикует дважды подписанный набор в хранилище артефактов по адресуемому содержимым пути: bundles/<sha256>.tar.gz. Флот тянет оттуда.
  6. Флот прокси проверяет обе подписи по закреплённым публичным ключам текущих версий и атомарно переключает активную политику. Ошибка загрузки оставляет прежний набор, отказ в закрытое состояние на всех уровнях.

Никто не может утвердить собственное изменение, и ревьюер не может опубликовать без основной подписи автора. Дистрибутор, в свою очередь, принимает только наборы, обе подписи которых проходят проверку по текущему набору публичных ключей. Каждый рубеж — жёсткая криптографическая проверка, а не элемент интерфейса.

5. Проверка на стороне прокси

Прокси держит в конфигурации небольшой набор публичных ключей, закреплённых по версиям:

json
{
  "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 }
}
Конфигурация trusted_signers на прокси

Процедура проверки по шагам:

  1. Распаковать архив во временный каталог.
  2. Пересобрать checksums.txt из распакованных файлов в отсортированном порядке.
  3. Захешировать его SHA-256.
  4. Для каждого файла .sig прочитать встроенные имя и версию ключа, найти их в trusted_signers, отказать при отсутствии.
  5. Проверить подпись Ed25519 против пересчитанного хеша.
  6. Если установлен require_both, должны пройти обе. Иначе отклонить.

Ed25519 выбран за стоимость проверки. Проверка одной подписи занимает меньше миллисекунды на обычном железе — достаточно дёшево, чтобы выполнять её при каждой перезагрузке набора, в health-check прокси и в стартовой пробе, которая не помечает под готовым, пока активный набор не перепроверен.

Прокси никогда не доверяет ключу, о котором ему не сказали. Новые версии ключей требуют обновления конфигурации, а оно само проходит тот же путь ревью и выкатки, что и любое продуктивное изменение.

6. Ротация ключей и шаблон переподписи

Ключи Ed25519 не слабеют со временем, но операционный принцип остаётся: ключ, подписывавший достаточно долго, следует ротировать. Риск не в алгоритме. Он в хранении, текучке персонала и медленном накоплении идентичностей, которые за несколько лет могли коснуться эндпоинта подписи.

Больно не сгенерировать новый ключ. Vault Transit делает это одним вызовом:

bash
vault write -f transit/keys/policy-bundle-primary/rotate
Ротация основного ключа подписи

Больно то, что происходит с уже развёрнутыми наборами, подписанными старой версией ключа. Есть два варианта:

  1. Заставить перепубликовать каждый набор для каждого арендатора при каждой ротации. Шумно, разрушительно и соблазнительно пропустить в следующий раз.
  2. Переподписать существующие наборы на месте контролируемым пакетным процессом.

Мы используем второй. Переподписчик — короткоживущая задача, работающая с хранилищем артефактов: она загружает каждый развёрнутый набор, перепроверяет существующие подписи (старые версии ключей остаются доверенными на время перекрытия), затем запрашивает новую основную подпись под ротированной версией ключа и записывает набор обратно с обеими подписями.

Шаг 01
T+0ч
Ротация основного ключа
  • Vault Transit создаёт новую версию ключа (v_new)
  • Старая версия (v_old) остаётся действительной для проверки
  • Конфигурация доверия прокси обновляется под обе версии

СостояниеВ обороте две версии ключа; для развёрнутых наборов пока ничего не изменилось

Шаг 02
T+0–48ч
Переподпись развёрнутых наборов
  • Задача переподписи перечисляет все наборы в хранилище
  • Для каждого: проверка(v_old) + проверка(вторичной), затем подпись(v_new)
  • Набор перезаписывается с новой основной подписью, вторичная сохраняется
  • Идентичность переподписчика ограничена только этим процессом

СостояниеКаждый развёрнутый набор несёт основную подпись версии v_new

Шаг 03
T+72ч
Вывод v_old
  • Удалить v_old из конфигурации доверия прокси
  • Удалить v_old из Vault Transit
  • Журнал аудита сохраняет оба события подписи для исторического набора

Состояниеv_old больше нигде не доверен; ротация завершена без простоя

Два замечания:

  • Вторичная подпись сохраняется без изменений. Одновременная ротация обоих ключей стёрла бы свидетельство двойного контроля на всех исторических наборах. Мы ротируем их со сдвигом по графику.
  • Переподписчик работает под собственной выделенной идентичностью, которой разрешён только процесс переподписи: он не может создавать новые наборы, публиковать новые изменения политик или убирать подписи. Он может лишь перепривязать набор, уже подписанный двумя людьми, к свежей версии основного ключа. Именно эта узость и делает безопасной его автоматизацию.

7. Со стороны комплаенса: что на самом деле спрашивают аудиторы

Статья 12 EU AI Act (ведение записей), ISO 27001 Приложение A.12.4 (журналирование), SOC 2 CC7.2 (управление изменениями). Применительно к политике управления ИИ все они сводятся к трём вопросам:

  1. Кто санкционировал это изменение и как вы это докажете?
  2. Можно ли подменить изменение после санкционирования?
  3. Можете ли вы восстановить, какая политика действовала в момент любого прошлого решения?

Подписанные наборы отвечают на все три:

  • Кто санкционировал — две разные идентичности 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

Готовы защитить
корпоративную инфраструктуру?

Запишитесь на технический брифинг. Без продаж, только архитекторы и ваша команда.