- BrainTools - https://www.braintools.ru -

Read‑only by construction: почему инструкции — не граница безопасности для AI‑агента в Kubernetes‑кластере

Всё чаще встречаю такую схему: берём LLM, даём ей доступ к kubectl или k8s API, в system prompt или подключённом skill‑е пишем что‑то вроде «ты можешь только читать, ничего не удаляй и не изменяй», и считаем вопрос закрытым. Прошёл через это сам и в какой‑то момент понял, что это не граница безопасности, а вежливая просьба.

Это не гипотетический риск: вы наверняка помните, как в июле 2025 агент Replit удалил базу данных SaaStr, несмотря на прямой запрет что‑либо менять — не Kubernetes и не MCP, но паттерн тот же самый. Инструкция «ничего не трогай» была прямо в контексте, исполнять её было просто некому, кроме самой модели. Дать агенту доступ на запись к k8s‑кластеру — значит собрать ровно ту же конструкцию, которая уже стоила SaaStr их базы.

Я далеко не первый, кто освещает эту тему, и за последнее время появилось множество read‑only MCP‑серверов. Но удивляет, насколько часто в них «read‑only» понимают неправильно. Например, у MCP‑серверов для Kubernetes “read‑only” нередко реализован как переменная окружения, которая фильтрует ответ tools/list, а не отсутствие функции в реестре. Именно так был устроен mcp‑server‑kubernetes (20 тысяч скачиваний в неделю на npm): флаг ALLOW_ONLY_READONLY_TOOLS прятал mutating‑инструменты из списка, а tools/call всё равно принимал kubectl_delete напрямую, в обход фильтра.

Получилась CVE-2026-46519, CVSS 8.8 — тот же принцип, о котором эта статья, доведённый до реального эксплойта: спрятанная из списка функция — не то же самое, что несуществующая. Причём это не только у сообщества — Azure/mcp‑kubernetes, официальный MCP‑сервер Microsoft для Kubernetes, устроен точно так же: --access-level readonly|readwrite вместо отсутствия mutating‑инструментов в принципе.

Почему инструкции не работают как ограничение

Модель — не песочница. Если у неё в списке доступных tool‑ов есть delete_pod или scale_deployment, она технически может его вызвать вне зависимости от того, что написано в system prompt. Например, атакующему для этого не нужен доступ к кластеру — достаточно обычного HTTP‑запроса с подставным значением заголовка вроде User‑Agent. Nginx или сам под залогируют его как есть: Kubernetes просто захватывает stdout/stderr контейнера, без всякой санитизации.

Дальше кто‑то (или сам ассистент) попросит модель «проверь логи этого пода» — совершенно безобидная просьба, — и модель прочитает эту строку как часть контекста, не отличая её от system prompt. Та же история с инструкцией из подключённого skill‑а или другого плагина, которая точно так же оказывается в контексте как «доверенная»; с jailbreak‑ом; с обычной галлюцинацией в попытке «исправить» проблему, которую вы просто попросили объяснить. Инструкция — что в system prompt, что в skill‑е, что в логе контейнера — это данные, которые модель интерпретирует, а не код, который её ограничивает.

Значит, единственная граница, которая реально держит — это то, какие tool‑ы вообще существуют в реестре, который ей доступен. Если функции delete_pod не существует — не важно, что скажет prompt injection, jailbreak или сама модель в порыве «помочь»: вызывать нечего.

Как это выглядит в реестре инструментов

Возьмём конкретный пример: MCP‑сервер для Kubernetes. В его реестре имеет смысл регистрировать только read‑инструменты — list_pods, list_deployments, get_yaml, get_events, read_pod_logs, start_pod_log_stream и так далее, порядка тридцати штук. И ни одного delete_*, scale_*, exec_*, apply_* или port_forward_* — не потому что они выключены каким‑то флагом, а потому что таких функций в коде просто нет.

Все mutating‑операции — scale, rollout restart, delete, cordon/drain — в таком дизайне живут в отдельном, человеческом пути: через GUI с диалогом подтверждения, через CLI с явным флагом или запросом «y/n», неважно — важно, что подтверждает его человек, и только тогда идёт прямой вызов Kubernetes API, без модели и без tool registry ИИ вообще. Это две разные кодовые дорожки, а не одна с флагом «разрешено/запрещено».

Один сервер, два транспорта

Отдельная проблема возникает, когда ИИ‑ассистент доступен в двух видах: как встроенная панель внутри более крупного инструмента и как отдельный бинарник для внешних MCP‑клиентов (Claude Desktop, Claude Code и так далее). Соблазн — собрать для встроенного варианта свой набор инструментов на скорую руку. Так наборы со временем расходятся, и в одном случайно оказывается лишний tool, которого нет в другом.

