seekrit
Docs/Cloudflare Sandbox

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.example gets no key.
tip

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
note

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 runsUse
Your build, migration, or test suitesetEnvVars() / exec({ env })
A coding agent, or model-generated codeAn outbound handler
Untrusted user submissionsAn 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