Как Sallyport защищает ваши ключи
Где хранится ключ и когда он открывается, как разделены аккаунты, что отклоняет сетевой фильтр и как подписан журнал.
На этой странице
- Ключ проходит один отрезок: от Sallyport до сервиса
- У каждой сохранённой записи свой ключ
- Аккаунты не видят данных друг друга
- Агент не может навести шлюз на сам шлюз
- Агенты входят со своими токенами
- Человек решает там, где вы попросили, а два действия ждут его всегда
- Каждый вызов записан до того, как выполнится
- Что происходит с ответами, которые приносят вызовы
- Перезапуски и сбои не теряют и не повторяют вызовы
Ключ проходит один отрезок: от Sallyport до сервиса
Один процесс держит хранилище ключей и журнал и сам делает исходящий вызов. Агент называет действие. Sallyport подставляет ключ в запрос и отправляет его. Агент ключа не получает.
- Ни один маршрут не возвращает ключ. Ни владельцу, ни поддержке, ни через API. Ключ можно заменить или удалить, показать его нельзя.
- Ключ уходит только на адреса, с которыми его сохранили. Новое подключение, новый адрес или новый адрес выдачи токенов для сохранённого ключа Sallyport не примет: для другого адреса ключ вводят заново. Ключ из каталога доходит и до API-хостов своего сервиса.
- Ключ открывается в трёх случаях: для вызова агента, допущенного к отправке, для проверки, которую вы запускаете сохранением ключа или кнопкой «Проверить снова», и для чтения списка инструментов подключённого вами MCP-сервера. Каждое открытие попадает в журнал до того, как ключ использован.
- В памяти ключ остаётся байтами и стирается после вызова. Строку Go стереть нельзя, поэтому тест роняет сборку, если ключ где-то превращается в строку.
У каждой сохранённой записи свой ключ
Корневой ключ открывает ключ аккаунта. Из ключа аккаунта через HKDF-SHA256 выводится ключ для каждого вида данных: ключи сервисов, продление OAuth, запросы агентов, секреты вебхуков. Из него выводится ключ записи.
Каждая запись запечатана ChaCha20-Poly1305 и привязана к аккаунту, виду данных, записи и версии. Измените любое из четырёх, и запись перестанет открываться.
- Корневой ключ никогда не лежит в базе. Это файл на хосте шлюза. Любые две из трёх долей восстановления собирают его заново.
- Корневой ключ проверяется каждый день. Ежедневная задача открывает им настоящий ключ аккаунта, и о сбое операторы узнают сразу.
Аккаунты не видят данных друг друга
Шлюз подключается к базе под ролью, которая не владеет ни одной таблицей и не может обойти построчную защиту. Каждый запрос идёт в транзакции, где указаны аккаунт и рабочие пространства, которые он видит.
- Тест роняет сборку, если у таблицы или раздела журнала нет политики.
- Ваш трафик идёт по вашим соединениям. Sallyport держит отдельный HTTP-транспорт на аккаунт и хост сервиса, и запрос другого аккаунта не попадёт в ваше соединение.
Агент не может навести шлюз на сам шлюз
Sallyport вызывает только публичные сервисы в интернете. Перед подключением шлюз определяет адреса имени и сверяет каждый со списком запретов, вшитым в бинарник. Настройки могут этот список только расширить.
- Что бы ни было написано в подключении, отклоняются частные диапазоны, loopback, link-local и адреса облачных метаданных, CGNAT, multicast и все имена и сети Sallyport.
- Имя определяется заново, когда вызов выполняется, даже если вызов ждал человека. Адрес проверяется и закрепляется в момент отправки.
- Ключ не уходит по редиректу. Ответ 3xx возвращается агенту как результат. Если агенту нужен новый адрес, он делает новый вызов, и все проверки проходят снова.
Агенты входят со своими токенами
У каждого агента свои токены, их показывают один раз при создании. В базе хранится только HMAC-SHA-256 токена с секретом, который лежит вне базы. Копия базы не содержит ни одного рабочего токена.
- Отключить агента можно одной кнопкой. «Приостановить агента» на его странице отклоняет следующий вызов, с каким бы токеном или входом он ни пришёл. Отзыв токена в разделе «Токены» останавливает только этот токен. «Удалить агента» внизу его страницы убирает агента насовсем.
- MCP-клиент, который входит через OAuth, получает токен Sallyport после того, как человек одобрит клиента на экране согласия. Ключа сервиса клиент не видит.
- Платформа, которая запускает агентов на своих машинах, подключается как доступ к управлению агентами, а не как агент. Её собственный токен ничего не вызывает: MCP-адрес и прокси его отклоняют. Часовой токен на каждую машину она получает только после согласия человека. Каждый такой агент начинает без доступа, и дать его может только человек. Каждый токен действует как собственный агент машины и заканчивается вместе с подключением.
- Список инструментов у каждого агента свой, поэтому он помечен как приватный. Клиент не может закешировать инструменты одного агента и показать их другому.
Человек решает там, где вы попросили, а два действия ждут его всегда
По умолчанию каждый вызов проходит без вопросов. Включите «Спрашивать каждый раз» для агента и подключения, если хотите видеть каждый вызов заранее.
- Карточка показывает запрос, который отправит Sallyport: метод, хост и путь. Слова самого агента стоят отдельным блоком с пометкой «не проверено».
- Кнопка «Разрешить вызов» действует на один конкретный запрос. Такое одобрение привязано к дайджесту запроса, и перед отправкой сохранённый запрос сверяется с ним. Кнопка разрешения на карточке покрывает больше: тот же агент, подключение, класс и путь на заданное время и число вызовов.
- DELETE не пройдёт под видом GET. Заголовки и параметры подмены метода удаляются до классификации вызова и до расчёта дайджеста.
- Лимит частоты или параллельности отвечает временем для повтора. Карточку он не создаёт: решать тут нечего.
Каждый вызов записан до того, как выполнится
Намерение вызова и его запись в журнале идут в одной транзакции с решением об отправке. Номера берутся из строки-счётчика, которую откат не может пропустить. В нумерации нет пропусков, и пропавшая запись читается как удаление.
- Каждая запись ссылается на предыдущую. Её хеш покрывает схему, аккаунт, номер, хеш предыдущей записи и само событие. Схема называется spc-chain/2.
- Каждая запись подписана Ed25519. Публичные ключи, текущий и выведенные из работы, опубликованы по адресу
https://mcp.sallyport.cloud/.well-known/sallyport-audit-keys.json, и выведенный ключ остаётся в этом списке после смены. - Шлюз не может править собственный журнал. Его роль в базе добавляет и читает записи, а права изменять их у неё нет.
- События хранятся читаемым каноническим JSON, и цепочка считается ровно по этим байтам. Любой, кому вы дадите файл, прочтёт то же, что и вы.
- Кнопка «Скачать журнал» на экране «История вызовов» сохраняет весь журнал в JSON Lines, каждую запись с её хешем и подписью. Она для того, кто аудирует все рабочие пространства: владельца либо администратора или аудитора всей организации без более узкой роли в каком-либо пространстве.
- Каждая запись журнала относится к рабочему пространству того, о чём она. Вкладка «Журнал» и вебхук показывают человеку записи пространств, где он владелец, администратор или аудитор, и записи самой организации. Администратор организации, который в пространстве клиента только участник, записей этого клиента не читает.
Что происходит с ответами, которые приносят вызовы
Ответ, который помещается в сообщение, сразу уходит агенту. Длинный ответ сохраняется, и агент дочитывает его частями.
- Сохранённый ответ может прочитать агент, чей вызов его получил, и люди вашего аккаунта, которым открыт журнал. Каждое чтение попадает в журнал.
- Сохранённые ответы, ответ каждого вызова и тела запросов больше 1 МиБ удаляются через 30 дней. Большое тело хранится зашифрованным своим ключом.
- История вызовов, запрос каждого агента и журнал хранятся 180 дней по умолчанию. Срок меняется в «Настройки», «Организация», «История вызовов и журнал (дни)».
- Уведомление в браузере называет агента, вид действия и сервис, а идентификаторы в пути заменены. Значений аргументов, тела запроса и ключей в нём не бывает.
Перезапуски и сбои не теряют и не повторяют вызовы
- Вызов, который ждёт человека, хранится в базе. Выкладка его не теряет, и агент продолжает с тем же id вызова.
- Если процесс остановился после того, как запрос ушёл к сервису, вызов помечается как вызов с неизвестным исходом. Sallyport сам его не повторяет, поэтому платёж не уйдёт дважды.