Безопасность

Где ключ на каждом хопе

Эта страница написана для того, кто подписывает решение поставить боевой кред за сервис. Здесь сказано, что держит конструкция и что решает ваш собственный каталог и политика. И то, и другое проверяется на вашей машине, и эта страница объясняет как.

Архитектура

Три хопа и одно поле

Две роли из одного бинарника. Control держит базу, хранилище и журнал. Executor делает исходящий вызов и живёт в сети, откуда нет маршрута ни к базе, ни к хранилищу. Кред покидает хранилище один раз на вызов, внутри процесса, который прямо сейчас будет его использовать.

Где кред на каждом хопеТри хопа. Агент отправляет действие без креда. Шлюз классифицирует его, при необходимости зовёт человека, пишет строку намерения, разворачивает ключ внутри своего процесса и отправляет. Апстрим отвечает, и внедрённые байты вырезаются до того, как ответ дойдёт до агента.намерениеunwrapрезультатВаш агентдействие, аргументы, без ключана этом отрезке креда нетControl и executor1. классификация по рецепту2. вопрос человеку или живой grant3. запись намерения и печать4. unwrap на этот вызов, подстановка, отправкаХранилище арендаторазапечатано, версияединственный отрезок с ключомАпстримобычный авторизованный запросвнедрённые байты вырезаны из тела, заголовков и Location
Жёлтое поле и провод под ним — единственные места, где кред существует. Всё, до чего дотягивается агент, лежит по другую сторону этой линии.

Граница

Что держит конструкция и что решает ваш каталог

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

Держится конструкцией
Ни один ответ агенту не несёт значения креда. Ни один маршрут платформы не читает его обратно: ни владельцу, ни поддержке, ни через API. Executor заменяет точные внедрённые байты и их base64-форму в теле, заголовках и Location каждого ответа. Исходящие байты сканируются на наши префиксы токенов и на внедрённое значение, и совпадение отклоняет вызов.
Решает ваш каталог
Агент не получает секрет от нас. Он может получить секрет от апстрима, если вы разрешили ему путь, который его выдаёт. Такие пути известны и названы: installation-токены GitHub, CI-переменные GitLab, env Vercel с decrypt, kv у Vault. Рецепт их помечает, класс — destructive, по умолчанию отказ, и консоль показывает полный их список в вашем арендаторе.
Держится двумя ролями в базе
Control подключается ролью, которая ничем не владеет и не может обойти row-level security. Изоляция двухуровневая, арендатор и воркспейс, и ставится внутри каждой транзакции. У партиционированной таблицы политика есть на каждой партиции, и тест по системному каталогу роняет сборку, если одной не хватает.
Держится сетью
Executor'ы сидят в одной сети с control и больше нигде: ни базы, ни объектного хранилища, ни ключей. Они получают один кред на один вызов по взаимному TLS. Для SSH приватный ключ им не достаётся вовсе: каждый challenge подписывает control.

Точный список

Пути, которые выдают собственный кред

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

СервисПутьЧто происходит
Amazon Web Services^sts(\.[a-z0-9-]+)?\.amazonaws\.com$destructive, по умолчанию отказ
Amazon Web ServicesX-Amz-Target: (?i)(^|[._])CreateAccessKey$destructive, по умолчанию отказ
Amazon Web Services?Action=^CreateAccessKey$destructive, по умолчанию отказ
Amazon Web ServicesAction=^CreateAccessKey$destructive, по умолчанию отказ
Amazon Web ServicesX-Amz-Target: (?i)(^|[._])GetSecretValue$destructive, по умолчанию отказ
Amazon Web ServicesX-Amz-Target: (?i)(^|[._])GetParameters?$ $.WithDecryption ~ ^true$destructive, по умолчанию отказ
GitHub APIPOST ^/app/installations/[^/]+/access_tokens$destructive, по умолчанию отказ
GitHub APIPOST ^/(repos/[^/]+/[^/]+|orgs/[^/]+)/actions/runners/(registration|remove)-token$destructive, по умолчанию отказ
GitLab API^(/api/v4)?/(projects|groups)/[^/]+/variablesdestructive, по умолчанию отказ
GitLab APIPOST ^(/api/v4)?/(projects|groups)/[^/]+/(deploy_tokens|access_tokens|runners)destructive, по умолчанию отказ
Sentry APIGET HEAD POST ^/api/0/projects/[^/]+/[^/]+/keys/destructive, по умолчанию отказ
Slack Web API^/api/(oauth\.(v2\.)?access|apps\.connections\.open|tooling\.tokens\.rotate)$destructive, по умолчанию отказ
Vercel API/env ?decrypt=^true$destructive, по умолчанию отказ
Vercel APIGET HEAD ^/v[0-9]+/projects/[^/]+/env/[^/]+$destructive, по умолчанию отказ

