По мере того как большие языковые модели (БЯМ) ИИ и инструменты вроде OpenCode и Claude Code становятся всё более мощными, мы видим, как всё больше пользователей запускают изолированных агентов в ответ на сообщения в чате, обновления в Kanban, интерфейсы для vibe coding, сессии терминала, комментарии в GitHub и многое другое.
Песочница — это важный шаг вперед по сравнению с простыми контейнерами, потому что она даёт вам несколько преимуществ:
-
Безопасность: Любой недоверенный конечный пользователь (или неконтролируемая БЯМ) может выполняться в песочнице, не ставя под угрозу хост-машину или другие песочницы, работающие рядом с ней. Традиционно этого добиваются с помощью микроВМ (но не всегда).
-
Скорость: Конечный пользователь должен иметь возможность быстро запустить новую песочницу и быстро восстановить состояние из ранее использованной.
-
Управление: Доверенной платформе должна быть доступна возможность выполнять действия внутри недоверенной области песочницы. Это может означать монтирование файлов в песочнице, контроль над тем, какие запросы получают к ней доступ, или выполнение конкретных команд.
Сегодня мы рады добавить ещё один ключевой компонент управления к нашим Песочницам и всем Контейнерам: исходящие Workers (outbound Workers). Это программные прокси для исходящего трафика, которые позволяют пользователям, запускающим песочницы, легко подключаться к различным сервисам, добавлять наблюдаемость (observability) и, что особенно важно для агентов, — гибкую и безопасную аутентификацию.
Как это работает
Вот краткий пример добавления секретного ключа в заголовок с помощью исходящего Worker:
class OpenCodeInABox extends Sandbox {
static outboundByHost = {
"github.com": (request, env, ctx) => {
const headersWithAuth = new Headers(request.headers);
headersWithAuth.set("x-auth-token", env.SECRET);
return fetch(request, { headers: headersWithAuth });
}
}
}
Всякий раз, когда код, выполняющийся в песочнице, делает запрос к “github.com”, запрос проксируется через этот обработчик. Это позволяет вам делать всё что угодно с каждым запросом, включая его логирование, модификацию или отмену. В данном случае мы безопасно внедряем секрет (об этом подробнее позже). Прокси работает на той же машине, что и песочница, имеет доступ к распределённому состоянию, и его можно легко модифицировать с помощью простого JavaScript.
Мы в восторге от всех новых возможностей, которые это открывает для Песочниц, особенно в области аутентификации для агентов. Прежде чем углубляться в детали, давайте сделаем шаг назад и кратко рассмотрим традиционные формы аутентификации и почему мы считаем, что есть нечто лучшее.
Распространённые методы аутентификации для агентских рабочих нагрузок
Основная проблема аутентификации для агентов заключается в том, что мы не можем полностью доверять рабочей нагрузке. Хотя наши БЯМ не являются злонамеренными (по крайней мере, пока), нам всё равно необходимо применять защитные меры, чтобы гарантировать, что они не будут использовать данные ненадлежащим образом или выполнять нежелательные действия.
Существует несколько распространённых методов предоставления аутентификации агентам, и у каждого есть недостатки:
Стандартные API-токены — это самый базовый метод аутентификации, обычно внедряемый в приложения через переменные окружения или смонтированные файлы с секретами. Это, пожалуй, самый простой, но и наименее безопасный метод. Вы должны доверять, что песочница каким-то образом не будет скомпрометирована или случайно не раскроет токен при выполнении запроса. Поскольку вы не можете полностью доверять агенту, вам придётся настраивать срок действия и ротацию токенов, что может быть хлопотно.
Токены идентификации рабочих нагрузок (workload identity tokens), такие как OIDC-токены, могут решить некоторые из этих проблем. Вместо того чтобы выдавать агенту токен с общими разрешениями, вы можете выдать ему токен, подтверждающий его личность. Теперь агент, вместо прямого доступа к какому-либо сервису с токеном, может обменять токен идентификации на токен доступа с очень коротким сроком жизни. OIDC-токен можно аннулировать после завершения конкретного рабочего процесса агента, а управление сроком действия становится проще. Один из самых больших недостатков токенов идентификации рабочих нагрузок — потенциальная негибкость интеграций. Многие сервисы не имеют встроенной поддержки OIDC, поэтому для работоспособной интеграции с вышестоящими сервисами платформам придётся разрабатывать собственные службы обмена токенами. Это затрудняет внедрение.
Пользовательские прокси (custom proxies) обеспечивают максимальную гибкость и могут использоваться в паре с токенами идентификации рабочих нагрузок. Если вы можете пропустить весь или часть исходящего трафика песочницы через доверенный фрагмент кода, вы можете вставить любые необходимые правила. Возможно, вышестоящий сервис, с которым общается ваш агент, имеет плохую систему RBAC и не может предоставить детальные разрешения. Не проблема, просто напишите правила контроля и разрешения сами! Это отличный вариант для агентов, которых вам нужно жёстко контролировать с помощью детальных правил. Однако как перехватить весь трафик песочницы? Как настроить динамический и легко программируемый прокси? Как эффективно проксировать трафик? Это не простые задачи.
Имея в виду эти неидеальные методы, как же выглядит идеальный механизм аутентификации?
В идеале он должен быть:
-
С нулевым доверием (Zero trust). Никакой токен никогда не выдаётся недоверенному пользователю ни на какое время.
-
Простой (Simple). Легко создавать. Не требует сложной системы генерации, ротации и расшифровки токенов.
-
Гибкий (Flexible). Мы не зависим от вышестоящей системы в предоставлении необходимого нам детального доступа. Мы можем применять любые нужные нам правила.
-
Осведомлённый об идентичности (Identity-aware). Мы можем идентифицировать песочницу, совершающую вызов, и применять к ней специфические правила.
-
Наблюдаемый (Observable). Мы можем легко собирать информацию о совершаемых вызовах.
-
Производительный (Performant). Мы не делаем циклические обращения к централизованному или медленному источнику истины.
-
Прозрачный (Transparent). Рабочей нагрузке в песочнице не нужно об этом знать. Всё просто работает.
-
Динамичный (Dynamic). Мы можем менять правила на лету.
Мы считаем, что исходящие Workers для Песочниц соответствуют всем этим критериям. Давайте посмотрим как.
Исходящие Workers на практике
Основы: ограничения и наблюдаемость
Сначала рассмотрим очень простой пример: логирование запросов и запрет определённых действий.
В этом случае мы будем использовать функцию `outbound`, которая перехватывает все исходящие HTTP-запросы из песочницы. С помощью нескольких строк JavaScript легко обеспечить выполнение только GET-запросов, а также логировать и отклонять любые неразрешённые методы.
class MySandboxedApp extends Sandbox {
static outbound = (req, env, ctx) => {
// Запретить любое не-GET действие и залогировать
if (req.method !== 'GET') {
console.log(`Container making ${req.method} request to: ${req.url}`);
return new Response('Not Allowed', { status: 405, statusText: 'Method Not Allowed'});
}
// Продолжить, если это GET-запрос
return fetch(req);
};
}
Этот прокси работает на Workers и выполняется на той же машине, что и изолированная ВМ. Workers создавались для быстрого времени отклика, часто находясь перед кешированным CDN-трафиком, поэтому дополнительная задержка крайне минимальна.
Поскольку это работает на Workers, мы получаем наблюдаемость "из коробки". Вы можете просматривать логи и исходящие запросы в дашборде Workers или экспортировать их в инструмент мониторинга производительности приложений на ваш выбор.
Внедрение учётных данных с нулевым доверием
Как мы можем использовать это для обеспечения среды с нулевым доверием для нашего агента? Представим, что мы хотим сделать запрос к частному экземпляру GitHub, но никогда не хотим, чтобы наша БЯМ получила доступ к приватному токену.
Мы можем использовать `outboundByHost` для определения функций для конкретных доменов или IP-адресов. В данном случае мы будем внедрять защищённые учётные данные, если домен — "my-internal-vcs.dev". Изолированный агент никогда не получает доступ к этим учётным данным.
class OpenCodeInABox extends Sandbox {
static outboundByHost = {
"my-internal-vcs.dev": (request, env, ctx) => {
const headersWithAuth = new Headers(request.headers);
headersWithAuth.set("x-auth-token", env.SECRET);
return fetch(request, { headers: headersWithAuth });
}
}
}
Также легко сделать ответ условным, в зависимости от идентификатора контейнера. Вам не нужно внедрять одни и те же токены для каждого экземпляра песочницы.
static outboundByHost = {
"my-internal-vcs.dev": (request, env, ctx) => {
// примечание: KV зашифровано как в состоянии покоя, так и при передаче
const authKey = await env.KEYS.get(ctx.containerId);
const requestWithAuth = new Request(request);
requestWithAuth.headers.set("x-auth-token", authKey);
return fetch(requestWithAuth);
}
}
Использование облачной платформы разработчика Cloudflare
Как вы могли заметить в последнем примере, ещё одним крупным преимуществом использования исходящих Workers является то, что это упрощает интеграцию в экосистему Workers. Ранее, если пользователю требовался доступ к R2, ему приходилось внедрять учётные данные R2, а затем выполнять вызов из своего контейнера к публичному API R2. То же самое касалось KV, Agents, других Containers, других сервисов Worker, и т.д..
Теперь вы просто вызываете любую привязку из своих исходящих Workers.
class MySandboxedApp extends Sandbox {
static outboundByHost = {
"my.kv": async (req, env, ctx) => {
const key = keyFromReq(req);
const myResult = await env.KV.get(key);
return new Response(myResult);
},
"objects.cf": async (req, env, ctx) => {
const prefix = ctx.containerId
const path = pathFromRequest(req);
const object = await env.R2.get(`${prefix}/${path}`);
const myResult = await env.KV.get(key);
return new Response(myResult);
},
};
}
Вместо разбора токенов и настройки политик мы можем легко условно определять доступ с помощью кода и любой желаемой логики. В примере с R2 мы также смогли использовать ID песочницы, чтобы дополнительно легко ограничить область доступа.
Сделаем элементы управления динамическими
Сетевое управление также должно быть динамическим. На многих платформах конфигурация для сетевого взаимодействия Контейнеров и ВМ статична и выглядит примерно так:
{
defaultEgress: "block",
allowedDomains: ["github.com", "npmjs.org"]
}
Это лучше, чем ничего, но мы можем сделать лучше. Для многих песочниц мы можем захотеть применить политику при запуске, а затем переопределить её другой после выполнения конкретных операций.
Например, мы можем запустить песочницу, получить наши зависимости через NPM и GitHub, а затем заблокировать исходящий трафик после этого. Это гарантирует, что мы открываем сеть на минимально возможное время.
Чтобы достичь этого, мы можем использовать outboundHandlers, что позволяет нам определять произвольные обработчики исходящего трафика, которые можно применять программно с помощью метода setOutboundHandler. Каждый из них также принимает параметры (params), позволяя настраивать поведение из кода. В данном случае мы разрешим некоторые имена хостов с помощью пользовательской политики "allowHosts", а затем отключим HTTP.
class MySandboxedApp extends Sandbox {
static outboundHandlers = {
async allowHosts(req, env, { params }) {
const url = new URL(request.url);
const allowedHostname = params.allowedHostnames.includes(url.hostname);
if (allowedHostname) {
return await fetch(newRequest);
} else {
return new Response(null, { status: 403, statusText: "Запрещено" });
}
}
async noHttp(req) {
return new Response(null, { status: 403, statusText: "Запрещено" });
}
}
}
async setUpSandboxes(req, env) {
const sandbox = await env.SANDBOX.getByName(userId);
await sandbox.setOutboundHandler("allowHosts", {
allowedHostnames: ["github.com", "npmjs.org"]
});
await sandbox.gitClone(userRepoURL)
await sandbox.exec("npm install")
await sandbox.setOutboundHandler("noHttp");
}
Это можно расширить ещё дальше. Ваш агент может задать конечному пользователю вопрос, например: «Хотите ли вы разрешить POST-запросы к cloudflare.com?» в зависимости от того, какие инструменты ему нужны в данный момент. С динамическими исходящими Workers вы можете легко изменять правила песочницы на лету, чтобы обеспечить такой уровень контроля.
Поддержка TLS с помощью MITM-проксирования
Чтобы делать что-то полезное с запросами, помимо их разрешения или запрета, вам нужен доступ к содержимому. Это означает, что если вы делаете HTTPS-запросы, они должны быть расшифрованы прокси Workers.
Для этого для каждого экземпляра Sandbox создаётся уникальный временный центр сертификации (CA) и приватный ключ, а CA помещается в песочницу. По умолчанию экземпляры песочницы будут доверять этому CA, в то время как стандартные экземпляры контейнеров могут согласиться доверять ему, например, выполнив команду sudo update-ca-certificates.
export class MyContainer extends Container {
interceptHttps = true;
}
MyContainer.outbound = (req, env, ctx) => {
// Все HTTP(S) запросы будут запускать этот хук.
return fetch(req);
};
TLS-трафик проксируется изолированным сетевым процессом Cloudflare путём выполнения TLS-рукопожатия. Он создаёт промежуточный CA из временного и уникального приватного ключа, используя SNI, извлечённый из ClientHello. Затем он вызовет на той же машине настроенный Worker для обработки HTTPS-запроса.
Наш временный приватный ключ и CA никогда не покинут наш побочный процесс (sidecar) рантайма контейнера и никогда не передаются другим побочным процессам контейнеров.
При таком подходе исходящие Workers действуют как полностью прозрачный прокси. Песочнице не требуется никакого понимания конкретных протоколов или доменов — весь HTTP и HTTPS трафик проходит через обработчик исходящего трафика для фильтрации или модификации.
Под капотом
Чтобы включить функциональность, показанную выше, как в Container, так и в Sandbox, мы добавили новые методы в объект ctx.container: interceptOutboundHttp и interceptOutboundHttps, которые перехватывают исходящие запросы к определённым именам хостов (с базовым glob-сопоставлением), диапазонам IP-адресов, и могут использоваться для перехвата всех исходящих запросов. Эти методы вызываются с WorkerEntrypoint, который настраивается как входная точка для исходящего Worker.
export class MyWorker extends WorkerEntrypoint {
fetch() {
return new Response(this.ctx.props.message);
}
}
// ... внутри вашего контейнера DurableObject ...
this.ctx.container.start({ enableInternet: false });
const outboundWorker = this.ctx.exports.MyWorker({ props: { message: 'привет' } });
await this.ctx.container.interceptOutboundHttp('15.0.0.1:80', outboundWorker);
// Отныне все HTTP-запросы к 15.0.0.1:80 возвращают "привет"
await this.waitForContainerToBeHealthy();
// Теперь вы можете решить вернуть другое сообщение...
const secondOutboundWorker = this.ctx.exports.MyWorker({ props: { message: 'подмена' } });
await this.ctx.container.interceptOutboundHttp('15.0.0.1:80', secondOutboundWorker);
// все HTTP-запросы к 15.0.0.1 теперь показывают "подмена", даже на соединениях, которые были
// открыты до этого вызова interceptOutboundHttp
// Вы даже можете задавать имена хостов, CIDR-блоки для IPv4 и IPv6
await this.ctx.container.interceptOutboundHttp('example.com', secondOutboundWorker);
await this.ctx.container.interceptOutboundHttp('*.example.com', secondOutboundWorker);
await this.ctx.container.interceptOutboundHttp('123.123.123.123/23', secoundOutboundWorker);
Весь прокси-трафик в Workers происходит локально на той же машине, где запущена ВМ песочницы. Несмотря на то, что связь между контейнером и Worker происходит «без аутентификации», она безопасна.
Эти методы могут быть вызваны в любое время, до или после запуска контейнера, даже пока соединения ещё открыты. Соединения, отправляющие несколько HTTP-запросов, автоматически получат новую точку входа, поэтому обновление исходящих Workers не нарушит существующие TCP-соединения и не прервёт HTTP-запросы.
Локальная разработка с wrangler dev также поддерживает перехват исходящего трафика. Чтобы сделать это возможным, мы автоматически запускаем побочный процесс (sidecar) внутри сетевого пространства имён (network namespace) локального контейнера. Мы назвали этот побочный компонент proxy-everything. Как только proxy-everything подключается, он применяет соответствующие правила TPROXY для nftables, направляя соответствующий трафик из локального Контейнера в workerd, открытый JavaScript-рантайм Cloudflare, который запускает исходящий Worker. Это позволяет локальному процессу разработки отражать то, что происходит в prod, поэтому тестирование и разработка остаются простыми.