В июле 2025-го агент Replit удалил продакшн-базу SaaStr, хотя его несколько раз просили ничего не менять. В июле 2026-го вышел разбор другого случая: имя, работодатель и город пользователя ушли наружу через Claude без единого клика с его стороны. В обеих историях инструкции в промпте не сработали как граница.
Ниже — как собрать вокруг агента три инфраструктурных слоя в Kubernetes: песочницу исполнения, отдельную идентичность с узкими правами и закрытый egress. Манифесты учебные: имена, версии и лимиты подставьте под своё окружение, а перед продом добавьте таймауты, ретраи и мониторинг.
Почему промпт и LLM-файрвол — не граница
Prompt injection не могут надёжно закрыть уже четыре года, и окончательного решения может не появиться вовсе. В отчёте OWASP State of Agentic AI Security and Governance инъекция связана с шестью из десяти категорий OWASP Top 10 для агентных приложений. Help Net Security, разбирая отчёт, объясняет причину так: нет надёжного способа пометить одни токены как команды, а другие — как данные. Системный промпт, запрос пользователя и внешний текст модель видит единым потоком.
Версия отчёта 2026 года опирается уже не на теоретические угрозы, а на реальные CVE и инциденты. В трекинге OWASP 53 агентных проекта, 28 из них — coding-агенты. Счёт advisories: 57 у n8n, 22 у Claude Code, 15 у AutoGPT.
Есть риск, до которого промпт-фильтр не дотягивается в принципе, — цепочка поставок. В марте 2026-го скомпрометированный пакет LiteLLM скачали почти 47 000 раз за три часа.
Продукты класса LLM Firewall проблему не снимают. Llama Firewall проверяет промпты моделью PromptGuard 2 на 22 млн параметров и статически анализирует сгенерированный код через CodeShield — но фильтрует он только текст и код. LLM в роли охранника наследует уязвимости охраняемой модели, от prompt steering до jailbreak. По оценке Ideco, девять из десяти угроз OWASP для агентных систем классический LLM-файрвол не покрывает.
Полезная рамка — lethal trifecta Саймона Уиллисона: доступ к приватным данным, контакт с недоверенным контентом и канал внешней коммуникации. Агент, в котором сходятся все три свойства, после одной удачной инъекции становится инструментом эксфильтрации. Правило Agents Rule of Two развивает мысль: без человека в контуре агенту оставляют не больше двух свойств из трёх. Это ограничения на доступы и каналы связи — промптом их не выразить.
Для read-only-агента, который только диагностирует кластер, границу можно удержать на API-сервере: хватит ServiceAccount с правами на чтение. Дальше речь об агентах, которые действуют — пишут в базы, вызывают API, применяют манифесты.
Слой 1. Runtime: одна песочница на агента
К похожей архитектуре независимо пришли минимум три команды: Anthropic (sandbox-runtime на bubblewrap и seccomp), Google (пул прогретых песочниц на GKE) и Kubernetes SIG (CRD Sandbox поверх gVisor и Kata). Общий принцип называют containment first: сбой политики не должен выходить за жёстко заданные границы.
Аргумент в пользу такого подхода — возможности самих моделей. Mythos Preview от Anthropic в собственном исследовании безопасности нашла 27-летнюю уязвимость в TCP-стеке OpenBSD примерно за тысячу прогонов и менее чем за $20 000. Перечислять в политиках всё, что запрещено такой системе, поздно — проще сузить поверхность, до которой она дотягивается.
Проект Agent Sandbox вводит CRD Sandbox: один объект — один изолированный под для агента. Изоляция ядра включается одной строкой runtimeClassName:
apiVersion: agents.x-k8s.io/v1beta1
kind: Sandbox
metadata:
name: report-agent
namespace: agents-report
spec:
podTemplate:
metadata:
labels:
app: report-agent # по этой метке под найдут сетевые политики слоя 3
spec:
runtimeClassName: gvisor # вся изоляция ядра — здесь
containers:
- name: agent
image: registry.example.com/agents/report:1.4.2
resources:
requests:
cpu: 300m
memory: 700Mi
limits:
cpu: 700m
memory: 1500MiМетка app: report-agent в podTemplate.metadata.labels понадобится в слое 3: без неё разрешающее сетевое правило не найдёт под агента. Запросы ресурсов подобраны под квоту из слоя 2 — в неё помещаются пять таких подов. Жизненный цикл, хранилище и идентичность Sandbox задаются отдельными полями, полный список — в документации проекта.
gVisor, Kata или обычный runc
gVisor ставит между контейнером и хостом userspace-ядро, которое перехватывает системные вызовы. Если атакующий всё же сбежит из контейнера, он окажется в ядре песочницы, и до хоста останется ещё один барьер. Kata Containers решает ту же задачу аппаратной виртуализацией.
| Рантайм | Граница | Производительность |
|---|---|---|
| runc | Общее ядро хоста, изоляция держится на политиках | Базовая линия для сравнения |
| gVisor | Userspace-ядро между контейнером и хостом | Вычисления и память почти как в runc; системные вызовы, сеть и файлы заметно медленнее |
| Kata Containers | Аппаратная виртуализация | Кандидат для нагрузки с частыми файловыми и сетевыми операциями — сравнивайте на своём трафике |
Если агент в основном считает и ждёт ответа LLM, разница с runc может оказаться несущественной. Публичных замеров gVisor на агентском профиле — длинные паузы на инференс и всплески файловых операций между ними — пока нет ни у одного вендора. Поэтому выбирать лучше на своей нагрузке: один агент, одна задача, по полсотни прогонов в runc, gVisor и Kata. Сравнивайте p95 wall-clock времени задачи и число системных вызовов через strace -c. Если разница тонет в разбросе времени инференса, выбирать не из чего.
Второй фактор — холодный старт. На KubeCon NA 2025 Google показала пул заранее запущенных песочниц: новая среда готова меньше чем за секунду, выигрыш — до 90% относительно холодного старта. Цифры получены в GKE, но сам приём с прогретым пулом от платформы не зависит.
Изоляция окупается и в удобстве. По данным Anthropic, в Claude Code песочница с egress-прокси сократила число запросов на подтверждение действий на 84%: в надёжно изолированной среде агенту можно заранее разрешить больше.
Слой 2. Pod, ServiceAccount и узкая Role
В open-source-проекте kagent эволюция видна наглядно: сначала все агенты жили в одном runtime, затем каждый переехал в отдельный под со своим Service и ServiceAccount. Дело не только в изоляции сбоев. Под даёт агенту идентичность: ServiceAccount становится субъектом RBAC, по меткам пода применяются NetworkPolicy, а namespace позволяет ограничить ресурсы квотой.
Минимальный набор для одного агента:
apiVersion: v1
kind: ServiceAccount
metadata:
name: report-agent
namespace: agents-report
automountServiceAccountToken: false # токен не монтируем; кому нужен API — включаем точечно
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: report-agent-role
namespace: agents-report
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list"] # ровно то, что нужно задаче
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: report-agent-binding
namespace: agents-report
subjects:
- kind: ServiceAccount
name: report-agent
namespace: agents-report
roleRef:
kind: Role
name: report-agent-role
apiGroup: rbac.authorization.k8s.io
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: report-agent-quota
namespace: agents-report
spec:
hard:
requests.cpu: "2"
requests.memory: 4Gi
limits.cpu: "4"
limits.memory: 8Gi
pods: "5" # потолок на спавн подагентовСверим с песочницей из слоя 1: пять подов по 300m CPU и 700Mi в requests дают 1,5 CPU и около 3,4 GiB при потолке 2 CPU и 4Gi, по limits — 3,5 CPU и около 7,3 GiB при потолке 4 и 8Gi. Шестой под квота уже не пропустит.
Две настройки здесь принципиальны.
`automountServiceAccountToken: false`. Если агент не ходит в Kubernetes API, токен ему не нужен, а внутри пода это лишняя цель: после успешной инъекции им можно воспользоваться от имени агента. Role при этом можно оставить как ограничитель на будущее. Когда доступ к API понадобится, токен включают точечно — automountServiceAccountToken: true в спецификации конкретного пода, и исключение сразу видно в манифесте.
Отдельный namespace. Агенту или небольшому пулу агентов лучше выделить свой namespace: по гайду Kubernetes, границы внутри одного namespace слабее, чем между ними. Права выдаются точечно: RoleBinding вместо ClusterRoleBinding, где это возможно, и никаких wildcard-правил.
У схемы «под на агента» есть экономический предел. Сервис живёт днями, агент — секунды или минуты, подагенты появляются волнами прямо во время задачи, и заранее поднятые поды простаивают. Альтернатива из статьи CNCF — agent-substrate: пул worker-подов (WorkerPool), шаблон окружения (ActorTemplate) с образом, закреплённым по sha256, и runtime gVisor, а логические агенты (Actor) запускаются на свободных воркерах. В примере шесть агентов уместились в один worker-под, потому что не работали одновременно. Для прода рано: речь о версии v0.0.6 от одного вендора.
Слой 3. Egress: default-deny и единственный выход
Изолированный агент с минимальными правами остаётся риском, если ему разрешён любой исходящий трафик. В разборе утечки через Claude данные выводили цепочкой внешне безобидных переходов по ссылкам.
Схема стандартная для NetworkPolicy: общий запрет в обе стороны, затем точечные разрешения.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: agents-report
spec:
podSelector: {} # все поды namespace
policyTypes: ["Ingress", "Egress"] # запрещено всё в обе стороны
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: report-agent-egress
namespace: agents-report
spec:
podSelector:
matchLabels:
app: report-agent
policyTypes: ["Egress"]
egress:
- to: # DNS кластера — иначе не заработает ничего
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
- to: # единственный выход наружу — egress-шлюз
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: agent-egress
ports:
- protocol: TCP
port: 3128Разрешающее правило выбирает под по метке app: report-agent. У Sandbox она задаётся в podTemplate.metadata.labels, у Deployment — в template.metadata.labels. Нет метки — правило не выберет ни одного пода, и агент останется под полным default-deny.
DNS — обязательное исключение. Default-deny режет и запросы к kube-dns. Под при этом ни о чём прямо не сообщает: имена перестают резолвиться, а в логах копятся таймауты к LLM API и внутренним сервисам. Поэтому правило на порт 53 в kube-system — не опция.
Почему выход только в шлюз. NetworkPolicy работает с IP-адресами и портами, а агент ходит по HTTPS к доменам, и их набор меняется от задачи к задаче. Поэтому из пода разрешают один выход — к прокси-шлюзу, который разбирает HTTP(S) и применяет правила по доменам. В схеме F5 из материалов CNCF весь исходящий трафик агента идёт через NGINX в роли forward proxy с allow-list доменов, а попытки обойти прокси напрямую режет iptables. Заодно шлюз показывает, куда именно агент ходит наружу, — без такой видимости агентов часто просто не пускают в прод.
Правила на уровне протокола
Allow-list доменов отвечает на вопрос «куда». Если агенту открыта прод-база, этого мало: нужно ограничить, какие запросы он выполняет и с какими объектами работает. Open-source-проект Claw Patrol от Deno разбирает трафик PostgreSQL, ClickHouse, Kubernetes API и HTTPS и применяет к полям запроса allow/deny-правила на CEL. Сами правила пишутся в HCL:
# читать каталог отчётов — можно
rule "reporting_read" {
endpoint = postgres.prod
condition = "sql.verb == 'SELECT' && sql.tables.all(t, t.startsWith('reporting.'))"
verdict = "allow"
}
# запись в прод — только с подтверждением человека
rule "prod_writes" {
endpoint = postgres.prod
condition = "sql.verb in ['INSERT', 'UPDATE', 'DELETE']"
approve = [human_approver.oncall]
reason = "запись в прод-БД — только с подтверждением"
}Эндпойнты вроде postgres.prod и тех, кто может подтверждать запросы, объявляют отдельными блоками в том же файле. Набор полей (sql.verb, sql.tables, k8s.verb, http.path) и точный синтаксис зависят от версии — сверяйтесь с актуальным справочником.
Секреты через агента не проходят: он получает плейсхолдер, а настоящие учётные данные шлюз подставляет при подключении к целевой системе. Извлечь их из памяти, логов или рабочего каталога агента нельзя — их там нет. Политика хранится как код в Git. По словам Deno, инструмент вырос из собственной задачи компании — пускать ops-агентов в прод без постоянных кредов.
Где контур не держит
Чёрные списки обходятся. В обсуждении Claw Patrol на Hacker News заметили: запретите агенту DROP TABLE — он удалит все строки через DELETE и формально правило не нарушит. Read-only-учётка и правила на шлюзе решают разные задачи: учётка задаёт, чего агент не сможет в принципе, шлюз — какие операции требуют подтверждения человека. Если выбирать одно, начинайте с учётки: она проще и не зависит от полноты списка правил.
HTTP-прокси обходится дочерним процессом. Агент может вызвать psql напрямую и не пройти через HTTP-слой. Поэтому шлюз должен понимать сам протокол, а у песочницы нужно убрать прямой сетевой доступ — например, отдельным сетевым namespace, как в sandbox-runtime. Одиночную границу почти всегда можно обойти, слои закрывают обходы друг друга.
Shadow AI вне кластера. Три слоя защищают только агентов, которых видит платформа. По данным OWASP, политику поиска и учёта shadow AI внедрили лишь 37% организаций. Скрипт на ноутбуке аналитика с прод-кредами в переменных окружения под NetworkPolicy не попадёт. Это решается выше манифестов: инвентаризацией агентов, контролируемой выдачей доступов и запретом использовать прод-секреты вне утверждённого контура.
Зависимости. Egress-контур ограничит, куда сможет достучаться скомпрометированный пакет, но не помешает ему попасть в образ. Фиксируйте версии и хеши зависимостей, закрепляйте образ по sha256 и сканируйте его в CI.
Аудит: журнал, который агент не перепишет
Каждый вызов проходит через шлюз и оставляет запись. OTel-модуль NGINX создаёт span на внешний запрос, трассировки уходят в SIEM или Grafana; в Claw Patrol ту же роль играет аудит-лог шлюза. Логам самого приложения доверять нельзя: скомпрометированный агент запишет туда то, что сочтёт нужным, а запись на шлюзе он не контролирует.
Оговорка из материалов CNCF: наблюдаемость должна следовать за логическим агентом, а не за подом. Под временный — он пересоздаётся и может обслуживать несколько задач, поэтому в трассировках и аудит-логах нужен идентификатор агента, а не только имя пода.
Сроки уведомления об инцидентах короткие: DORA отводит четыре часа, RAISE Act — 72. Если агент работает со значимым объектом КИИ, уведомить ГосСОПКА нужно в течение трёх часов, для остальных объектов КИИ — в течение 24 часов. Без журнала на шлюзе за такое время трудно даже восстановить картину. Отдельной категории требований для AI-агентов в России пока нет, но контур ложится на привычные меры для аттестованных систем: сегментацию (NetworkPolicy и шлюз), разграничение доступа (ServiceAccount, Role, песочница) и регистрацию событий (аудит-лог шлюза).
Проверка перед запуском
Стабильные группы networking.k8s.io/v1 и rbac.authorization.k8s.io/v1 есть в любом поддерживаемом кластере, а бета-API Agent Sandbox — только после установки CRD. Перед полноценным стендом хватит трёх коротких проверок:
# 1. Эффективные права ServiceAccount после всех привязок
kubectl auth can-i --list \
--as=system:serviceaccount:agents-report:report-agent
# 2. На поде агента есть метка, домен вне allow-list недоступен
kubectl -n agents-report get pods -l app=report-agent
kubectl -n agents-report exec -it <pod-агента> -- curl -sS -m 5 https://example.com
# 3. В кластере зарегистрирован RuntimeClass для gVisor
kubectl get runtimeclassЕсли can-i показывает лишнее, ищите унаследованные ClusterRole и слишком широкие RoleBinding. Если curl проходит, первым делом проверьте, применяет ли CNI политики и создан ли default-deny-all в этом namespace. Если запрос падает на резолве имени, а не по таймауту соединения, сломано DNS-исключение — проверьте namespaceSelector для kube-system. RuntimeClass лучше найти до того, как прописывать runtimeClassName в манифест.
Минимальный контур для прода
Если получается внедрить только один слой, начинайте с сети. Песочница защищает хост, RBAC — кластер, а из компании данные уходят по сети. Default-deny и egress-шлюз собираются за день: не нужны ни CRD, ни другой runtime — только CNI, который реально применяет NetworkPolicy.
| Слой | Что включить |
|---|---|
| Runtime | RuntimeClass с gVisor или Kata, одна песочница на агента |
| Идентичность | Отдельный под, свой ServiceAccount без автомонтирования токена, узкая Role, namespace с ResourceQuota |
| Сеть | Default-deny для ingress и egress, исключение для DNS, выход только через шлюз с allow-list, правила на уровне протокола для баз и Kubernetes API |
С таким контуром DELETE из истории с Replit упёрся бы в правило шлюза, требующее подтверждения записи, а сама попытка осталась бы в аудит-логе ещё до начала расследования.
FAQ
Если внедрять только один слой, какой выбрать?
Сеть: default-deny для namespace агента и выход только через egress-шлюз с allow-list доменов. Это собирается за день без CRD и нового runtime. Но слои не взаимозаменяемы: RBAC не остановит эксфильтрацию по сети, NetworkPolicy не помешает удалить данные при широких правах, а песочница не заменяет минимум привилегий.
Работает ли это в managed Kubernetes?
RBAC, ServiceAccount и ResourceQuota — стандартные API и работают везде. NetworkPolicy зависит от CNI: убедитесь, что политики в вашем кластере действительно применяются. RuntimeClass для gVisor или Kata одни провайдеры дают как опцию (например, GKE Sandbox на gVisor), у других его ставят сами — проверьте kubectl get runtimeclass. CRD Agent Sandbox — бета-API, его устанавливают и обновляют отдельно.
Как проверить, что egress действительно закрыт?
Зайдите через kubectl exec в под самого агента и выполните curl к домену вне allow-list — запрос не должен пройти. Тестовый под без метки агента для этого не годится: на него действует только общий default-deny, и проверка покажет «закрыто», даже если разрешающее правило для агента слишком широкое.
Зачем egress-шлюз, если есть NetworkPolicy?
NetworkPolicy оперирует IP-адресами и портами, а агент ходит к доменам по HTTPS. Шлюз — например, NGINX в роли forward proxy — применяет allow-list доменов и пишет журнал всех обращений. Шлюз с разбором протоколов, как Claw Patrol, ещё и ограничивает сами операции и подставляет секреты, которых агент не видит.
Не проще ли дать агенту read-only-учётку вместо правил на шлюзе?
Если запись агенту не нужна — да, и начинать стоит именно с неё: учётка не зависит от полноты списка правил. Шлюз нужен там, где писать агенту всё-таки надо, но отдельные операции должны проходить через подтверждение человека. Чёрный список вида «запретить DROP TABLE» сам по себе ненадёжен: агент удалит строки через DELETE.