Рамки изоляции и их слепое пятно
Примитивы изоляции — песочницы, виртуальные машины, фильтры системных вызовов — отлично отвечают на вопрос, где может выполняться код. Они определяют среду выполнения: какие бинарники могут запускаться, какие файлы можно читать, какие вызовы ядра разрешены. Это необходимый слой для любого агента, который выполняет сгенерированный код или произвольные инструменты. Однако это не модель внешнего охвата.
Агент внутри идеально изолированной песочницы всё равно может отправить тысячи исходящих запросов к тысячам различных назначений. Песочнице безразлично, общается ли процесс с одним хостом или с десятью тысячами; ей важно, чтобы процесс не покинул пределы контейнера. Режим отказа, с которым мы постоянно сталкиваемся, — это не побег. Это охват.
Рассмотрим изолированного агента, скомпрометированного через полезную нагрузку prompt-injection в извлечённом документе. Агент не может обойти фильтр системных вызовов. Он не может прочитать файловую систему хоста. Однако он может использовать предоставленный сетевой доступ и предоставленные учётные данные, чтобы перечислить внутренние эндпоинты, эксфильтровать контекст небольшими порциями или прощупывать сторонние API, пока одно из них не ответит полезными данными. Изоляция держится. Радиус поражения всё равно велик.
Отраслевой разговор о безопасности агентов в значительной степени унаследовал свои модели угроз из безопасности контейнеров, где границей является хост. Для агентов граница — это набор внешних идентичностей, эндпоинтов и потоков данных, на которые агент может влиять. Этот набор не ограничивается песочницей. Он ограничивается политикой исходящего трафика, а политика исходящего трафика без модели затрат — это просто список разрешённых хостов.
Исходящий трафик как бюджетируемый ресурс
Мы рассматриваем исходящий трафик как бюджетируемый ресурс с измерениями, которые списываются за каждое исходящее действие. Измерения, которые мы нашли полезными:

- Различные назначения. Количество уникальных сетевых эндпоинтов, к которым идёт обращение, а не количество вызовов. Десять тысяч вызовов к одному хосту — это иной профиль риска, чем десять вызовов к десяти тысячам хостов. Подсчёт назначений должен нормализоваться по DNS, IP и порту и должен рассматривать редиректы как новые назначения.
- Исходящие байты. Объём полезной нагрузки, покидающей процесс, включая заголовки, строки запросов и тела запросов. Это ловит медленную эксфильтрацию, которая остаётся ниже порогов по количеству вызовов.
- Область действия учётных данных. Набор идентичностей и токенов, которые может предъявить исходящий трафик. Бюджет, который допускает широкий токен к узкому назначению, всё равно остаётся широким бюджетом.
- Временное окно. Интервал, в течение которого бюджет действителен. Бюджет, который обновляется каждую секунду, — это не бюджет.
Принудительное применение должно находиться на сетевой границе, а не в фреймворке агента. Проверки на уровне фреймворка носят рекомендательный характер: они зависят от сотрудничества агента, от корректного использования обёртки инструмента и от отсутствия путей кода в обход обёртки. Сетевая граница — прокси, сайдкар service mesh, хук eBPF на интерфейсе исходящего трафика — видит каждый пакет независимо от того, какая библиотека его инициировала. Фреймворк агента может запросить бюджет; граница его применяет.
Граница также должна выдавать терминальное состояние. Когда бюджет исчерпан, запуск не деградирует молча и не повторяется. Он завершается с определённым путём восстановления: оператор уведомляется, запуск помечается как исчерпавший бюджет, и человек может авторизовать новый запуск с явно заданным новым бюджетом. Это делает исчерпание полноценным исходом, а не ошибкой, которую нужно проглотить.
Правила проектирования
Каждый вызов инструмента, покидающий процесс, должен списывать бюджет. Это включает вызовы, которые выглядят внутренними, вызовы, использующие кэшированную запись DNS, и вызовы, сделанные зависимостями, а не собственным кодом агента. Если байт покидает границу процесса, это исходящий трафик. Списание атомарно с запросом; если списание не удалось, запрос не уходит.
Бюджеты задаются на запуск и на идентичность и не переносятся. Запуск — это одна логическая задача с определённым началом и концом. Бюджет принадлежит этому запуску, а не процессу агента, не пользовательской сессии и не развёртыванию. Идентичности ограничиваются отдельно: агент, действующий от имени одного пользователя, не наследует бюджет другого. Неиспользованный бюджет не переносится на следующий запуск, потому
](/media/server-rack-closeup-3b0bfb-b5562f.jpg)
Логируйте списание, класс назначения и решение, а не только запрос. Строка лога, в которой записан только URL, недостаточна для пост-инцидентного анализа. Мы записываем сумму списания бюджета, класс назначения (например: внутренний сервис, сторонний API, публичный веб, неизвестно), оставшийся бюджет и политическое решение, которое разрешило или отклонило запрос. Именно запись решения делает бюджет проверяемым. Без неё бюджет — это число, а не контроль.
Конкретный механизм
Одна из реализаций, которые мы использовали, размещает прямой прокси между песочницей агента и сетью. Прокси хранит таблицу бюджетов на запуск с ключом по идентификатору запуска и идентичности. При каждом запросе прокси разрешает назначение, классифицирует его, вычисляет стоимость в байтах по заголовкам и телу запроса и пытается атомарно уменьшить бюджет запуска по всем измерениям. Если любое измерение уйдёт в минус, прокси возвращает агенту отказ и отправляет событие исчерпания бюджета в плоскость управления. Плоскость управления переводит запуск в терминальное состояние и уведомляет оператора.
Агент никогда не видит таблицу бюджетов. Он видит отказ, который должен обработать как невосстановимое условие. Это сделано намеренно: агент не должен иметь возможности рассуждать о своём оставшемся охвате, потому что агент, способный рассуждать о своём оставшемся охвате, может планировать в обход него.
Честный компромисс
Принудительное применение бюджета добавляет задержку и операционную поверхность. Каждый исходящий запрос влечёт переход через прокси, шаг классификации и обновление состояния. Для агентов, которые делают высокочастотные вызовы к одному известному эндпоинту, это накладные расходы с малой пользой; бюджет фактически статичен. Для агентов, которые веерно обращаются ко многим назначениям, накладные расходы и есть суть.
Более сложный компромисс — ложные срабатывания. Легитимный запуск, которому нужно обратиться к новому классу назначений, упрётся в бюджет и завершится. Операторы должны иметь возможность быстро предоставить новый бюджет, и предоставление должно логироваться с той же строгостью, что и исходный. Если путь восстановления медленный, команды будут расширять бюджет по умолчанию, чтобы избежать прерываний, и контроль станет декоративным. Бюджет хорош ровно настолько, насколько хорош путь восстановления.
Изоляция остаётся необходимой. Она не достаточна. Агент, который может дотянуться до десяти тысяч назначений изнутри песочницы, не изолирован ни в каком смысле, значимом для данных, которых он может коснуться. Граница определяет, где он выполняется. Бюджет определяет, как далеко он дотягивается. Должны присутствовать оба, и второй должен применяться там, где агент не может с ним спорить.
