seekrit
Docs/Vercel Sandbox

Secrets in Vercel Sandbox

Vercel Sandbox runs Firecracker microVMs you create from your own code, so seekrit resolves in that code and hands the microVM only what it needs. It also has the one feature that makes the no-credential shape genuinely enforceable: networkPolicy: 'deny-all'.

Inject at creation

import { Sandbox } from '@vercel/sandbox';
import { Seekrit } from '@seekrit/sdk';

const secrets = await new Seekrit({ token: process.env.SEEKRIT_TOKEN }).resolve();

const sandbox = await Sandbox.create({
  env: {
    OPENAI_API_KEY: secrets.OPENAI_API_KEY,
    TAVILY_API_KEY: secrets.TAVILY_API_KEY,
  },
});

env sets the defaults for every command the sandbox runs. Named keys, not env: secrets — see Pick names, not the whole environment.

Inject per command

Per-command env overrides the creation-time defaults, which is how you keep a key out of the steps that do not need it:

await sandbox.runCommand({
  cmd: 'node',
  args: ['summarize.js'],
  env: { OPENAI_API_KEY: secrets.OPENAI_API_KEY },
});

// No key in this one.
await sandbox.runCommand({ cmd: 'node', args: ['render.js'] });
note

Sandbox.fork() copies the source sandbox's environment variables into the new one, so a forked sandbox inherits any key you injected. Pass env on the fork to override them, and remember that a persistent sandbox auto-snapshots its configuration on stop, so the values come back on resume.

Keep the credential outside the microVM

This is the shape to use for agent output, and Vercel Sandbox makes it enforceable rather than advisory. Create the sandbox with no egress at all, then give it one route out — the proxy:

const sandbox = await Sandbox.create({
  networkPolicy: 'deny-all',
  env: {
    // A placeholder, not a key. Worthless if it leaks.
    OPENAI_API_KEY: '{{seekrit:OPENAI_API_KEY}}',
    OPENAI_BASE_URL: 'https://proxy.internal.example/openai',
  },
});
# seekrit-proxy.toml — wherever you run the proxy, never in the sandbox
[[route]]
prefix = "/openai"
upstream = "https://api.openai.com"
allow = ["OPENAI_API_KEY"]
methods = ["POST"]
paths = ["/v1/chat/completions", "/v1/embeddings"]

Why this combination is stronger than either half:

  • deny-all makes the proxy the only way out. Elsewhere an OPENAI_BASE_URL pointing at the proxy is a suggestion the agent can ignore by calling api.openai.com directly — with a placeholder that fails, but a stolen key would work. With no egress, there is nothing to fall back to.
  • The allowlist makes the route narrow. Even through the proxy, the key reaches one upstream, one method, two paths. An agent that decides to spend your model budget on a different endpoint is refused, and the refusal is logged.
  • The microVM holds no seekrit token, so a compromise yields a placeholder string and a URL.
caution

deny-all blocks the sandbox's package installs too. Bake dependencies into a custom image rather than installing at runtime — and if you widen the network policy to reach a registry, the argument above weakens by exactly the reach you added back.

See also