Let code in a sandbox call an API that needs a token, while the token stays in your Worker. Requests from the sandbox reach an entrypoint in your Worker, which allows only the requests you choose and adds the token. In this example, the code reads your profile from the GitHub API.
- A Worker with a Durable Object that starts a container with the Durable Object scheduling policy, or a Worker that loads Dynamic Workers with a Worker Loader binding named
LOADER. To create one, refer to Run a Linux command or Run JavaScript. - A GitHub access token that can read your profile.
-
In
wrangler.jsonc, declare the token as a secret, then generate types:{ "secrets": { "required": ["GITHUB_TOKEN"], }, }[secrets] required = [ "GITHUB_TOKEN" ]npx wrangler typesyarn wrangler typespnpm wrangler types -
Store the token when Wrangler prompts for it:
npx wrangler secret put GITHUB_TOKENyarn wrangler secret put GITHUB_TOKENpnpm wrangler secret put GITHUB_TOKEN
The container sends its request to api.github.com as usual. The Durable Object intercepts HTTPS requests to that hostname and delivers them to an entrypoint in your Worker, which adds the token.
-
Add an entrypoint to your Worker that accepts one GitHub API request and adds the token:
src/index.jsjs import { WorkerEntrypoint } from "cloudflare:workers"; export class GitHubGateway extends WorkerEntrypoint { async fetch(request) { const url = new URL(request.url); if ( request.method !== "GET" || url.hostname !== "api.github.com" || url.pathname !== "/user" || url.search !== "" ) { return new Response("Forbidden", { status: 403 }); } return fetch("https://api.github.com/user", { headers: { Accept: "application/vnd.github+json", Authorization: `Bearer ${this.env.GITHUB_TOKEN}`, "User-Agent": "cloudflare-sandbox", "X-GitHub-Api-Version": "2022-11-28", }, }); } }src/index.tsts import { WorkerEntrypoint } from "cloudflare:workers"; export class GitHubGateway extends WorkerEntrypoint<Env> { async fetch(request: Request): Promise<Response> { const url = new URL(request.url); if ( request.method !== "GET" || url.hostname !== "api.github.com" || url.pathname !== "/user" || url.search !== "" ) { return new Response("Forbidden", { status: 403 }); } return fetch("https://api.github.com/user", { headers: { Accept: "application/vnd.github+json", Authorization: `Bearer ${this.env.GITHUB_TOKEN}`, "User-Agent": "cloudflare-sandbox", "X-GitHub-Api-Version": "2022-11-28", }, }); } }The entrypoint allows only
GET /user, so code in the sandbox cannot use the token for anything else. Allow each request your code needs, and nothing more. -
Add a method to your Durable Object that intercepts
api.github.comand runs code that calls it:src/index.tsts export class MyContainer extends DurableObject<Env> { // ... async getUsername(): Promise<string> { const container = this.ctx.container; if (!container) { throw new Error("The container binding is not configured"); } await container.interceptOutboundHttps( "api.github.com", this.ctx.exports.GitHubGateway, ); if (!container.running) { container.start({ image: "cloudflare/debian-trixie", entrypoint: ["sleep", "infinity"], // The container can reach only the hostnames you intercept enableInternet: false, }); } const process = await container.exec( [ "node", "--input-type=module", "--eval", `const response = await fetch("https://api.github.com/user"); if (!response.ok) throw new Error("GitHub request failed"); console.log((await response.json()).login);`, ], { env: { // The intercept terminates TLS with a certificate that the container // CA certificate signs. Node.js reads the CA certificate from this path NODE_EXTRA_CA_CERTS: "/etc/cloudflare/certs/cloudflare-containers-ca.crt", }, }, ); const output = await process.output(); if (output.exitCode !== 0) { throw new Error(new TextDecoder().decode(output.stderr)); } return new TextDecoder().decode(output.stdout).trim(); } } -
Add a route to your Worker that returns the username:
src/index.tsts if (url.pathname === "/username") { const sandbox = env.MY_CONTAINER.getByName("sandbox"); return Response.json({ username: await sandbox.getUsername() }); } -
Deploy your Worker, then send a request to
/usernameon theworkers.devURL that Wrangler prints:npx wrangler deployyarn wrangler deploypnpm wrangler deploycurl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/username{ "username": "octocat" }
A Dynamic Worker gets a method that calls one fixed GitHub endpoint, instead of the token.
-
Add an entrypoint to your Worker with a method that calls GitHub with the token:
src/index.jsjs import { WorkerEntrypoint } from "cloudflare:workers"; export class GitHub extends WorkerEntrypoint { async getUsername() { const response = await fetch("https://api.github.com/user", { headers: { Accept: "application/vnd.github+json", Authorization: `Bearer ${this.env.GITHUB_TOKEN}`, "User-Agent": "cloudflare-sandbox", "X-GitHub-Api-Version": "2022-11-28", }, }); if (!response.ok) { throw new Error(`GitHub returned ${response.status}`); } const user = await response.json(); return user.login; } }src/index.tsts import { WorkerEntrypoint } from "cloudflare:workers"; export class GitHub extends WorkerEntrypoint<Env> { async getUsername(): Promise<string> { const response = await fetch("https://api.github.com/user", { headers: { Accept: "application/vnd.github+json", Authorization: `Bearer ${this.env.GITHUB_TOKEN}`, "User-Agent": "cloudflare-sandbox", "X-GitHub-Api-Version": "2022-11-28", }, }); if (!response.ok) { throw new Error(`GitHub returned ${response.status}`); } const user = await response.json<{ login: string }>(); return user.login; } } -
In the
fetch()handler of your Worker, pass the entrypoint to the Dynamic Worker as a binding. The handler needs itsctxargument forctx.exports:src/index.tsts const sandbox = env.LOADER.load({ compatibilityDate: "$today", mainModule: "code.js", modules: { "code.js": ` import { WorkerEntrypoint } from "cloudflare:workers"; export class Code extends WorkerEntrypoint { async run() { return this.env.GITHUB.getUsername(); } } `, }, env: { // Create a stub that the Dynamic Worker can receive GITHUB: ctx.exports.GitHub({ props: {} }), }, // Block every other `fetch()` and `connect()` call from the Dynamic Worker globalOutbound: null, }); const username = await sandbox .getEntrypoint<WorkerEntrypoint & { run(): Promise<string> }>("Code") .run(); return Response.json({ username });The code calls
this.env.GITHUB.getUsername(), and the method reads the token in your Worker. -
Deploy your Worker, then send a request to the
workers.devURL that Wrangler prints:npx wrangler deployyarn wrangler deploypnpm wrangler deploycurl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev{ "username": "octocat" }
Authenticate callers to your Worker first, so other people cannot use the quota of your token. For more information, refer to Sandbox security.
- Clone a private repository: the same pattern for Git.
interceptOutboundHttps: intercept other Container destinations.- Bindings: pass other methods to a Dynamic Worker.
- Egress control: restrict or audit requests from a Dynamic Worker.
- Secrets: rotate or manage the token.