Ранее уже разбирали, зачем нужен 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 инструментов
Группа | Инструмент | Что делает |
|---|---|---|
Каталог |
| Список провайдеров и модель авторизации каждого |
Каталог |
| Список сохранённых секретов — только метаданные |
Каталог |
| Ссылка для человека на ввод реального ключа |
Пропуски |
| Выпустить pass для уже существующего secret |
Пропуски |
| Выпустить pass до того, как secret заполнен |
Пропуски |
| Изменить лимиты, режим IP-привязки, срок действия |
Пропуски |
| Перевыпустить токен pass с теми же настройками |
Пропуски |
| Мгновенно отозвать / удалить отозванный pass |
Пропуски |
| Сбросить обучение по IP |
Наблюдаемость |
| Все пропуска со статусом, лимитами, привязкой |
Наблюдаемость |
| Лог запросов конкретного pass |
Наблюдаемость |
| Статистика использования pass |
Ни один из 13 методов не принимает и не возвращает значение секрета. Это ограничение контракта API, а не соглашение об использовании: агент физически не может запросить то, чего нет в спецификации инструмента. По той же причине MCP-поверхность не может включить логирование тел запросов — эта настройка тоже осталась только в панели, доступной человеку.
Сценарий pending secret
Рабочий кейс: агент настраивает Telegram-бота — пишет код, конфигурирует вебхук, — но токена от BotFather ещё нет, потому что сам бот ещё не создан.
Порядок действий агента:
create_pending_pass— pass (vlt_…) выдаётся немедленно и сразу прописывается в конфиг бота. Прокси при этом отвечает статусомoriginal_key_requiredна любой реальный запрос через этот pass, пока secret не заполнен.get_manual_secret_setup— агент получает ссылку и передаёт её человеку (в чат, в тикет, куда угодно).Человек открывает панель и один раз вводит реальный токен бота.
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 может иметь значение.
Итог
Разделение по типам операций, а не по доверию к агенту: значение секрета проходит через систему один раз, через человека, и больше никогда не покидает сервер в открытом виде. Агенту достаётся инструмент с намеренно узким контрактом — управление доступом без возможности прочитать то, чем он управляет. Для сценариев, где агент сам разворачивает сервисы и ботов, это закрывает конкретный практический вопрос: что делать с ключом, которого у агента ещё нет.

