Move outbound rules
Last updated Sep 30, 2026
In Sandbox SDK 0.12, ContainerProxy applies the outbound fields of your Sandbox class to each request that the container sends. In 1.0, your Durable Object registers your own WorkerEntrypoint for HTTP and HTTPS requests from the container, and the entrypoint applies the same rules in the same order.
Before you start
Your 1.0 class replaces the 0.12 Sandbox class, and has the container getter, the ensureRunning() and startContainer() methods, and the ENV and INACTIVITY_TIMEOUT_MS constants from Replace the Sandbox class. The entrypoint uses this.ctx.exports, which needs compatibility date 2025-11-17 or later, or the enable_ctx_exports flag. 0.12 outbound rules needed it too.
This page replaces enableInternet, interceptHttps, allowedHosts, deniedHosts, outbound, outboundByHost, outboundHandlers, their aliases outboundProxy and outboundProxies, the runtime setters such as allowHost() and setOutboundByHost(), and gitCheckout(). Remove ContainerProxy from your imports and from the exports of your Worker. For the handlers that 0.12 added for bucket mounts, refer to Move bucket mounts.
Apply the rules in an entrypoint
- Copy the outbound fields of your 0.12 class into constants, and give each handler a name:
src/index.tsts
```ts type Handler = { method: string; params?: unknown };
type OutboundHandler = (
request: Request,
env: Env,
params: unknown,
) => Response | Promise
// The outbound fields of your 0.12 class.
const ENABLE_INTERNET = false;
const ALLOWED_HOSTS: string[] | undefined = [
"github.com",
"registry.npmjs.org",
"api.example.com",
];
const DENIED_HOSTS: string[] | undefined = undefined;
const OUTBOUND_BY_HOST: Record
// Your 0.12 handlers, by name.
const HANDLERS: RecordBearer ${env.API_TOKEN});
return fetch(new Request(request, { headers }));
},
};
```
If your 0.12 class did not set enableInternet, set ENABLE_INTERNET to true, the 0.12 default. For a field that your class did not set, use {} for OUTBOUND_BY_HOST and undefined for the other constants.
Put every function from outboundHandlers, outboundByHost, and outbound in HANDLERS, and refer to them by name in OUTBOUND_BY_HOST and OUTBOUND. A 0.12 handler takes (request, env, ctx). Change it to take (request, env, params), and read params where it read ctx.params.
2. Add WorkerEntrypoint to your cloudflare:workers import, and add the entrypoint, which checks each request in the order that 0.12 used:
src/index.tsts
```ts import { WorkerEntrypoint } from "cloudflare:workers";
// The shape that 0.12 stored under OUTBOUND_CONFIGURATION.
type StoredRules = {
outboundByHostOverrides?: Record
// "" matches any run of characters, as in 0.12.
function matches(pattern: string, hostname: string): boolean {
const escaped = pattern
.split("")
.map((part) => part.replace(/[.+?^${}()|[]\]/g, "\$&"));
return new RegExp(^${escaped.join(".*")}$).test(hostname);
}
function lookup(
handlers: Record
export class Outbound extends WorkerEntrypoint
if (denied && listed(denied)) { return blocked; }
if (allowed && !listed(allowed)) { return blocked; }
const handler = [ lookup(rules.outboundByHostOverrides, hostname), lookup(OUTBOUND_BY_HOST, hostname), rules.outboundHandlerOverride, OUTBOUND, ].find((handler) => handler && HANDLERS[handler.method]);
if (handler) { const run = HANDLERS[handler.method]; return run(request, this.env, handler.params); }
if (allowed || ENABLE_INTERNET) { return fetch(request); }
return blocked; } } ```
The entrypoint blocks denied hostnames, then hostnames outside the allowed list. Next, it runs the first handler that matches, in this order: a hostname handler that the sandbox set at runtime, a hostname handler from your class, the catch-all handler set at runtime, and the catch-all handler of your class. With no handler, it sends the request to the Internet if you set an allowed list or allow the Internet. A blocked request gets status 520 with the body Origin is disallowed, as in 0.12.
The props carry the rules that the sandbox set at runtime, in the shape 0.12 stored them. Like 0.12, the entrypoint skips a handler whose name is not in HANDLERS, such as the handlers that 0.12 stored for bucket mounts.
3. Add the certificate variables to ENV, and register the entrypoint in startContainer():
src/index.tsts
``ts
const RULES = "OUTBOUND_CONFIGURATION";
const CA = "/etc/cloudflare/certs/cloudflare-containers-ca.crt";
const BUNDLE = "/etc/ssl/certs/ca-certificates.crt";
const ENV = {
NODE_ENV: "test",
NODE_EXTRA_CA_CERTS: CA,
REQUESTS_CA_BUNDLE: BUNDLE,
};
// Keep a copy of the system bundle, wait up to 10 seconds for the
// certificate, then write the bundle from the copy and the certificate.
const SYSTEM = "/etc/ssl/certs/ca-certificates.system.crt";
const CA_WAIT =until [ -s ${CA} ]; do sleep 0.1; done;
const CA_READY =timeout 10 sh -c '${CA_WAIT}';
const TRUST = [
"sh",
"-c",[ -e ${SYSTEM} ] || { cp ${BUNDLE} ${SYSTEM}.tmp && mv ${SYSTEM}.tmp ${SYSTEM}; }
${CA_READY} && cat ${SYSTEM} ${CA} >${BUNDLE}.tmp && mv ${BUNDLE}.tmp ${BUNDLE}`,
];
export class MySandbox extends DurableObject
private rules(): StoredRules {
return this.ctx.storage.kv.get
private async intercept(): Promise
private async trust(): Promise
if ((await trust.exitCode) !== 0) { throw new Error("The container did not trust the intercept CA"); } }
private async startContainer(): Promise
if (newContainer) { this.container.start({ image: this.container.images.sandbox, instance: "standard-1", env: ENV, enableInternet: ENABLE_INTERNET, }); }
try { if (newContainer) { // Replaces onStart(). await this.ctx.storage.put("startedAt", Date.now()); } await this.intercept(); await this.trust(); await this.container.setInactivityTimeout( INACTIVITY_TIMEOUT_MS, ); } catch (error) { // The next request starts a new container. await this.container.destroy(); throw error; } } } ```
// ... stands for the constructor, the container getter, and your other methods. rules() reads the key where 0.12 stored the rules that each sandbox set at runtime, so those rules keep applying after the switch. If you move sandboxes side by side, copyFrom0x() copies the key into the 1.0 class.
The intercepts send every request on port 80, and every HTTPS request on port 443, to the entrypoint. enableInternet keeps the 0.12 value and still decides connections on other ports. An intercept lasts until the container stops. startContainer() registers it for each new container, and again when a restarted Durable Object finds the container running. Registering it again replaces the earlier registration, so a container whose setup a deploy interrupted still gets its intercepts before the next command.
Register other intercepts, such as bucket mounts, before intercept(). A hostname intercept that you register after intercept() never receives requests, because the Outbound entrypoint receives them. For more information, refer to If you moved outbound rules.
