В июле 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:

plain
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Общее ядро хоста, изоляция держится на политикахБазовая линия для сравнения
gVisorUserspace-ядро между контейнером и хостомВычисления и память почти как в 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 позволяет ограничить ресурсы квотой.

Минимальный набор для одного агента:

plain
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: общий запрет в обе стороны, затем точечные разрешения.

plain
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:

plain
# читать каталог отчётов — можно
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. Перед полноценным стендом хватит трёх коротких проверок:

bash
# 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.

СлойЧто включить
RuntimeRuntimeClass с 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.

Источники