Ранее уже разбирали, зачем нужен credential-прокси и почему у него не может быть «zero-knowledge»-режима: proxy обязан расшифровать ключ в памяти, чтобы подставить его в запрос к провайдеру. Это ProxyKey (proxykey.org) — наш проект. В этот раз — практическая часть: MCP-сервер, который даёт агенту (Claude Code, Cursor) управлять доступом к API, но не даёт прочитать сам ключ, и сценарий pending secret — когда агент разворачивает сервис раньше, чем появляется реальный токен.

Проблема: агент и ключ в одном контексте

Агентный код (Claude Code, Cursor и подобные) читает и пишет .env, генерирует конфиги, иногда логирует свои действия для отладки. Любой токен, который агент хоть раз увидел, нужно считать скомпрометированным по умолчанию: контекст модели логируется, трассируется, часть данных может уйти в третий сервис через промпт-инъекцию в обрабатываемом контенте. Обычная практика «положить ключ в переменную окружения и дать агенту доступ к shell» на практике означает, что ключ рано или поздно попадёт в лог диалога с моделью.

Вариант решения — вынести управление доступом за пределы контекста модели: агент работает не с ключом, а с MCP-инструментом, у которого физически нет операции чтения секрета.

Модель ProxyKey: secret / pass / proxy

  • Secret — реальный ключ провайдера. Вводится один раз через веб-панель, хранится зашифрованным (AES-256-GCM, envelope encryption — подробности в открытом репозитории proxykey-crypto), ни одна ручка API не возвращает его значение после создания.

  • Pass (vlt_…) — виртуальный токен, привязанный к secret. У него своя привязка по IP, свои rate-лимиты, свой TTL и отдельный лог запросов.

  • Proxy — принимает запрос с pass вместо ключа, валидирует его, расшифровывает secret в памяти на время одного запроса, подставляет в исходящий запрос к провайдеру.

Вызов меняется минимально — хост и токен, путь и тело без изменений:

# было
curl https://api.openai.com/v1/chat/completions -H "Authorization: Bearer sk-..."
# стало
curl https://api.proxykey.org/p/openai/v1/chat/completions -H "Authorization: Bearer vlt_openai_..."

Стриминг (SSE) и заголовки проходят прозрачно. У Telegram-ботов формат URL сохраняется: /p/telegram-bot/<pass>/getMe.

Подключение MCP-сервера

Регистрация — обычный веб-флоу (GitHub OAuth или magic link), результат — токен mcp_… для доступа к самому MCP-серверу (не путать с pass — доступом к провайдерам).

Claude Code:

claude mcp add --transport http proxykey https://mcp.proxykey.org/mcp \
  --header "Authorization: Bearer mcp_YOUR_TOKEN"

Cursor / Claude Desktop, mcp.json:

{
  "mcpServers": {
    "proxykey": {
      "url": "https://mcp.proxykey.org/mcp",
      "headers": { "Authorization": "Bearer mcp_YOUR_TOKEN" }
    }
  }
}

Транспорт — Streamable HTTP.

13 инструментов

Группа

Инструмент

Что делает

Каталог

list_providers

Список провайдеров и модель авторизации каждого

Каталог

list_secrets

Список сохранённых секретов — только метаданные

Каталог

get_manual_secret_setup

Ссылка для человека на ввод реального ключа

Пропуски

create_pass

Выпустить pass для уже существующего secret

Пропуски

create_pending_pass

Выпустить pass до того, как secret заполнен

Пропуски

update_pass

Изменить лимиты, режим IP-привязки, срок действия

Пропуски

rotate_pass

Перевыпустить токен pass с теми же настройками

Пропуски

revoke_pass / delete_pass

Мгновенно отозвать / удалить отозванный pass

Пропуски

rebind_pass_ip

Сбросить обучение по IP

Наблюдаемость

list_passes

Все пропуска со статусом, лимитами, привязкой

Наблюдаемость

get_pass_logs

Лог запросов конкретного pass

Наблюдаемость

get_pass_stats

Статистика использования pass

Ни один из 13 методов не принимает и не возвращает значение секрета. Это ограничение контракта API, а не соглашение об использовании: агент физически не может запросить то, чего нет в спецификации инструмента. По той же причине MCP-поверхность не может включить логирование тел запросов — эта настройка тоже осталась только в панели, доступной человеку.

Сценарий pending secret

Рабочий кейс: агент настраивает Telegram-бота — пишет код, конфигурирует вебхук, — но токена от BotFather ещё нет, потому что сам бот ещё не создан.

Порядок действий агента:

  1. create_pending_pass — pass (vlt_…) выдаётся немедленно и сразу прописывается в конфиг бота. Прокси при этом отвечает статусом original_key_required на любой реальный запрос через этот pass, пока secret не заполнен.

  2. get_manual_secret_setup — агент получает ссылку и передаёт её человеку (в чат, в тикет, куда угодно).

  3. Человек открывает панель и один раз вводит реальный токен бота.

  4. Pass активируется автоматически, без дополнительного вызова со стороны агента. Бот начинает получать трафик.

За весь цикл значение токена ни разу не попадает в контекст модели — оно проходит только через панель.

Что остаётся человеку

Разграничение по интерфейсам, а не по правилам:

  • Панель — единственное место ввода значения секрета (первичная настройка и pending secret), просмотр секретов по метаданным, список пропусков и лог запросов (метаданные, без заголовков авторизации и без ключей), включение опционального логирования тел запросов.

  • MCP — весь жизненный цикл pass (создание, ротация, отзыв, обновление лимитов), просмотр статистики и логов, ссылка на форму ввода секрета.

Ограничение hosted-модели

Прокси не может быть zero-knowledge структурно: подставить ключ в исходящий запрос можно только расшифровав его — то есть в какой-то момент plaintext существует в памяти процесса. Процесс с полным доступом (а значит и оператор сервиса) теоретически может получить это значение. Это архитектурное свойство любого hosted credential-прокси, а не специфика конкретной реализации. Развёрнутый разбор, что шифрование покрывает (утечка БД, бэкапа, логов) и что нет (полная компрометация сервера), — на странице proxykey.org/en/security. MCP-доступ агента не расширяет эту поверхность — он строже, чем доступ человека через панель, поскольку не может читать значения секретов вовсе.

Крипто-модуль, который шифрует secrets на диске, опубликован отдельно и с тестами: proxykey-crypto — envelope encryption, AES-256-GCM, KEK из переменной окружения, зануление DEK-буферов после использования. В README раздел design notes отвечает на типовые вопросы ревьюеров (границы для случайного nonce GCM, почему обёрнутый DEK не привязан к AAD и так далее).

Когда паттерн не подходит

  • Один ключ используется в одном полностью контролируемом месте, и ни один агент к нему не обращается — прокси добавляет точку отказа без выгоды.

  • Threat model не допускает третью сторону в цепочке запроса вообще — прокси видит трафик, пусть и логирует только метаданные; в этом случае разумнее собственный инстанс паттерна.

  • Критична задержка на уровне единиц миллисекунд — лишний хоп и обращение к кэшу валидации добавляют накладные расходы; для LLM-вызовов это не заметно на фоне генерации, для части низколатентных не-LLM API может иметь значение.

Итог

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