Границы безопасности MCP stdio для локальных ИИ-агентов
Безопасность MCP stdio для локальных ИИ-агентов: отделяйте контекст инструментов от действий с учетными данными, используйте подтверждения, контроль SSH и защищенные от изменений журналы аудита.

Локальные MCP-серверы часто считают безобидными, потому что они работают через stdio на том же Mac, что и агент. Такой вывод перестает работать, как только сервер может вызвать внешний API, открыть SSH-подключение, прочитать файл с учетными данными или запустить команду оболочки с полномочиями разработчика. Локальный транспорт убирает один сетевой переход. Он не уменьшает полномочия процесса, который получает запросы.
Важна простая граница: MCP-сервер должен предоставлять контекст и узкие вычислительные операции, а доверенный шлюз действий должен хранить учетные данные и выполнять действия, которые выходят за пределы компьютера. Если смешать эти роли, языковая модель получит удобный путь от недоверенных инструкций к долгосрочным полномочиям. Я видел, как эта ошибка начиналась с аккуратного инструмента в одном файле, а затем превращалась в набор токенов, вызовов подпроцессов и исключений, которые во время инцидента никто не мог объяснить.
Stdio это транспорт, а не решение о доверии
Безопасность MCP stdio начинается с понимания того, что стандартный ввод и вывод не подтверждают намерения. MCP-клиент запускает сервер и обменивается с ним сообщениями JSON-RPC через каналы. Спецификация Model Context Protocol описывает stdio как транспорт: сервер читает сообщения из стандартного ввода и записывает их в стандартный вывод. В ней не утверждается, что транспорт доказывает безопасность запроса, его одобрение человеком или даже то, что запрос создала ожидаемая модель.
Локальный клиент может напрямую отправить запрос tools/call. Он способен обойти обычный цикл работы модели, повторять вызовы с максимальной скоростью, выбирать аргументы, которые описание инструмента не рекомендовало, и сохранять каждый полученный результат. Если скомпрометированное расширение редактора запускает клиент, сервер не может каким-то чудесным образом понять, что это были не Claude Code или другой ожидаемый вызывающий процесс, если окружающая архитектура не передает ему такую информацию.
Обычное дерево процессов тоже дает меньше изоляции, чем принято считать. Если агент запускает MCP-сервер от вашего имени, сервер обычно наследует идентификатор пользователя, рабочий каталог, окружение, права доступа к файлам, сетевой доступ и любые секреты, оставленные в переменных окружения. Канал не уменьшает эти полномочия.
Считайте каждый вызов инструмента недоверенным запросом от процесса, который убедил модель или выдал себя за нее, чтобы выполнить этот вызов. Это звучит строго, потому что так и есть. Зато такое допущение выдерживает prompt injection, ошибки клиентов, скопированные конфигурации и разработчика, который проверяет запрос с помощью обычного скрипта JSON-RPC.
Сервер должен остановиться до получения многоразовых полномочий
MCP-сервер должен завершать работу до того, как ему понадобится раскрывать или обрабатывать многоразовые учетные данные. Его подходящие задачи включают поиск по индексированному репозиторию, разбор вывода сборки, форматирование данных, чтение специально открытого файла проекта и подготовку команды для проверки. Такие задачи тоже могут причинить вред при плохой реализации, но для них не нужен секрет, который останется полезным после окончания сеанса.
Внешними действиями должен заниматься другой компонент. Аутентифицированный API-запрос, SSH-подключение, публикация пакета, запрос к рабочей базе или обновление задачи связывает недоверенные входные данные с идентификатором, за которым стоят реальные последствия. Поместите учетные данные в компонент, который сам выполняет запрос, а MCP-серверу или агенту возвращайте ограниченный результат.
Это различие часто размывается, потому что оба компонента могут быть локальными исполняемыми файлами. Но они не взаимозаменяемы:
- MCP-сервер переводит запрос агента в ограниченную операцию или запрос на выполнение операции.
- Шлюз действий хранит учетные данные, решает, может ли этот процесс ими воспользоваться, выполняет внешний вызов и записывает результат.
- Агент получает результат, а не средства для повторения аутентифицированного действия в обход шлюза.
Не отправляйте API_TOKEN=... в результат инструмента. Не отправляйте ссылку на хранилище, называя это безопасным решением. Не открывайте команду, которая выводит закрытый ключ в stdout, и не рассчитывайте, что модель отведет взгляд. После того как клиент получил секрет, все последующие меры контроля носят рекомендательный характер.
Шлюз тоже не должен превращаться в универсальный API для оболочки. run(command) кажется удобным сокращением, потому что избавляет от проектирования инструментов. Но такой подход передает разбор аргументов, доступ к файлам, сетевые адреса и часто доступ к секретам одной непрозрачной строке. Создавайте узкие действия: get_deployment_status, create_issue, run_readonly_query или ssh_exec с именованным хостом и ограниченным набором команд. Узкие действия позволяют проводить проверку и ревью.
Схемы инструментов описывают вызовы, но не ограничивают их
JSON Schema для инструмента полезна для проверки входных данных, но не является авторизацией. Спецификация MCP требует, чтобы инструменты публиковали схемы входных данных, а клиенты могут использовать их для формирования вызовов. При этом модель все равно может выбрать любое значение, допустимое схемой. Хуже того, небрежные реализации часто принимают строку, подходящую под схему, а затем вставляют ее в команду оболочки или URL, где она получает другое значение.
Рассмотрим инструмент, предназначенный для получения статуса развертывания:
{
"name": "deployment_status",
"inputSchema": {
"type": "object",
"properties": {
"environment": {"enum": ["staging", "production"]},
"service": {"type": "string", "pattern": "^[a-z0-9-]{1,48}$"}
},
"required": ["environment", "service"],
"additionalProperties": false
}
}
Эта схема запрещает неожиданные поля верхнего уровня и отклоняет очевидные символы оболочки в service. Но она не дает вызывающей стороне права проверять рабочую среду, не доказывает, что service относится к текущему репозиторию, и не ограничивает адрес HTTP после того, как сервер сформировал URL. Валидатор схемы отвечает на вопрос «Правильно ли это сформировано?». Авторизация отвечает на вопрос «Может ли этот вызывающий процесс выполнить это действие сейчас с этим идентификатором?». Разделяйте эти вопросы в коде и при ревью.
Плохая реализация часто выглядит так:
subprocess.run(
f"ssh {host} systemctl status {service}",
shell=True,
check=True,
)
Даже если host и service прошли нестрогую проверку схемы, разбор командой оболочки создает другой язык с другой поверхностью атаки. Используйте массивы аргументов, отклоняйте неизвестные хосты до открытия подключения и позволяйте шлюзу выбирать учетные данные по фиксированному идентификатору, а не принимать путь или имя токена от агента.
Для HTTP разбирайте URL до подключения, требуйте https, сравнивайте нормализованное имя хоста с точным списком разрешенных хостов, а перенаправления отключайте или проверяйте повторно. Перенаправление с разрешенного хоста на внутренний адрес или на адрес под контролем злоумышленника может превратить безобидный на вид запрос в утечку учетных данных. Не полагайтесь на проверку префикса вроде url.startswith("https://api.example.com"): пользовательская информация, порты и похожие имена хостов делают строковые проверки ненадежными.
Идентификатор процесса должен быть виден в момент подтверждения
Кнопка подтверждения помогает только тогда, когда показывает, кто запрашивает полномочия и что именно они разрешают. Формулировка «Разрешить доступ агенту» слаба: она скрывает исполняемый файл, который получит разрешение, и срок его действия. Такая формулировка приучает подтверждать расплывчатую категорию действий.
Лучше определять вызывающий процесс по полномочиям подписи кода, связи с родительским процессом, пути к исполняемому файлу и сроку жизни процесса. Тогда человек сможет одобрить один запуск известного клиента, а не навсегда разрешить действия некоему ярлыку. После завершения процесса его разрешение должно закончиться. Новый процесс требует нового решения.
Подпись кода не доказывает, что каждый prompt или плагин внутри клиента безопасен. Она отвечает на более узкий, но все равно полезный вопрос: какой подписанный исполняемый файл запросил полномочия? Это различие важно, когда вредоносная или измененная локальная программа пытается использовать знакомое имя. В macOS есть сведения о подписи кода, которые шлюз может показать до разрешения действия процесса.
Sallyport использует идентификатор процесса для авторизации на время сеанса, а его заблокированное хранилище отклоняет каждое действие, пока пользователь не откроет его с помощью аппаратных средств защиты Mac. Эта модель намеренно проста: заблокированное хранилище, подтверждение нового процесса и, при необходимости, отдельное подтверждение каждого использования конкретных учетных данных.
Не пытайтесь решить проблему усталости от подтверждений сложной политикой на естественном языке. Люди не могут надежно оценивать плотный набор правил после того, как в нем появляются десятки исключений. Оставьте небольшое число решений, которые соответствуют тому, что разработчик может увидеть: доступны ли секреты, какой процесс может действовать в этом запуске и какие учетные данные требуют нового подтверждения при каждом использовании.
Объем подтверждения должен соответствовать возможному ущербу
Одного подтверждения на сеанс достаточно для повторяющихся действий с небольшим риском, например чтения трекера задач или проверки состояния сервиса разработки. Но оно становится опасным, если то же подтверждение незаметно включает разрушительные изменения в базе данных, публикацию пакета, перевод денег, сообщения клиентам или SSH-доступ к рабочей среде.
Для каждой учетной записи задайте собственную чувствительность подтверждения. Токен только для чтения может работать после одобрения процесса на сеанс. Токен для записи в рабочую среду или SSH-ключ должны требовать подтверждения при каждом использовании. Шлюз обязан показать достаточно контекста для оценки действия: идентификатор учетных данных, целевой хост или сервис, метод или класс команды и очищенные аргументы. Сам секрет показывать нельзя.
Подтверждение должно разрешать конкретный запрос, а не обещание, что агент будет вести себя правильно позже. Если вызов инструмента содержит POST /releases, окно подтверждения не должно сводить его к формулировке «использовать API релизов». Метод, конечный адрес и название операции отличают безобидное чтение от необратимой записи.
Популярная альтернатива это широкий список разрешений: одобрить домен, бинарный файл оболочки или агента на весь рабочий день. Это кажется эффективным, пока внедренная инструкция не направит ту же разрешенную возможность на другой репозиторий, конечную точку или набор аргументов. Широкие разрешения уменьшают число прерываний, перенося проверку на момент, когда никто не видит настоящий вызов.
Активно используйте срок действия. Разрешение сеанса должно исчезать вместе с процессом клиента. Решение для одного вызова должно заканчиваться после этой операции. Если позже шлюзу понадобятся более долгие разрешения, явно указывайте их объем и срок, а не позволяйте сохраненному подтверждению выглядеть как постоянное доверие.
Для SSH нужна отдельная граница, а не обход через оболочку
Именно с SSH локальные архитектуры агентов часто теряют дисциплину. У разработчиков уже есть SSH-агент, псевдонимы хостов, переадресованные ключи и привычка вводить произвольные команды в терминале. Возникает соблазн позволить MCP-серверу вызвать ssh с существующим окружением пользователя. В результате агент получает доступ ко всем идентификаторам и правилам хостов, доступным из вашей оболочки.
OpenSSH описывает серьезное ограничение переадресации SSH-агента: удаленный пользователь, получивший доступ к переадресованному сокету агента, может запрашивать операции у вашего локального агента, хотя и не может извлечь закрытые ключи. Этого достаточно, чтобы действовать от вашего имени, пока переадресация активна. Автономный агент не должен бездумно выбирать такой путь, потому что он расширяет полномочия за пределы исходного хоста.
Используйте отдельный SSH-идентификатор для работы агента и привяжите его к именованной записи хоста. На сервере ограничьте этот идентификатор подходящими для учетной записи параметрами, например принудительной командой и отключенной переадресацией, если это допускает сценарий. На локальной стороне выбирайте хост и идентификатор из конфигурации, находящейся вне контроля агента. Агент может запросить host: build-staging и ограниченное действие, но не должен передавать произвольное имя хоста, путь к закрытому ключу или -o ProxyCommand=....
Вот минимальная форма запроса, которую может проверить шлюз действий:
{
"action": "ssh_exec",
"host_id": "build-staging",
"command_id": "read_service_status",
"args": {"service": "worker"}
}
Шлюз сопоставляет build-staging с известным хостом, политикой проверки ключа хоста, учетной записью и отдельными учетными данными. read_service_status он сопоставляет с фиксированным массивом аргументов. Он не объединяет эти данные в строку оболочки. Отклоненный запрос должен объяснять причину в журнале, не выводя секретные данные или потенциально опасные входные данные в терминал.
Если нужна произвольная удаленная диагностика, оформите ее как отдельную операцию с повышенными требованиями, подтверждением каждого вызова и очевидными ограничениями вывода. Не прячьте произвольный доступ к оболочке за дружелюбным названием инструмента вроде check_server.
Журнал аудита должен пережить того, кто выполнил действие
Текстовый журнал, который записывает тот же процесс, что выполняет действие, остается доказательством лишь до тех пор, пока процесс не захочет его изменить. Для активности агента нужна запись, позволяющая восстановить весь запуск и каждый внешний вызов, а затем обнаружить удаление или редактирование задним числом.
Записывайте идентификатор процесса, идентификатор сеанса, время, тип действия, идентификатор учетных данных, одобренную цель, форму запроса без секретов, результат подтверждения, статус ответа и категорию ошибки. Разделяйте журнал сеанса и журнал действий. Представление сеанса отвечает на вопрос «Какой запуск агента имел полномочия?». Представление действий отвечает на вопрос «Что он сделал с этими полномочиями?». Не заставляйте расследующего восстанавливать одно по плоскому потоку строк.
Цепочка хешей дает практичную проверку целостности. Для каждой записи вычисляйте дайджест на основе дайджеста предыдущей записи и канонических байтов новой зашифрованной записи. Сохраняйте новый дайджест вместе с записью. Проверяющий сможет обнаружить измененную или удаленную запись в середине, а также изменение порядка без доступа к открытому тексту.
Проверяющий журнал должен работать независимо от агента и не нуждаться в доступе к учетным данным. Интерфейс команды может быть таким простым:
$ sp audit verify
records: 184
first sequence: 1
last sequence: 184
chain: valid
Такой формат дает оператору конкретные данные для тикета или записи об инциденте. Если проверка не пройдена, команда должна указать первую последовательность, на которой нарушилась целостность, и завершиться с ненулевым кодом. Формулировка «Журнал не читается» слишком расплывчата для расследования.
Признаки вмешательства не равны защите от вмешательства. Локальный пользователь с достаточными правами все еще может удалить весь журнал или откатить его хранилище. Это ограничение должно оставаться видимым. Если нужна защита от локального отката, экспортируйте подписанные контрольные точки в отдельную контролируемую систему. Не утверждайте, что локальная цепочка хешей решает задачу, против которой она не предназначена.
Не храните секреты в переменных окружения и результатах инструментов
Переменные окружения удобны для сеанса человека в оболочке, но плохо изолируют секреты автономного агента. Дочерний процесс наследует их по умолчанию. Отладочный журнал может вывести их. Команда, перечисляющая окружение, может вернуть их модели. Секреты неоднократно раскрывались таким образом через отчеты о сбоях, пакеты поддержки, просмотр процессов и скопированные стенограммы терминала.
Файл с учетными данными в рабочем каталоге еще опаснее. Модель может прочитать его, инструмент может загрузить его, команда Git может добавить его в индекс, а сервис индексирования сохранит копию. Перемещение файла в скрытый каталог снижает вероятность случайности, но не меняет границу безопасности.
Храните учетные данные в хранилище, которым управляет шлюз действий. Шлюз выбирает учетные данные на основе фиксированного сопоставления действия и передает их только собственной операции HTTP или SSH. Он возвращает тело ответа после фильтрации, только если его безопасно показывать агенту. Bearer-токен никогда не должен проходить через результат MCP, даже в замаскированном виде: ошибки маскирования становятся постоянной частью стенограммы.
Для HTTP предпочитайте контракт ответа, а не передачу необработанных данных. Действие для получения статуса развертывания может вернуть следующее:
{
"environment": "staging",
"service": "worker",
"state": "healthy",
"revision": "a1b2c3d4"
}
Оно не должно возвращать заголовки ответа, в которых могут находиться идентификаторы сеанса, внутренние сведения о маршрутизации или новый токен. Заранее решите, какие поля нужны агенту. Необработанный прокси-ответ это еще одно сокращение, устранение последствий которого впоследствии обойдется дорого.
Небольшую границу проще поддерживать под давлением
Локальную интеграцию агента можно проверить без языка политик и большой программы безопасности. Начните с перечня действий, а затем отнесите каждое к одной из двух категорий: оно либо читает или вычисляет локальный контекст без многоразовых полномочий, либо выходит во внешний сервис и требует шлюза.
Для каждого внешнего действия запишите фиксированную целевую систему, учетные данные, выбранные шлюзом, точные аргументы, на которые может влиять агент, объем подтверждения и создаваемую запись аудита. Если в какой-либо строке указано «произвольная команда», «любой URL», «токен из окружения» или «агент выбирает учетные данные», граница еще не готова.
Проведите эту проверку отказов до того, как дадите агенту полезный секрет:
- Отправьте допустимый по схеме запрос с неожиданной целью или чрезмерно большим аргументом.
- Повторите ранее одобренный запрос после завершения процесса клиента.
- Попробуйте HTTP-запрос с перенаправлением и SSH-запрос с параметрами переадресации.
- Заблокируйте хранилище и убедитесь, что каждое действие завершается ошибкой до открытия сетевого подключения.
- Измените одну сохраненную запись аудита и проверьте, что команда аудита обнаруживает нарушенную цепочку.
Эти тесты выявляют ошибки архитектуры, которые приятная демонстрация скрывает. Идеальное следование модели инструкциям не является проверкой безопасности.
Самая чистая локальная архитектура оставляет MCP-сервер обычным и легко заменяемым. Пусть он предоставляет полезный контекст и запрашивает узко спроектированные операции. Секреты, подтверждение с учетом процесса, выполнение действий и проверяемую запись поместите за границу действий. Когда кто-то спросит, почему инструмент не может просто получить рабочий токен, ответ должен быть виден в архитектуре: токен никогда не требовался инструменту для выполнения его задачи.
Вопросы и ответы
Безопасен ли MCP stdio только потому, что работает локально?
Нет. stdio дает локальному процессу поток байтов, а не границу безопасности. Клиент запускает сервер, может отправить любой допустимый запрос MCP, и обычно сервер получает тот же доступ на уровне пользователя, что и запустивший его процесс.
Что разрешить MCP-серверу?
Используйте MCP-сервер для локального контекста, предсказуемых преобразований и узких возможностей, которым не нужны привилегированные учетные данные. Аутентифицированные HTTP-запросы, SSH-подключения, платежи, развертывания и другие внешние действия с побочными эффектами вынесите в отдельный шлюз действий.
Должен ли ИИ-агент когда-либо получать API-ключ через MCP?
Агент должен получать только результат, необходимый для задачи: например, статус, выбранные поля или вывод команды. Он не должен получать многоразовый токен, закрытый ключ, ссылку на учетные данные или файл конфигурации, с помощью которого их можно получить позже.
Являются ли описания инструментов MCP политикой безопасности?
Нет. Описание инструмента помогает модели выбрать инструмент, но не ограничивает вредоносные или ошибочные аргументы. Исполняемая программа должна проверять аргументы и сама обеспечивать реальную границу полномочий.
Когда нужно требовать подтверждение каждого действия агента?
Подтверждение на уровне процесса полезно, если оно указывает исполняемый файл, который получит полномочия, и заканчивается вместе с процессом. Для учетных данных, способных причинить серьезный или необратимый ущерб, такое подтверждение слишком широкое: каждое использование должно требовать отдельного решения.
Как не дать prompt injection изменить вызов инструмента?
Аргументы с именем хоста, URL, путем, веткой, получателем или учетной записью являются входными данными безопасности. Проверяйте их по явным ограничениям, разрешайте пути до проверки, отклоняйте перенаправления за пределы одобренных хостов и записывайте конечный адрес.
Почему переменные окружения плохо подходят для учетных данных агента?
Переменные окружения легко раскрываются через дочерние процессы, диагностику, историю оболочки, отчеты о сбоях и случайный вывод команд. Локальный брокер учетных данных уменьшает риск: секрет остается в отдельном процессе, который сам выполняет аутентифицированную операцию.
Что должен содержать журнал действий ИИ-агента?
Полезная запись связывает каждый запрос с вызывающим процессом, именем инструмента, одобренными полномочиями, очищенными аргументами, статусом результата и временем. Журнал только для добавления с защитой от изменений надежнее текстового файла, который может отредактировать тот же пользователь или процесс.
Безопасна ли переадресация SSH-агента для агентов программирования?
Нет. Перенаправление SSH и переадресация SSH-агента могут позволить удаленному хосту запрашивать подписи через ваш локальный агент. Используйте отдельный ключ с узкими ограничениями на сервере либо поручите шлюзу выполнять SSH-операцию и запрашивайте подтверждение в момент использования.
Нужен ли шлюз действий для каждого локального инструмента ИИ?
Локальный шлюз действий особенно полезен, когда агенту нужны реальные внешние полномочия, но он не должен хранить многоразовые секреты. Для форматировщика без прав на чтение и записи или инструмента поиска по репозиторию без учетных данных и доступа к внешним сервисам он не нужен.