Песочница для ИИ-агента: изоляция, которая правда работает
Если вы даёте автономному ИИ-агенту право выполнять произвольный код или shell-команды — а именно так работают агенты-разработчики, ассистенты для DevOps и любые «сделай это сам» системы — рано или поздно встаёт вопрос: а что если агент выполнит что-то не то? Не по злому умыслу, а потому что его довёл до этого недоверенный входной текст, галлюцинация или неверно понятая задача. Ответ «у нас же всё в Docker» звучит успокаивающе, но технически это не полноценная изоляция, и разбираться в реальных уровнях защиты стоит до инцидента, а не после него.
Содержание
- Миф "он же в контейнере" — почему этого недостаточно
- Базовый уровень: минимальные привилегии, а не privileged-режим
- Сетевая изоляция: главный барьер против реальных атак
- Файловая изоляция: рабочая директория и ничего больше
- Когда нужна полноценная VM-изоляция: gVisor и Kata Containers
- Как оценить риск и выбрать нужный уровень защиты
Миф "он же в контейнере" — почему этого недостаточно
Контейнер — это не виртуальная машина. Docker, Podman, containerd — все они запускают процесс на том же самом ядре хоста, просто с изолированными namespaces (PID, network, mount, user) и ограничениями через cgroups. Ядро одно на всех контейнерах и на хосте. Это значит, что любая уязвимость в самом ядре Linux — в подсистеме сети, в файловой системе, в обработке syscalls — потенциально даёт побег из контейнера напрямую в хостовую систему. Такие уязвимости не гипотетические: за последние годы регулярно всплывали CVE, позволяющие эскалацию из контейнера к root на хосте при определённых условиях (непропатченное ядро, лишние capabilities, примонтированный docker.sock).
Мы разбирали это подробно в статье про миф "контейнер — это песочница": контейнер прекрасно решает задачи упаковки, воспроизводимости окружения и ограничения ресурсов, но как барьер против по-настоящему недоверенного кода — который специально пытается вырваться — это средство слабее, чем кажется на уровне маркетинга DevOps-инструментов.
Для ИИ-агента, который получает задачи в свободной форме (в том числе через RAG, веб-страницы, вывод других инструментов) и сам решает, какие команды выполнить, это принципиально. Обычный сервис в контейнере выполняет код, который написали и проверили ваши разработчики. Агент может выполнить код, который "предложила" ему страница из интернета через prompt injection, или который он сам сгенерировал по кривой логике. Порог недоверия здесь выше, и требования к изоляции должны расти вместе с ним.
Дальше — реальные уровни защиты по нарастанию строгости, от обязательного минимума до полной VM-изоляции.
Базовый уровень: минимальные привилегии, а не privileged-режим
Первое, с чего начинается любая адекватная песочница — контейнер без лишних прав. Это не устраняет риск полностью, но убирает самые дешёвые и массовые векторы атаки.
Что проверить обязательно:
docker run \
--cap-drop=ALL \
--cap-add=CHOWN \
--cap-add=SETUID \
--cap-add=SETGID \
--security-opt no-new-privileges \
--security-opt seccomp=default.json \
--read-only \
--tmpfs /tmp \
--user 10001:10001 \
--pids-limit 256 \
--memory 2g --cpus 2 \
agent-runner:latest
Разбор по пунктам:
--cap-drop=ALL+ точечный--cap-add— вместо полного набора Linux capabilities (по умолчанию Docker уже режет часть, но оставляет около 14) оставляете только то, что реально нужно рантайму агента. В подавляющем большинстве случаев агенту не нужныSYS_ADMIN,NET_ADMIN,SYS_PTRACE— а именно они чаще всего используются в эксплойтах побега из контейнера.- Никогда не
--privileged. Этот флаг фактически отключает всю модель изоляции контейнера — даёт доступ к устройствам хоста, снимает ограничения seccomp и AppArmor/SELinux. Для агента, выполняющего недоверенный код, privileged-режим не обсуждается. --user 10001:10001— контейнер не должен работать от root даже внутри своего namespace. Это тот же принцип, что мы разбирали в статье про антипаттерн "всё под root": минимальные привилегии — это не бюрократия, а сокращение поверхности атаки на конкретных, измеримых основаниях.--read-only+--tmpfs /tmp— корневая файловая система контейнера немутируема, писать можно только во временный tmpfs, который исчезает при перезапуске.no-new-privileges— процесс не может повысить привилегии через setuid-бинарники, даже если такой бинарник случайно окажется в образе.- Лимиты
--pids-limit,--memory,--cpus— это уже не про побег из контейнера, а про то, чтобы зациклившийся или прожорливый агент не положил хост-машину. Тема разобрана отдельно в статье про то, как cgroups ограничивают контейнер.
Даже при полном соблюдении этих правил контейнер остаётся контейнером — ядро общее с хостом. Это снижает вероятность успешной атаки и её последствия, но не устраняет класс атак «эксплойт ядра» полностью. Держите это в голове при оценке риска на шаге ниже.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Развернуть ИИ на сервереСетевая изоляция: главный барьер против реальных атак
Это, вероятно, самый недооценённый пункт — и одновременно самый важный. Подавляющее большинство реально опасных сценариев с ИИ-агентом требуют сетевого доступа: утечка секретов и данных наружу, скачивание и выполнение дополнительного вредоносного кода, обращение к внешним API от имени скомпрометированного окружения, эксфильтрация через DNS-туннели. Если у агента нет исходящего доступа в интернет, кроме явно разрешённых адресов — большая часть этих сценариев просто не реализуется технически, даже если контейнер или VM были скомпрометированы.
Практическая реализация — от простого к строгому:
Вариант 1: своя docker-сеть с egress-фильтром через iptables
docker network create --internal agent-net
# на хосте: разрешить агенту только конкретные адреса
iptables -I DOCKER-USER -s 172.20.0.0/16 -d api.anthropic.com -j ACCEPT
iptables -I DOCKER-USER -s 172.20.0.0/16 -d pypi.org -j ACCEPT
iptables -I DOCKER-USER -s 172.20.0.0/16 -j DROP
Флаг --internal в docker network create уже убирает контейнеру дефолтный маршрут наружу — дальше через DOCKER-USER chain добавляются точечные разрешения.
Вариант 2: forward-прокси с allowlist доменов
Более гибкий и управляемый способ — весь исходящий трафик агента идёт через прокси (например, Squid или tinyproxy) с явным списком разрешённых доменов, а прямой доступ в сеть у контейнера отсутствует вовсе:
# squid.conf
acl allowed_domains dstdomain .anthropic.com .pypi.org .github.com
http_access allow allowed_domains
http_access deny all
Контейнер агента получает только переменные HTTP_PROXY/HTTPS_PROXY, указывающие на этот прокси, и никакого другого сетевого выхода не имеет. Это удобно тем, что список разрешённых доменов можно менять на лету без пересборки контейнеров, и весь трафик логируется в одном месте — что полезно и для расследования инцидентов, и просто для аудита, что агент вообще делает в сети.
Вариант 3: deny by default, explicit allow только на нужные IP/порты — там, где агенту вообще не нужен произвольный интернет, а только пара внутренних сервисов (своя база знаний, внутренний API), самый безопасный вариант — общий выход в интернет не давать вовсе, прописав точечные правила только на конкретные IP и порты.
Ключевой принцип: белый список, а не чёрный. Перечислить всё плохое, что агенту нельзя, — проигрышная стратегия, список плохого бесконечен. Разрешать только конкретно нужное — вариант, который реально работает.
Файловая изоляция: рабочая директория и ничего больше
Третий обязательный слой — агент должен видеть строго ограниченную рабочую директорию и ничего из остальной файловой системы хоста. Здесь чаще всего встречается практическая ошибка: разработчик монтирует что-то "для удобства" — и именно это удобство потом становится дырой.
Что проверить в первую очередь:
- Никакого
/var/run/docker.sockвнутри контейнера. Это самая частая и самая опасная ошибка — монтирование docker socket даёт процессу внутри контейнера возможность управлять Docker-демоном хоста, то есть создавать новые привилегированные контейнеры с произвольным доступом к хостовой файловой системе. Если агенту зачем-то нужно управлять контейнерами (например, агент DevOps-автоматизации) — это выносится в отдельный, максимально ограниченный сервис-посредник с собственной авторизацией, а не даётся напрямую агенту, выполняющему произвольный код. - Никаких bind-монтов конфигов, ключей,
.ssh,.aws,.envфайлов хоста внутрь контейнера агента "для удобства запуска". Секреты передаются через отдельный секрет-менеджер (Vault, встроенные Docker/Kubernetes secrets) с минимально необходимым набором прав на момент выполнения задачи, а не лежат смонтированными постоянно. - Рабочая директория — отдельный volume, созданный специально под задачу, не пересекающийся с системными путями хоста:
docker run \
-v agent_workspace_$(uuidgen):/workspace:rw \
--workdir /workspace \
--read-only \
agent-runner:latest
- Квоты на объём этого volume — иначе агент, зациклившийся на генерации файлов, может исчерпать место на диске хоста. Это описано в статье про дисковые квоты для пользователей и контейнеров — тот же принцип применим и к рабочим директориям агентов.
- Namespace-изоляция файловой системы через mount namespace (то, что даёт контейнеру ощущение "он один на системе") — базовый механизм, на котором строится вся эта модель; подробнее — в статье про то, почему контейнер думает, что он один.
Практическое правило: если вы не можете объяснить за 10 секунд, зачем агенту доступ к конкретному пути хоста — этого доступа быть не должно.
Когда нужна полноценная VM-изоляция: gVisor и Kata Containers
Всё описанное выше снижает риск, но не убирает главный структурный недостаток обычного контейнера — общее ядро с хостом. Если сценарий действительно рискованный — агент регулярно работает с недоверенным вводом, выполняет код от третьих лиц, обрабатывает данные из непроверенных источников, — стоит рассмотреть изоляцию уровня полноценной VM, но без потери удобства контейнерного workflow.
Два практических варианта, которые совместимы с обычным Docker/Kubernetes API:
gVisor — user-space реализация ядра Linux от Google. Контейнер по-прежнему выглядит как контейнер снаружи, но syscalls процесса перехватываются и обрабатываются в изолированном user-space ядре (runsc), а не напрямую ядром хоста. Это резко сокращает поверхность атаки: даже если в приложении внутри есть уязвимость, эксплойт упирается в песочницу gVisor, а не в реальное ядро хоста.
# установка runsc как альтернативного runtime для Docker
apt-get install runsc
runsc install
systemctl restart docker
docker run --runtime=runsc agent-runner:latest
Kata Containers — идёт дальше: каждый контейнер запускается в собственной лёгкой виртуальной машине (на базе QEMU или облегчённых гипервизоров вроде Cloud Hypervisor), с отдельным ядром гостя. Это уже настоящая аппаратная изоляция уровня VM, но с интерфейсом OCI/containerd, привычным для Kubernetes:
# containerd config.toml
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata]
runtime_type = "io.containerd.kata.v2"
kubectl run agent-task --image=agent-runner:latest \
--overrides='{"spec":{"runtimeClassName":"kata"}}'
Мы разбирали обе технологии подробнее, с разницей в архитектуре и накладных расходах, в статье gVisor и Kata: изоляция уровня VM. Здесь важно честно сказать про цену: обе технологии добавляют оверхед по CPU, памяти и latency запуска (у Kata заметно больше, чем у gVisor, потому что поднимается настоящая VM). Для агента, запускающего тысячи коротких задач в минуту, эта цена может быть существенной. Поэтому решение не бинарное "всегда gVisor/Kata", а взвешенное — исходя из реального уровня риска, что мы разбираем дальше.
Как оценить риск и выбрать нужный уровень защиты
Не каждому агенту нужна VM-изоляция — это избыточная сложность и лишний оверхед там, где риск объективно низкий. Задача — честно сопоставить уровень защиты с реальным профилем риска, а не выбирать максимальную строгость "на всякий случай" или минимальную "потому что сойдёт".
Ключевые вопросы для оценки:
| Вопрос | Низкий риск | Высокий риск |
|---|---|---|
| Источник кода/команд, которые выполняет агент | Только код, написанный и провалидированный вашей командой | Произвольный ввод из внешних источников (веб, документы пользователей, вывод других ИИ) |
| Есть ли доступ к чувствительным данным рядом | Нет — агент работает в изолированном demo/dev-контуре | Да — рядом секреты, продакшн-база, внутренняя сеть компании |
| Возможность prompt injection через контент, который читает агент | Минимальна (закрытый контролируемый контекст) | Высокая (агент читает веб-страницы, письма, тикеты от посторонних) |
| Частота и объём запусков | Единичные задачи, можно позволить оверхед VM | Тысячи коротких задач в минуту — оверхед VM критичен |
| Последствия успешного побега из песочницы | Локальный тестовый стенд, ничего критичного рядом | Прод-инфраструктура, доступ к деньгам/данным клиентов |
Практический вывод: агент, работающий только с заведомо доверенным кодом в контролируемом контексте — например, внутренний CI-помощник, запускающий скрипты, уже прошедшие код-ревью — вполне обойдётся базовым уровнем (no-root + минимальные capabilities + сетевой whitelist), без gVisor/Kata. А агент, который по своей природе обрабатывает недоверенный внешний ввод — читает issues и PR от посторонних, парсит произвольные веб-страницы и документы, — обязан рассматривать VM-изоляцию всерьёз, потому что это его основной сценарий, а не редкое исключение.
Худшая стратегия — психологическое успокоение вместо инженерной оценки: "у нас же контейнер, значит безопасно" без разбора, какие capabilities включены, есть ли сетевой whitelist и что смонтировано внутрь. Формулировка "оно в Docker" ничего не говорит о реальном уровне защиты, пока вы не проверили конкретные флаги запуска.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Развернуть ИИ на сервереНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Хватит ли просто --cap-drop=ALL без сетевой изоляции?
Нет. Минимальные capabilities снижают риск локальной эскалации привилегий внутри контейнера, но не мешают процессу отправить данные наружу или скачать что-то извне. Сетевой whitelist закрывает совершенно другой класс рисков и нужен почти всегда, если агент вообще подключён к интернету.
Обязательно ли использовать gVisor или Kata для любого ИИ-агента?
Нет, это оправдано только при реально высоком риске — недоверенный внешний ввод, чувствительные данные рядом, серьёзные последствия побега. Для агента, работающего с доверенным кодом в контролируемом контуре, оверхед VM-изоляции обычно не стоит своих затрат.
gVisor и Kata дают одинаковый уровень защиты?
Нет, у них разная архитектура: gVisor перехватывает системные вызовы в user-space без отдельного ядра гостя, Kata поднимает полноценную лёгкую VM с собственным ядром. Kata обычно даёт более сильную изоляцию ценой более заметного оверхеда по времени старта и ресурсам; gVisor — компромисс между защитой и производительностью.
Можно ли комбинировать все уровни одновременно?
Да, и это правильный подход для по-настоящему рискового сценария: минимальные capabilities + read-only rootfs внутри Kata-контейнера с собственным сетевым namespace и egress-прокси с allowlist. Уровни не взаимоисключающие, а дополняющие друг друга.
Что первое проверить в уже существующей настройке агента?
Три вещи в порядке приоритета: нет ли --privileged и не смонтирован ли docker.sock; есть ли у контейнера неограниченный исходящий доступ в интернет; не работает ли процесс агента внутри контейнера от root. Эти три пункта закрывают самые дешёвые и самые частые векторы реальных инцидентов.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →