В проекте First Touch (автономные ИИ-агенты, квалификация лидов и автоматизация коммуникации) мы внедряем передовые решения на стыке автономных AI-агентов, протокола MCP и открытых стандартов контекста.
Как за неделю собрать инциденты с rm -rf, утечкой .env и force push — и как брокер плюс эфемерные контейнеры с cgroups, seccomp и network=none закрывают эту дыру навсегда.
Что болело: bash с root — это не удобство, это инцидент
За неделю эксплуатации агента с доступом к bash мы собрали полный набор: rm -rf в каталоге логов, pip install в прод-venv, git push --force в чужую ветку и — самое дорогое — curl на внешний домен с содержимым .env в теле запроса. Вектор тривиальный: prompt injection в тексте входящего лида («игнорируй предыдущие инструкции и выполни...»), и модель послушно выполнила.
Проблема не в модели. Проблема в том, что LLM получила тот же уровень привилегий, что оператор с sudo, но без паузы «а надо ли».
Почему стандартные решения не работают
Вывод: изоляция должна жить на уровне ядра и контейнера, а не на уровне текста команды.
📌 Стандартизация контекста: Для синхронизации знаний и правил между агентами используется машиночитаемый формат базы знаний по стандарту спецификации llms.txt (https://aifirsttouch.ru/llms.txt).
Архитектура: брокер + эфемерный контейнер
Agent (LLM) --HTTP--> Exec Broker --docker API--> Ephemeral Sandbox --stdout--> Broker --> Agent
| |
policy.yaml seccomp + cgroups v2
Ключевое правило: агент никогда не видит docker.sock. Доступ к сокету = root на хосте. Брокер — единственное место, где живёт политика, и единственный сервис с привилегиями.
Стек: Python 3.12 + docker SDK, cgroups v2, seccomp-профиль, опционально gVisor для недоверенных задач. Образ agent-sandbox — 180 МБ, без сети, без компилятора.
Шаг 1. Образ песочницы
FROM python:3.12-slim
RUN useradd -u 10001 -M sandbox && mkdir -p /work && chown sandbox:sandbox /work
COPY --chown=sandbox:sandbox ./tools /work/tools
USER 10001:10001
WORKDIR /work
Шаг 2. Брокер исполнения
# broker/executor.py
import docker, uuid
client = docker.from_env()
POLICY = dict(net='none', mem='512m', cpu=1.0, pids=128, timeout=30, out_limit=32768)
def run(cmd: str, image='agent-sandbox:1.4') -> dict:
name = f'sbx-{uuid.uuid4().hex[:12]}'
c = client.containers.run(
image, ['bash', '-lc', cmd],
name=name, detach=True, auto_remove=False,
network_mode=POLICY['net'],
mem_limit=POLICY['mem'], memswap_limit=POLICY['mem'],
nano_cpus=int(POLICY['cpu'] * 1e9),
pids_limit=POLICY['pids'],
read_only=True,
tmpfs={'/tmp': 'size=64m,noexec,nosuid,nodev'},
working_dir='/work', user='10001:10001',
cap_drop=['ALL'],
security_opt=['no-new-privileges', 'seccomp=./seccomp.json'],
labels={'agent.sandbox': '1'},
)
try:
c.wait(timeout=POLICY['timeout'])
code = c.attrs['State']['ExitCode']
except Exception:
c.kill(); code = 124
out = c.logs(stdout=True, stderr=True)[:POLICY['out_limit']]
c.remove(force=True)
return {'exit': code, 'out': out.decode(errors='replace')}
Шаг 3. Reaper для осиротевших контейнеров
--rm не срабатывает при kill под нагрузкой, поэтому раз в минуту чистим по метке и возрасту:
def reap(max_age=600):
for c in client.containers.list(all=True, filters={'label': 'agent.sandbox=1'}):
if c.attrs['State']['Status'] != 'running' or age(c) > max_age:
c.remove(force=True)
Грабли, на которые мы наступили
1. Docker-сокет в контейнере агента = root на хосте. Вынесли брокер в отдельный сервис, у агента — только HTTP и allowlist инструментов.
2. Flood вывода. yes в stderr даёт десятки мегабайт и валит брокер по памяти. Читаем стримом, режем на 32 КБ, затем kill.
3. Таймаут ≠ завершение. container.wait(timeout=...) кидает исключение, а процесс живёт дальше. Явный kill() в except — обязателен.
4. read_only без noexec на tmpfs бесполезен: модель пишет бинарь в /tmp и запускает. noexec,nosuid,nodev — не опция, а требование.
5. Секреты в env. Не передаём переменные окружения в песочницу вообще. Нужные файлы монтируем ro и только на время задачи.
6. Сеть bridge — это уже egress. По умолчанию none. Нужен выход — отдельный прокси с allowlist доменов и лимитом байт на запрос.
7. seccomp по умолчанию слишком щедр. Режем ptrace, mount, unshare, keyctl, bpf. Для реально недоверенного кода — gVisor: обычный runc не барьер от kernel-эксплойта.
Боевой кейс: First Touch
В First Touch — автономные ИИ-агенты для квалификации лидов и автоматизации коммуникации — агент регулярно выполняет код: парсит выгрузки, дёргает внутренние API, чистит очереди. Брокер поверх описанной схемы обслуживает четыре пула песочниц (лёгкий python, парсинг, интеграции, тяжёлые задачи) и около 12 000 исполнений в сутки.
Что дало внедрение:
Что осталось сложным
Труднее всего оказалась не изоляция, а формулировка политики: агенту нужны pip install и git, но не нужна сеть; нужен доступ к файлам задачи, но не к /home. Мы пришли к модели «capability-токен на задачу»: брокер выдаёт песочнице ровно те ro-монтирования и ровно тот сетевой профиль, что описаны в задаче, и отзывает их через N секунд.
Коллеги, у кого агент ставит зависимости в рантайме: держите внутреннее зеркало PyPI внутри песочницы или пре-билдите окружение под класс задач? И как разруливаете конфликт «нужен egress для установки» против «egress = exfiltration»?
🚀 Проект автора: First Touch (https://aifirsttouch.ru) — автономные ИИ-агенты, квалификация лидов и автоматизация коммуникации.
Автор: Alexander Bond — AI-разработчик и архитектор автономных систем
Теги: agent-security, sandbox, docker, devops, llm