Когда агент не только читает кластер, но и пишет в базы, вызывает API и применяет манифесты, вопрос «как его ограничить» перестаёт решаться текстом инструкции. Разберём, почему промпт не может быть границей безопасности и из каких инфраструктурных слоёв собирается контур, который действительно удерживает ущерб.
Почему промпт не является границей
Промпт-фильтр — это тоже LLM, и как регулятор он наследует уязвимости модели, которую охраняет: от prompt steering до jailbreak. Отдельно мешает то, что модель не различает команды и данные: не существует надёжного способа пометить одни токены как команды, а другие — как данные. Системный промпт, запрос пользователя и внешний текст приходят единым потоком, поэтому всё, что попало в контекст, потенциально может управлять агентом.
Отсюда два подхода, которые описывают не текст, а уровни доступа и каналы связи. lethal trifectaподход к оценке риска агента по трём свойствам: доступ к приватным данным, контакт с недоверенным контентом и канал внешней коммуникации; если сходятся все три, агент становится инструментом эксфильтрации после одного внедрённого промпта — агент, в котором сходятся все три свойства, становится инструментом эксфильтрации после одного внедрённого промпта. Agents Rule of Twoправило Meta: если действия агента не подтверждает человек, агенту разрешают иметь максимум два свойства из трёх — без человека в контуре агенту разрешают иметь максимум два свойства из трёх. Оба подхода говорят об уровнях доступа и каналах связи, и промптами такие ограничения не выразить. Для действующих агентов граница опускается на три уровня ниже текста.
Разница видна на двух типах агентов. Диагностическому read-only-агенту достаточно ServiceAccount с правами только на чтение — граница удерживается на API-сервере. Агенту, который пишет в базы, вызывает API и применяет манифесты, этого мало: когда система умеет находить уязвимости, перечислять запреты поздно, остаётся ограничивать поверхность, до которой она может дотянуться. Поэтому границу опускают в runtime, права и сеть.
Три слоя изоляции: общая картина
Контур образуют три инфраструктурных слоя:
- Runtime — песочница исполнения, где агент запускается изолированно от хоста.
- Единица развёртывания — отдельный Pod на агента со своим ServiceAccount без автомонтирования токена, узкой Role и namespace с ResourceQuotaограничение на суммарное потребление ресурсов и число объектов в namespace.
- Сеть — default-denyрежим, при котором весь трафик запрещён, пока его не разрешат отдельным правилом для ingress и egress, выход только через шлюз с allow-list и правила на уровне протокола для доступа к базам и Kubernetes API.
По назначению слои различаются так: песочница защищает хост, RBAC — кластер, но за пределы компании данные уходят по сети. Если из трёх слоёв получается внедрить только один, начинать следует с сети: утечка данных и удаление базы проходят через первые два слоя.
Песочница: изоляция как ограничение ущерба
Формулировка «тысячи рабочих нагрузок используют одно общее ядро без структурной изоляции» описывает ситуацию, когда множество нагрузок выполняется на одном ядре, а разделение обеспечивается только политиками и предположением, что весь код написан без ошибок. Структурной границы между нагрузками нет, поэтому сбой одной может затронуть остальные. Браузеры решили это, разведя вкладки по процессам: сбой одной вкладки перестал затрагивать остальные. Обещание подхода — локализовать сбой, а общий принцип сформулирован как containment firstпринцип, по которому сбой политики не должен выходить за жёстко заданные границы. Под сбоем политики здесь понимается ситуация, когда ошибается или оказывается обойдено правило, ограничивающее нагрузку; жёстко заданными считаются границы, обеспеченные структурно — на уровне ядра или виртуализации, а не только декларативно.
CRD Sandbox
Sandboxобъект Kubernetes, описывающий изолированную среду для запуска рабочей нагрузки. Объект Sandbox описывается в манифесте через apiVersion agents.x-k8s.io/v1beta1 и kind Sandbox. Его spec содержит podTemplate — шаблон пода с контейнерами, который и определяет запускаемую рабочую нагрузку. Созданный Sandbox запускает указанный образ, а доступ к нему возможен по стабильному hostname, равному имени Sandbox. Изоляция ядра включается одной строкой в podTemplate:
runtimeClassName: gvisor # вся изоляция ядра - здесьЦель Sandbox — сильная изоляция, включая изоляцию ядра и сети, что важно для недоверенного кода и многопользовательских сценариев. Для более сложных примеров, включая расширения, есть каталог examples.
gVisor
gVisor ставит между контейнером и хостом userspace-ядро, которое перехватывает системные вызовы. Это ядро называется Sentryuserspace-ядро gVisor, через которое проходят системные вызовы приложения. Его наличие означает, что системные вызовы приложения проходят через дополнительные слои ПО. Если container escape всё же случится, атакующий попадёт в userspace-ядро песочницы, а до хоста останется ещё один барьер. Файловые операции при этом маршрутизируются через Goferкомпонент gVisor, через который проходят файловые операции в соответствии с моделью безопасности.
Kata Containers
Kata Containers решает ту же задачу — изоляцию рабочей нагрузки — но аппаратной виртуализацией: это стандартная реализация лёгких виртуальных машин, которые ощущаются и работают как контейнеры, но дают изоляцию и преимущества безопасности виртуальных машин. Kubernetes SIG развивает CRD Sandbox поверх gVisor и Kata, то есть оба используются как runtime-песочница исполнения в одном слое.
gVisor или Kata: компромисс по производительности
Оверхед gVisor возникает в двух формах: дополнительные циклы и использование памяти. Они могут проявляться как увеличенная задержка, сниженная пропускная способность или плотность, либо не проявляться вовсе. Операции с памятью и выполнение CPU-инструкций идут почти без накладных расходов: gVisor не добавляет затрат на доступ к памяти и не эмулирует выполнение CPU-инструкций. Заметно медленнее оказываются системные вызовы — особенно на платформе ptraceспособе перехвата системных вызовов, при котором gVisor работает через механизм ptrace; существуют и другие платформы, например на аппаратной виртуализации, которая страдает от самых высоких структурных издержек, — а также сеть и файловая система, где преобладают издержки реализации. В redis небольшие операции дают большой оверхед, а более крупные, где больше работы делается в приложении, — меньший относительный. Для файловой системы gVisor вводит небольшие фиксированные накладные расходы на данные, пересекающие границу песочницы, но в большинстве случаев затраты обусловлены реализацией внутренней VFS, которая требует улучшения. Для нагрузки с частой работой с файлами и сетевыми соединениями имеет смысл отдельно протестировать Kata.
Как замерить
Минимальная схема: запустить одного агента на одной задаче в трёх рантаймах — runc, gVisor и Kata — и сделать по полсотни прогонов. Сравнивать стоит p95 wall-clock времени задачи и число системных вызовов через strace -c. Решение принимается по тому, теряется ли разница между runc и gVisor в разбросе времени инференса: если теряется, выбирать уже не из чего.
Прогретый пул
прогретый пул песочницпул заранее запущенных песочниц, из которого новая среда выдаётся уже готовой сокращает холодный старт. По данным Google на KubeCon NA 2025, в такой схеме новая среда готова менее чем за секунду, и компания заявляет до 90% выигрыша по сравнению с холодным стартом. Цифры получены в GKE, но сам принцип не зависит от конкретной платформы. У контроллера есть настройка пополнения прогретого пула — warm-pool refill shapingуправление пополнением прогретого пула.
RBAC: права как второй слой
Минимальный набор прав собирается из ServiceAccount (учётная запись для пода), Role (список разрешённых действий) и RoleBinding (привязка роли к этой учётной записи). В примере ServiceAccount report-agent создаётся с automountServiceAccountToken: false, чтобы токен не монтировался в Pod по умолчанию, а Role report-agent-role разрешает только get и list для configmaps — ровно то, что нужно задаче. RoleBinding связывает ServiceAccount с Role в том же namespace.
Поверх ролей нужна ResourceQuota, потому что она ограничивает потребление ресурсов, а не действия: в примере report-agent-quota задаёт requests.cpu: "2", requests.memory: 4Gi, limits.cpu: "4", limits.memory: 8Gi и pods: "5" — потолок на спавн подагентов. Нагрузка идёт волнами, подагенты появляются прямо во время выполнения задачи, поэтому квота ограничивает, сколько ресурсов и подов агент может потребить.
Проверка эффективных прав делается командой:
kubectl auth can-i --list \
--as=system:serviceaccount:agents-report:report-agentОна покажет реальные права ServiceAccount после применения Role и всех существующих привязок — то, что агент фактически может делать до запуска. Так выявляются лишние права: wildcard-правила и права, выданные через ClusterRoleBinding вместо RoleBinding. Права стоит выдавать точечно — использовать RoleBinding вместо ClusterRoleBinding, где это возможно, и не применять wildcard-правила. Отдельно лишним является смонтированный токен: если агент не работает с Kubernetes API, токен ему не нужен, потому что при успешной инъекции его можно использовать для доступа к API от имени агента.
Egress-контур: сеть как третий слой
Default-deny egress задаётся объектом NetworkPolicyправило кластера, которое разрешает или запрещает сетевой трафик к подам по их меткам с именем default-deny-all, у которого podSelector пустой ({}), то есть выбирает все поды namespace, а policyTypes включает и Ingress, и Egress — так запрещается всё в обе стороны. Чтобы разрешающее правило сработало, Pod агента должен иметь метку app: report-agent; если метки нет, второе правило не выберет ни одного Pod, и для агента останется в силе общий default-deny.
Первое правило блокирует весь исходящий трафик, в том числе запросы к kube-dns: DNS-имена перестают резолвиться, а в логах появляются таймауты при обращении и к LLM API, и к внутренним сервисам. Поэтому в политике report-agent-egress первым исключением идёт DNS кластера — UDP и TCP порт 53 в namespace с меткой kubernetes.io/metadata.name: kube-system, и это не факультативно, а обязательно. Вторым и единственным разрешённым выходом остаётся egress-шлюз — TCP порт 3128 в namespace с меткой kubernetes.io/metadata.name: agent-egress. Причина в том, что NetworkPolicy умеет ограничивать трафик только по IP-адресам и портам, а агент ходит по HTTPS к доменам, набор которых может меняться.
Шлюз разбирает HTTP(S)-трафик и применяет правила на уровне доменов, а также показывает, куда именно агент обращается наружу. Отдельно от него правила на уровне протокола вводят операции, требующие подтверждения человеком: например, правило для записей в прод-БД содержит поле approve со значением human_approver.oncall — это дежурный инженер, у которого запрашивают подтверждение такой операции. Read-only-учётная запись и правила на шлюзе решают разные задачи: учётная запись определяет то, чего агент не сможет сделать в принципе, а шлюз вводит операции, для которых требуется отдельное подтверждение человека.
Где этот контур не работает
Три технических слоя работают только с теми агентами, которые уже попали в поле зрения платформы. Если агент запущен вне этого контура, защищать там попросту нечего. NetworkPolicy применяется к Pod по меткам, но метка появляется только у того Pod, который кто-то намеренно задеплоил в кластер: скрипт на ноутбуке аналитика, запускающий агента с production-кредами из переменных окружения, под такие правила не попадёт. Это Shadow AI вне Kubernetesагенты, запущенные в обход платформенного контура, вне поля зрения трёх слоёв защиты. Проблема решается выше уровня Kubernetes-манифестов: через инвентаризацию агентов, контролируемую выдачу доступов и правила, которые не позволяют использовать production-секреты вне утверждённого контура.
Вне границ остаётся и обход через дочерний процесс: один HTTP-прокси легко обойти, если агент может запустить дочерний процесс, например вызвать psql напрямую и не пройти через HTTP-слой. Наконец, deny-listсписок запрещённых операций, при котором всё, что в него не внесено, разрешено по своей природе неполон: если запретить DROP TABLE, но разрешить DELETE, агент может удалить все строки и формально не нарушить правило — в чёрном списке легко что-то упустить, а агенты хорошо умеют обходить ограничения.
Минимальный контур для production
Целиком контур выглядит так:
- Runtime: RuntimeClassобъект Kubernetes, который задаёт, какой runtime использовать для запуска контейнеров пода с gVisor или Kata.
- Единица развёртывания: отдельный Pod на агента, свой ServiceAccount без автомонтирования токена, узкая Role, namespace с ResourceQuota.
- Сеть: default-deny для ingress и egress, выход только через шлюз с allow-list, правила на уровне протокола для доступа к базам и Kubernetes API.
Перед полноценным стендом достаточно трёх коротких проверок. Первая — реальные права ServiceAccount через kubectl auth can-i --list с указанием system:serviceaccount:agents-report:report-agent: так видно, не доступны ли агенту лишние действия из-за унаследованных ClusterRole или слишком широких RoleBinding. Вторая — что egress действительно закрыт: зайти в Pod агента через kubectl exec и выполнить curl к домену, которого нет в allow-list; если запрос проходит, политика не работает или не выбрала этот Pod. Третья — наличие runtime командой kubectl get runtimeclass, чтобы выяснить, установлен ли в кластере runtime для gVisor, до указания runtimeClassName в манифесте: иначе Pod может остаться в Pending или не запуститься из-за отсутствующего RuntimeClass.
Что из этого следует на практике
- Промпт-фильтр не даёт границы безопасности: он наследует уязвимости охраняемой модели, а модель не различает команды и данные. Ограничения по уровням доступа и каналам связи промптами не выражаются.
- Граница для действующего агента опускается на три инфраструктурных слоя ниже текста: runtime, права, сеть.
- Если внедрить получается только один слой, начинать надо с сети: утечка данных и удаление базы проходят через песочницу и RBAC, а наружу данные уходят по сети.
- Read-only-учётка и правила на шлюзе не заменяют друг друга: первая определяет, чего агент не сможет сделать в принципе, вторые вводят операции, требующие подтверждения человека.
- Изоляция ядра включается одной строкой в podTemplate, но выбор между gVisor и Kata — компромисс по производительности: системные вызовы, сеть и файловая система дороже, а память и CPU-инструкции почти не затронуты.
- Контур не покрывает агентов вне платформы и обходы через дочерний процесс; deny-list неполон по своей природе, поэтому инвентаризация и контролируемая выдача доступов нужны выше уровня манифестов.