Secrets in Cloudflare Sandbox
Cloudflare Sandbox is the one provider on this list where the
credential-never-enters-the-sandbox shape needs no proxy at all. Outbound
handlers run in the Workers runtime, outside the container, and can attach a
credential to a request on its way past — which is exactly what
seekrit-proxy does elsewhere, built into the
platform.
Cloudflare's own documentation recommends it over injection, in as many words:
Use environment variables for non-secret configuration (paths, feature flags,
NODE_ENV, and similar). Do not put live API keys or other long-lived credentials into the sandbox.
So this page leads with that shape, and covers injection second.
Keep the credential in the Worker
Resolve in the Worker, hold the values there, and attach them in an outbound handler. The container never sees a key:
import { getSandbox, Sandbox } from '@cloudflare/sandbox';
import { Seekrit } from '@seekrit/sdk';
export class AgentSandbox extends Sandbox {}
AgentSandbox.outboundByHost = {
'api.openai.com': async (request: Request, env: Env) => {
const secrets = await new Seekrit({ token: env.SEEKRIT_TOKEN }).resolve();
const authed = new Request(request);
authed.headers.set('authorization', `Bearer ${secrets.OPENAI_API_KEY}`);
return fetch(authed);
},
};
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const sandbox = getSandbox(env.AgentSandbox, 'user-123');
// No credential passed in — the agent calls api.openai.com with no key and
// the handler supplies one on the way out.
const proc = await sandbox.exec('python agent.py');
return Response.json({ output: await proc.output({ encoding: 'utf8' }) });
},
} satisfies ExportedHandler<Env>;
What this buys, and it is the whole argument for the pattern:
- Nothing to steal. The container holds no key and no seekrit token, so prompt injection, a leaked log, or arbitrary code execution inside the sandbox yields nothing.
- Rotation lands immediately. The next request picks up whatever the Worker resolves; there is no sandbox to restart and no value to re-inject.
- The host is the boundary. A handler is registered per host, so a
credential can only ever be attached to a request going to that host. An agent
that POSTs your key's worth of traffic at
evil.examplegets no key.
Cache the resolve rather than doing it per request. A Worker isolate can hold
the resolved map in module scope, or you can keep it on the Durable Object —
/v1/resolve is metered, and a busy agent makes a lot of outbound calls. Do
not cache it into the container.
Per-instance credentials work the same way, using ctx.containerId to pick which
secret an instance gets — useful when each tenant's agent should carry its own
key:
AgentSandbox.outboundByHost = {
'api.example.com': async (request, env, ctx) => {
const secrets = await new Seekrit({ token: env.SEEKRIT_TOKEN }).resolve();
const authed = new Request(request);
authed.headers.set('x-api-key', secrets[`TENANT_${ctx.containerId}_KEY`]);
return fetch(authed);
},
};
Inject into the container
When the code inside is yours — a build step, a test run — injection is fine and simpler. Resolve in the Worker and set the variables before anything runs:
const sandbox = getSandbox(env.Sandbox, 'build-42');
const secrets = await new Seekrit({ token: env.SEEKRIT_TOKEN }).resolve();
await sandbox.setEnvVars({
DATABASE_URL: secrets.DATABASE_URL,
NODE_ENV: 'production',
});
await sandbox.exec('npm run migrate');
Or per command, which keeps a value out of the steps that do not need it:
await sandbox.exec('node seed.js', {
env: { DATABASE_URL: secrets.DATABASE_URL },
});
await sandbox.exec('node report.js'); // no credential
Call setEnvVars() before other sandbox operations — the SDK merges stored
names in at each exec() launch, so a command that started earlier does not
get them. The stored names live in the sandbox Durable Object's memory, not on
the container filesystem, so after the DO is evicted you must set them again
(or pass env per launch).
Named keys, not the whole resolved map — see Pick names, not the whole environment.
Which one, concretely
| The sandbox runs | Use |
|---|---|
| Your build, migration, or test suite | setEnvVars() / exec({ env }) |
| A coding agent, or model-generated code | An outbound handler |
| Untrusted user submissions | An outbound handler, and unset anything you injected earlier |
A useful tell: if you would be unhappy to find the value printed in the sandbox's stdout, it belongs in the Worker.
See also
- Agent sandboxes — the two shapes and when each is right
- Agent proxy — the same pattern for every other runtime
- Sync to Cloudflare — for Workers, Pages, and Secrets Store, which are the other problem