Надёжнее — поднимать один и тот же MCP‑сервер в обоих случаях и говорить с ним по‑настоящему через MCP‑протокол, просто по разным транспортам: in‑memory для встроенного варианта, stdio — для внешнего клиента:

server, shutdown := mcpserver.NewServer(ctx)
serverTransport, clientTransport := mcp.NewInMemoryTransports()
server.Connect(ctx, serverTransport, nil)

client := mcp.NewClient(&mcp.Implementation{Name: "desktop-assistant"}, nil)
session, _ := client.Connect(ctx, clientTransport, nil)
list, _ := session.ListTools(ctx, nil) // тот же ListTools, что вызовет любой внешний MCP-клиент

То есть в кодовой базе физически один сервер и один список read‑only инструментов — не оригинал и отдельная копия для GUI, про которую кто‑то забудет. На практике это закрывает ровно один класс бага: когда через полгода рефакторинга tool добавили в один список и забыли про второй.

Read‑only — не значит «ничего не видно»

Read‑only решает проблему мутации состояния, но не проблему утечки данных, которые и так лежат в кластере. Если у kubeconfig есть доступ читать Secret в namespace — то и модель, вызывая get_yaml, теоретически тоже. Бороться с этим нужно на уровне данных, а не на уровне промпта: значения Secret редактируются ещё до того, как YAML попадает в tool response —

sec.Data[k] = []byte("<redacted>")

— и ИИ (что встроенный ассистент, что MCP‑клиент) физически никогда не видит расшифрованное значение, потому что оно заменяется на плейсхолдер раньше, чем строка YAML вообще формируется. В таком дизайне имеет смысл ещё и отклонять apply, если в YAML остался <redacted>, — иначе плейсхолдер может случайно затереть реальное значение.

Что этот подход не решает

  • Модель по‑прежнему может прочитать много данных, до которых у вас и так есть доступ по RBAC. Read‑only ограничивает, что можно сделать, а не что можно увидеть в рамках тех же прав.

  • Нагрузку на API‑сервер от болтливого цикла tool‑calling read‑only сам по себе тоже не ограничивает — здесь просто разумные меры (лимит на число итераций в диалоге, лимит байт на результат tool‑а, таймауты на чтение логов, capped и idle‑reaped стримы), а не криптографическая гарантия.

  • Если важно, чтобы промпт и то, что читают tool‑ы, вообще не покидало машину — это отдельная настройка (локальная модель через Ollama/vLLM/LM Studio), а не следствие read‑only архитектуры самой по себе.

  • Скомпрометированный реестр или подменённые описания tool‑ов — отдельная история: tool poisoning работает и против read‑only tool‑ов, если модель доверяет инструкции внутри описания так же, как system prompt. Список из тридцати read‑инструментов сам по себе не гарантирует, что каждый делает ровно то, что в нём написано — подробный разбор этой темы: «MCP и безопасность агентов» [1].

Весь описанный подход простой именно потому, что сознательно не решает более общую задачу — дать агенту хоть какой‑то write‑доступ. Если он реально нужен (например, для production‑дебага с возможностью что‑то поправить), то это принципиально другая, гораздо более тяжёлая архитектура: whitelisting конкретных команд, rate‑limiting, ролевые ограничения, immutable audit log с алертами. Хороший разбор именно такого случая — «Как дать ИИ‑агенту доступ к проду и не поседеть» [2]: там read‑only — только один из семи слоёв защиты, потому что задача изначально шире.

Почему это не завязано на конкретную модель

Раз граница — это отсутствие функции, а не поведение [3] модели, гарантия работает одинаково вне зависимости от движка — Anthropic API, существующий логин в Claude Code/Codex CLI, локальная модель через Ollama. Можно вообще не доверять alignment конкретного вендора: реестр tool‑ов один и тот же для всех, кто бы к нему ни подключился.


Если хочется посмотреть на конкретную Go‑реализацию — она в открытом доступе: я добавил MCP‑сервер к инструменту, который изначально делал для себя, а потом выложил в опенсорс. Репозиторий: https://github.com/cyb3rKn1ght/nereida [4]cmd/nereida-mcp — отдельный README про сам MCP‑сервер).

Автор: elcon

Источник [5]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/34533

URLs in this post:

[1] «MCP и безопасность агентов»: https://habr.com/ru/articles/1055316/

[2] «Как дать ИИ‑агенту доступ к проду и не поседеть»: https://habr.com/ru/articles/1049782/

[3] поведение: http://www.braintools.ru/article/9372

[4] https://github.com/cyb3rKn1ght/nereida: https://github.com/cyb3rKn1ght/nereida

[5] Источник: https://habr.com/ru/articles/1071528/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1071528

www.BrainTools.ru

Rambler's Top100