Консоль показывает тот же список для вашего арендатора, отфильтрованный по binding’ам, которые у ваших агентов действительно есть.

Хранилище

Конвертное шифрование и ни одного пути прочитать значение

Четыре шага, каждый уже предыдущего. Root-wrapper на хосте открывает ключ арендатора. Тот открывает доменный ключ: один для кредов, один для журнала, один для тел и записей SSH-сессий, которые вы решили хранить. Из него через HKDF-SHA256 выводится подключ записи, и запись запечатывается ChaCha20-Poly1305 под аутентифицированным заголовком из арендатора, домена, записи и версии. Поменяйте любое из четырёх, и запись не откроется.

Один путь unwrap, и он открывается на одном состоянии
Движок разворачивает значение для вызова, который уже перешёл в dispatching. Второго вызывающего нет: ни экспорта, ни инструмента поддержки, ни экрана администратора, и тест обходит пакет, чтобы так и оставалось.
У root-ключа вторая копия есть с минуты рождения
При генерации он делится на три доли, любые две из которых его собирают: доля в менеджере секретов владельца, доля офлайн, доля во внешнем KMS. Обращение к доле — событие в платформенном журнале.
Ежедневный самотест, чтобы первым сказал монитор
Раз в сутки джоба разворачивает тестовый ключ и публикует результат метрикой с алертом. Это ловит именно тот случай, когда всё отвечает, а любой вызов с кредом падает.

Журнал

Пишется до вызова, запечатывается от собственного писателя

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

Запечатано age на получателя, которого у писателя нет
Тело каждого события шифруется на X25519-получателя арендатора до того, как его хешируют. У процесса, который пишет журнал, есть только публичная половина.
Checkpoint’ы остаются у вас, подпись Ed25519
Корень Меркла над цепочкой, подписанный, доступен по API и уходит в вашу SIEM вместе с номером и хешем строки. Ваша SIEM становится свидетелем для своего сегмента. Формат цепочки несёт собственную версию, spc-chain/1, поэтому верификатор, написанный сегодня, читает экспорт, снятый завтра.
Верификатор без единого нашего ключа
Наведите открытый инструмент на любой экспорт. Он пересчитает цепочку, проверит подпись и скажет, где первый разрыв. Испорченная строка в эталонном экспорте — часть нашего собственного набора тестов.
Цепочка одного арендатораПять запечатанных строк, каждая хеширует предыдущую, под подписанным checkpoint'ом.#4129f2c…#41341ab…#4140d77…#415b3e1…#4165c8a…каждая строка хеширует предыдущуюзапечатано на ключ, которого у писателя нетподписанный checkpointваш, и его можно отдать аудитору

Ваши данные, а не только ключи

Тела, которые несут ваши вызовы

Через нас едет не только кред. Ответ Stripe с карточкой клиента, дифф из GitHub, вывод SSH-команды — всё это проходит через executor. Вот что с этим происходит.

