Безопасность MCP
Минимальные привилегии для MCP-сервера, работающего с внутренними системами
Одна техническая учётная запись с правами на чтение, изменение и удаление превращает ошибочный вызов модели или промпт-инъекцию в доступ ко всей интеграции. Исправление начинается не с системного промпта, а с разделения полномочий на уровне идентификационных данных и целевой системы.
Что получится
Вы построите отдельный контур полномочий для каждого класса MCP-действий, свяжете инструменты с узкими учётными данными и проверите разрешённые и запрещённые операции фактическими запросами. Центральный принцип — минимальные привилегии: компонент получает только те права, ресурсы и срок доступа, которые необходимы для конкретной задачи.
Примеры ниже условны: имена инструментов, ролей, URL и ресурсов нужно заменить значениями вашей системы. В статье не используются реальные клиенты, секреты или результаты тестов.
Почему списка разрешённых инструментов недостаточно
Предположим, MCP-сервер публикует три инструмента: поиск заявок, добавление комментария и закрытие заявки. Если все обработчики используют один токен с административными правами, реальная граница доступа проходит не по схемам инструментов, а по возможностям токена.
Проверка имени инструмента защищает только диспетчеризацию внутри сервера. Она не помогает, если обработчик ошибся в маршруте, параметры запроса были подменены, библиотека допускает произвольный URL или уязвимость позволяет вызвать внутренний клиент напрямую. Надёжная конструкция требует двух независимых ограничений:
- MCP-сервер разрешает инструменту только ожидаемое действие и валидирует его аргументы.
- Целевая система отклоняет всё, что запрещено роли, связанной именно с этим инструментом.
Шаг 1. Постройте матрицу действий
Начните не с ролей, а с операций. Для каждого инструмента укажите метод, точный набор ресурсов, допустимый эффект и уровень риска.
| Инструмент | Действие | Ресурс | Нужное право | Не должно быть доступно |
|---|---|---|---|---|
ticket_search |
Чтение списка | Заявки своей очереди | tickets:read |
Комментарии, изменение, удаление |
ticket_comment |
Добавление комментария | Заявки своей очереди | tickets:read, comments:create |
Смена статуса, удаление |
ticket_close |
Смена статуса | Заявки своей очереди | tickets:read, status:close |
Удаление, переназначение, администрирование |
Не объединяйте права только потому, что инструменты обращаются к одному API. Если действие имеет отдельный побочный эффект, ему нужна отдельная роль или отдельная проверяемая комбинация роли и ограниченной политики.
Шаг 2. Выдайте отдельные идентичности
Создайте по одной технической идентичности для чтения, комментирования и закрытия. Конкретный синтаксис зависит от вашей IAM-системы; следующий YAML — пример декларативного описания желаемых ролей, а не универсальная команда:
roles:
mcp_ticket_reader:
allow:
- action: tickets.read
resource: "queue:support/*"
mcp_ticket_commenter:
allow:
- action: tickets.read
resource: "queue:support/*"
- action: comments.create
resource: "queue:support/*"
mcp_ticket_closer:
allow:
- action: tickets.read
resource: "queue:support/*"
- action: tickets.close
resource: "queue:support/*"
Не добавляйте неявные универсальные права вроде tickets:*. Ограничивайте не только действие, но и область ресурсов: очередь, проект, подразделение, среду и, если система позволяет, отдельные поля. Для продакшена и тестовой среды используйте разные идентичности.
Шаг 3. Привяжите инструмент к своим учётным данным
Секрет выбирает сервер, а не модель. Аргументы инструмента не должны содержать имя профиля, токен, роль, URL внутреннего API или произвольный HTTP-метод.
# Пример конфигурации MCP-сервера
tools:
ticket_search:
handler: ticket_search
credential_ref: mcp/ticket-reader
upstream: ticket_api
allowed_operation: list_tickets
ticket_comment:
handler: ticket_comment
credential_ref: mcp/ticket-commenter
upstream: ticket_api
allowed_operation: create_comment
ticket_close:
handler: ticket_close
credential_ref: mcp/ticket-closer
upstream: ticket_api
allowed_operation: close_ticket
credential_ref здесь обозначает ссылку на секрет в вашем хранилище, а не сам секрет. MCP-серверу следует получать значение во время выполнения, не записывать его в журнал и не возвращать в ответе инструмента.
Маршрутизация должна быть статической:
const toolPolicies = Object.freeze({
ticket_search: {
credential: "mcp/ticket-reader",
operation: "list_tickets"
},
ticket_comment: {
credential: "mcp/ticket-commenter",
operation: "create_comment"
},
ticket_close: {
credential: "mcp/ticket-closer",
operation: "close_ticket"
}
});
Это пример структуры. В рабочей реализации дополнительно проверяйте схему аргументов, длину текста, допустимые идентификаторы, принадлежность заявки разрешённой очереди и отсутствие лишних полей.
Шаг 4. Ограничьте транспорт и адрес назначения
Даже узкая роль не должна использоваться как универсальный HTTP-клиент. Для каждого upstream зафиксируйте базовый адрес, разрешённые маршруты и методы. Не принимайте полный URL от модели и не следуйте перенаправлениям на неизвестные узлы.
# Пример политики исходящих запросов
upstreams:
ticket_api:
base_url: "https://tickets.internal.example"
redirects: deny
routes:
list_tickets:
method: GET
path: "/api/v1/tickets"
create_comment:
method: POST
path: "/api/v1/tickets/{ticket_id}/comments"
close_ticket:
method: POST
path: "/api/v1/tickets/{ticket_id}/close"
Адрес tickets.internal.example является демонстрационным. Подставьте контролируемое внутреннее имя и обеспечьте сетевое правило, разрешающее процессу доступ только к необходимым сервисам.
Шаг 5. Проверьте фактические права
Проверка конфигурации не доказывает, что целевая система применяет её правильно. Выполните положительную и отрицательную проверку каждой идентичности напрямую против тестовой среды или специально выделенного тестового ресурса.
Сначала подготовьте переменные локально. Не вставляйте токены в историю командной оболочки. Следующий фрагмент — безопасный шаблон: значение читается без отображения и существует только в текущем процессе оболочки.
read -r -s READER_TOKEN
export READER_TOKEN
printf '\nТокен загружен во временную переменную текущей оболочки.\n'
Положительная проверка читателя:
curl --fail-with-body --silent --show-error \
--request GET \
--header "Authorization: Bearer ${READER_TOKEN}" \
--header "Accept: application/json" \
"https://tickets.internal.example/api/v1/tickets?limit=1"
Отрицательная проверка той же идентичности должна использовать тестовую заявку и заведомо безопасный текст. Ожидаемый результат определяется контрактом вашего API: обычно это отказ авторизации, но конкретный код заранее не предполагается.
TEST_TICKET_ID="REPLACE_WITH_TEST_TICKET_ID"
curl --silent --show-error \
--output /tmp/mcp-denied-response.txt \
--write-out 'HTTP %{http_code}\n' \
--request POST \
--header "Authorization: Bearer ${READER_TOKEN}" \
--header "Content-Type: application/json" \
--data '{"text":"least-privilege verification"}' \
"https://tickets.internal.example/api/v1/tickets/${TEST_TICKET_ID}/comments"
Не запускайте отрицательные проверки на реальной заявке: ошибочная политика может разрешить действие. После проверки удалите временный ответ и переменную:
rm -- /tmp/mcp-denied-response.txt
unset READER_TOKEN TEST_TICKET_ID
Повторите процедуру для каждой роли. Зафиксируйте результат в матрице:
| Идентичность | Чтение | Комментарий | Закрытие | Удаление |
|---|---|---|---|---|
| reader | Разрешено | Запрещено | Запрещено | Запрещено |
| commenter | Разрешено | Разрешено | Запрещено | Запрещено |
| closer | Разрешено | Запрещено | Разрешено | Запрещено |
Таблица показывает ожидаемую политику примера, а не результаты реальной проверки. В вашем отчёте храните время проверки, идентификатор роли, операцию, ресурс, наблюдаемый статус и корреляционный идентификатор — без токенов и содержимого чувствительных объектов.
Шаг 6. Проверьте путь через MCP
После прямой проверки API вызовите каждый MCP-инструмент в тестовом контуре. Сопоставьте запись MCP-сервера с аудитом целевой системы по корреляционному идентификатору. Так можно доказать, что ticket_search действительно использовал роль читателя, а не резервную административную учётную запись.
Минимальная запись аудита должна содержать:
- имя инструмента и версию его схемы;
- нечувствительный идентификатор технической роли;
- тип операции и нормализованный идентификатор ресурса;
- решение политики: разрешено или отклонено;
- корреляционный идентификатор и время;
- результат upstream без секретов и полного тела ответа.
Не журналируйте токены, заголовок Authorization, полные промпты и содержимое внутренних документов по умолчанию.
Типовые ошибки
Одна роль на весь MCP-сервер
Разделение обработчиков не создаёт границу безопасности, если они используют одинаковые учётные данные. Делите идентичности по эффектам операций.
Проверяется только успешный сценарий
Успешное чтение подтверждает наличие права на чтение, но ничего не говорит об отсутствии права на удаление. Для каждого разрешения нужна хотя бы одна отрицательная проверка соседнего, более опасного действия.
Модель выбирает роль или endpoint
Параметры вроде credential_profile, http_method и target_url расширяют управляемую моделью поверхность. Выбирайте их статически по имени инструмента.
Слишком широкая область ресурса
Право comments:create может выглядеть узким, но оставаться опасным, если действует во всех проектах. Ограничивайте одновременно действие и набор ресурсов.
Резервный административный токен
Автоматический fallback при отказе узкой роли уничтожает модель привилегий. Ошибка авторизации должна завершать вызов, а временное повышение прав — проходить отдельный контролируемый процесс.
Секрет попадает в конфигурацию или журнал
Храните ссылки на секреты, маскируйте заголовки и проверяйте трассировку. Ротация не компенсирует постоянную утечку в логи.
Ограничения подхода
Минимальные привилегии уменьшают радиус поражения, но не делают разрешённое действие безопасным. Промпт-инъекция всё ещё может заставить инструмент комментирования отправить нежелательный текст в разрешённую заявку. Для необратимых и чувствительных операций дополнительно нужны подтверждение человеком, лимиты частоты, идемпотентность, ограничения объёма, контекстная авторизация и журналирование.
Разделение токенов также не защищает от компрометации самого MCP-хоста, если процесс способен прочитать все секреты сразу. Для операций высокого риска используйте отдельные процессы или изолированные сервисы, раздельные хранилища секретов и сетевые политики. Если целевая система не поддерживает достаточно узкие роли, поставьте перед ней контролируемый шлюз с фиксированными операциями — но учитывайте, что шлюз становится частью доверенной границы.
Контрольный список
- Каждый инструмент описан как конкретная операция над конкретным ресурсом.
- Чтение, запись и необратимые действия разделены по идентичностям.
- Модель не выбирает токен, роль, HTTP-метод или произвольный URL.
- Целевая система ограничивает и действие, и область ресурсов.
- Для каждой роли выполнены положительные и отрицательные проверки.
- Проверки проходят на выделенных тестовых объектах.
- В аудите можно связать MCP-вызов с фактической ролью upstream.
- Отказ узкой роли не приводит к автоматическому повышению прав.
- Секреты отсутствуют в аргументах инструментов, конфигурации и логах.
- Роли и результаты проверок пересматриваются после изменения инструмента.
Что изучить дальше
Продолжите с другими материалами в разделе практических руководств. Определения терминов, связанных с MCP, авторизацией и агентными системами, собраны в глоссарии.