Egress control becomes programmable for sandboxed agents

As LLM-driven coding agents gain traction, more developers are launching sandboxed sessions from chat messages, kanban updates, terminal prompts, and other entry points. Sandboxes go beyond plain containers by providing three things: isolation from the host and neighboring workloads (usually via microVM), fast startup with state restoration, and control — letting the trusted platform act inside the untrusted domain by mounting files, gating requests, or executing commands.

We're extending that control to Sandboxes and all Containers with outbound Workers: programmatic egress proxies that intercept traffic from a sandbox to external services. That opens up observability, request modification, and safer authentication for agent workloads.

A minimal outbound Worker injects a secret into a header before a request goes out:

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 });
    }
  }
}

Every request from the sandbox to a matching host passes through the handler. The proxy runs on the same machine as the sandbox, can read distributed state, and is editable with plain JavaScript. You can log, alter, or cancel requests inline — no round trip to a remote service.

BLOG-3199 2

Why agent auth is hard

The fundamental problem: the workload itself isn't fully trusted. Agent code can make mistakes or be exploited, so credentials granted to it need protection. Traditional approaches each carry tradeoffs.

Standard API tokens injected as environment variables or mounted secrets are simplest but leak-prone. If the sandbox is compromised, the token goes with it. Mitigations like expiry and rotation add operational overhead.

Workload identity tokens (e.g., OIDC) improve on this by attesting the workload's identity instead of granting broad access. The agent exchanges a short-lived identity token for an access token, and the credential can be revoked when the workflow finishes. The catch: many upstream services lack native OIDC support, so platforms must build their own token-exchange layer.

Custom proxies offer maximum flexibility, particularly when paired with identity tokens. You control every request and enforce whatever granular policy you need, independent of the upstream service's RBAC. But intercepting all sandbox traffic with a dynamic, efficient, programmable proxy is hard to build yourself.

An ideal mechanism would be zero trust (no token ever reaches the untrusted workload), simple to author, flexible, identity-aware per sandbox, observable, low-latency, transparent to the sandboxed code, and dynamically adjustable. Outbound Workers for Sandboxes check those boxes.

Outbound Workers in action

Logging and blocking

The most basic use is request control. With an outbound function, you can observe and restrict all outbound HTTP traffic. A short handler can ensure only GET requests pass, logging and denying everything else:

class MySandboxedApp extends Sandbox {
  static outbound = (req, env, ctx) => {
    // Deny any non-GET action and log
    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'});
    }

    // Proceed if it is a GET request
    return fetch(req);
  };
}

Because the proxy runs on Workers alongside the VM, added latency stays minimal. Logging and request traces are available in the Workers dashboard or through Logpush integration with external monitoring tools.

Zero-trust credential injection

For agent workloads that must call authenticated services, the goal is to keep secrets away from the LLM entirely. Using outboundByHost, you can match specific domains and inject credentials only when the destination is trusted:

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 });
    }
  }
}

The sandboxed process never sees the secret. You can also condition behavior on the sandbox's identity, so different instances get different tokens without code changes inside the VM:

 static outboundByHost = {
  "my-internal-vcs.dev": (request, env, ctx) => {
    // note: KV is encrypted at rest and in transit
    const authKey = await env.KEYS.get(ctx.containerId);

    const requestWithAuth = new Request(request);
    requestWithAuth.headers.set("x-auth-token", authKey);
    return fetch(requestWithAuth);
  }
}

Native Cloudflare platform access

Outbound Workers remove the credential dance for Cloudflare services. Previously, a container calling R2 needed an injected R2 token and a round trip to the public API. Now the outbound Worker can use any Cloudflare binding directly — R2, KV, Agents, other Containers, or Worker services via service bindings:

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);
    },
  };
}

Instead of parsing tokens and wiring up policies, you write conditional logic in the handler. The sandbox ID is available for scoping access to specific buckets or resources.

Dynamic policy changes

Networking rules shouldn't be fixed at boot. On many platforms, container egress config is static:

{
  defaultEgress: "block",
  allowedDomains: ["github.com", "npmjs.org"]
}

But a sandbox often has changing needs over its lifetime. For example: start with network access open to fetch NPM dependencies and clone from GitHub, then lock egress down after setup completes. The outboundHandlers API plus the setOutboundHandler method let you define named policies and swap them programmatically at runtime, with parameters to tune behavior per call:

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: "Forbidden" });
      }
    }
    
    async noHttp(req) {
      return new Response(null, { status: 403, statusText: "Forbidden" });
    }
  }
}

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");
}

That pattern generalizes far beyond bootstrapping. An agent could ask the user for permission before enabling specific methods or hosts mid-session, and the platform can flip the policy on the fly to match.

Transparent TLS interception

To filter or rewrite request content, the proxy must see inside HTTPS. Each sandbox instance gets a unique ephemeral certificate authority and private key; the CA is placed inside the sandbox and trusted by default there, while standard containers can opt in (e.g., via sudo update-ca-certificates).

export class MyContainer extends Container {
  interceptHttps = true;
}

MyContainer.outbound = (req, env, ctx) => {
  // All HTTP(S) requests will trigger this hook.
  return fetch(req);
};

TLS traffic is terminated by an isolated Cloudflare network process that constructs a leaf certificate from the ephemeral key, using the SNI from the ClientHello, then invokes the configured Worker on the same machine to handle the decrypted request.

The ephemeral private key and CA stay confined to the container runtime's sidecar process and are never shared with other sidecars. With this in place, outbound Workers operate as a fully transparent proxy — the sandbox needs no awareness of domains or protocols, because every HTTP and HTTPS request flows through the handler for inspection, filtering, or modification.

How outbound interception works

Cloudflare added two new methods — interceptOutboundHttp and interceptOutboundHttps — to the ctx.container object in both the Container and Sandbox SDKs. These let you intercept outgoing requests by hostname (with basic glob matching), IP range, or all outbound traffic. Each call takes a WorkerEntrypoint, which acts as the entry point for the outbound Worker handling the intercepted requests.

Proxying happens entirely on the same machine running the sandbox VM. The connection between the container and the Worker doesn't use authentication, but the local nature of the communication keeps it secure.

You can invoke these methods before or after starting the container, and even while connections are active. Connections carrying multiple HTTP requests automatically switch to a new entrypoint when one is set, so updating an outbound Worker won't drop existing TCP connections or interrupt in-flight requests.

Local development through wrangler dev supports egress interception as well. Cloudflare handles this by spawning a sidecar process called proxy-everything inside the container's network namespace. The sidecar applies TPROXY nftable rules that route matching traffic to workerd, Cloudflare's open-source JavaScript runtime, which executes the outbound Worker. This keeps the local testing experience consistent with production behavior.

Getting started with outbound Workers

New users can follow the Getting Started guide for Cloudflare Sandboxes. Existing users of Containers or Sandboxes can begin using outbound Workers by reading the outbound traffic documentation and upgrading to @cloudflare/[email protected] or @cloudflare/[email protected].