Захват выключен, пока вы его не включите
Binding хранит тела запросов и ответов, только если вы об этом попросили. До этого в журнале лежит форма вызова и его результат, но не содержимое.
То, что хранится, запечатано как журнал
Сохранённые тела и записи SSH-сессий лежат под своим доменным ключом, зашифрованные по записи, со ссылкой из журнала. Они живут по сроку вашего плана и своему TTL.
Уведомления не несут содержимого никогда
Карточка в Telegram или письмо содержат цель вызова и ссылку на аутентифицированный экран. Решение принимается только на поверхности, куда вы вошли.
Удаление арендатора уничтожает ключи, которыми они открываются
Crypto-shredding вместо джобы удаления, которой нужно верить. После него ни одна запись этого арендатора больше не открывается, ни у нас, ни где-либо ещё.

Executor держит тело ровно столько, сколько идёт вызов. У него нет ни базы, ни объектного хранилища, ни маршрута к ним.

Egress

Агент не может навести шлюз на шлюз

Список запретов вкомпилирован в бинарник, и конфигурация умеет только добавлять. Приватные диапазоны, loopback, link-local, CGNAT, деплой-сеть и наши собственные имена отклоняются независимо от того, есть ли на них binding.

Имя резолвится заново на каждом исполнении
В том числе на исполнении после ожидания одобрения. Хост, который был публичным, когда человек отвечал на карточку, к моменту вызова может указывать на метадата-адрес, поэтому адрес резолвится и пинится при отправке, и вызов идёт на тот адрес, который проверили.
Редиректы с кредом останавливаются
3xx возвращается агенту как результат. Шлюз не идёт по нему с вашим ключом.
Лимит отправляет вызов в одобрение
Одновременно работают три лимитера: на агента, на binding и на арендатора. Когда один срабатывает, вызов уходит в одобрение, а не в тупик, и лимит скорости отвечает временем повтора словами, по которым модель может действовать.

Спецификация

Ревизия MCP от 2026-07-28 требует именно такой формы и запрещает противоположную

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

Обе живые ревизии, по хендлеру на каждую
Ревизия 2026-07-28 stateless: без сессионного заголовка, без серверного стрима, discover вместо initialize. Ревизия 2025-11-25 сохраняет сессию. Один хендлер на обе неправ для обеих.
Список инструментов приватен для binding'а
Ваш список отличается от чужого, поэтому он помечен приватным. Клиент, кеширующий его как общий, показал бы одному арендатору инструменты другого.
Сервер авторизации с флагами, которые нужны коннекторам
Метаданные защищённого ресурса, client id metadata documents, динамическая регистрация для совместимости, PKCE, индикатор ресурса и параметр issuer. Реестр согласий на пользователя и клиента и страница согласия, написанная по чек-листу самой спецификации для прокси со статическим client id.

Когда лежим мы

Вы продолжаете работать без нас

В каждом воркспейсе есть печатная справка в консоли, которую продукт держит актуальной: каждый binding, сервис, кред, провайдер, у которого он выпущен, ссылка на перевыпуск и ссылка на отзыв. Команда, которая не может работать, пока лежим мы, не поставит нас на критический путь.

Ожидающие одобрения переживают рестарт
Состояние лежит в базе. Деплой control не роняет ждущий вызов, и агент продолжает по тому же call_id.
Неизвестный исход остаётся неизвестным
Если реплика умерла после того, как апстрим принял запрос, вызов помечается исходом, которого никто не знает, и автоматически не повторяется. Двойное списание хуже пропавшей строки.
Кража токена агента — обнаруживаемый, отзываемый, ограниченный инцидент
Новый источник поднимает сигнал, агент ставится на паузу, токен ротируется с перекрытием, а журнал показывает ровно те вызовы, где он использовался.

Сначала прочитайте верификатор, потом доверяйте журналу

Инструмент открытый, экспорт — файл, а проверка идёт на вашей машине без единого нашего ключа.

Подключить первого агента

10 000 вызовов в месяц бесплатно. Журнал выгружается на любом плане, и checkpoint'ы есть на любом плане.