AI agents · sandbox execution · code interpreter · credential safety
AI agent sandbox execution: credential safety in isolated code execution environments
Sandboxed code execution — E2B, Modal, Fly Machines, Docker containers with seccomp, Firecracker micro-VMs — provides strong OS-level isolation: the agent's generated code can't escape to the host filesystem, can't read other processes' memory, and (with network policies) can't reach arbitrary internet endpoints. But it can read its own environment variables. If you inject STRIPE_SECRET_KEY into the sandbox so the agent can call Stripe, that key is visible to every subprocess the agent spawns, every Python os.environ call, and every bash script the LLM generates. Sandbox isolation contains the OS. It doesn't contain the credential.
TL;DR
Issue a vault key before spawning the sandbox. Inject the vault key token (not the real Stripe secret) as STRIPE_SECRET_KEY=vk_xxx with the proxy base URL set to https://proxy.keybrake.com/stripe. Set the vault key TTL to match the maximum expected sandbox lifetime. If the vault key leaks — to logs, to the agent's output, to a subprocess — it can only be used for the specific vendor endpoints you allowed, up to the daily cap you set, and it expires automatically when the sandbox should have terminated.
What sandbox isolation actually protects against
A well-configured sandbox provides layers of isolation — but each layer has a different threat model:
| Threat | Protected by sandbox? | Protected by vault key? |
|---|---|---|
| Agent-generated code escapes to host filesystem | Yes — gVisor/Firecracker prevents this | N/A |
| Agent-generated code reads host network interfaces | Yes — network namespace isolation | N/A |
| Agent exfiltrates real Stripe key via env var read | No — the key is in the sandbox's own env | Yes — vault key is scoped, capped, and expires |
| Agent calls Stripe in a loop, burning $500 | No — sandbox allows outbound HTTPS | Yes — daily_usd_cap stops it at your limit |
| Agent calls Stripe admin endpoint to delete data | No — sandbox doesn't know about Stripe permissions | Yes — allowed_endpoints allowlist blocks disallowed endpoints |
| Real key persists after sandbox is destroyed | No — real key rotates separately | Yes — vault key TTL matches sandbox lifetime |
Pattern: inject vault key, not real key
Before spawning the sandbox, issue a vault key for the session and inject it as the API key the sandbox will use:
import os
import httpx
async def spawn_agent_sandbox(
user_id: str,
session_id: str,
max_spend_usd: int = 50,
max_lifetime_minutes: int = 30,
) -> dict:
# Issue a vault key scoped to this sandbox session
resp = httpx.post(
"https://api.keybrake.com/v1/keys",
headers={"Authorization": f"Bearer {os.environ['KEYBRAKE_TOKEN']}"},
json={
"label": f"sandbox-{user_id}-{session_id}",
"vendor": "stripe",
"daily_usd_cap": max_spend_usd,
"allowed_endpoints": ["/v1/payment_intents", "/v1/payment_intents/*"],
"expires_in": f"{max_lifetime_minutes}m",
},
timeout=3.0,
)
resp.raise_for_status()
vault_key = resp.json()
# Inject the vault token as STRIPE_SECRET_KEY
# Set the Stripe base URL to the Keybrake proxy
sandbox_env = {
"STRIPE_SECRET_KEY": vault_key["token"], # vault key, not real key
"STRIPE_API_BASE": "https://proxy.keybrake.com/stripe",
"KEYBRAKE_SESSION_ID": session_id,
}
# Spawn sandbox (E2B example — adapt for Modal, Fly, Docker, etc.)
sandbox = await Sandbox.create(
env_vars=sandbox_env,
timeout=max_lifetime_minutes * 60,
)
return {
"sandbox": sandbox,
"vault_key_id": vault_key["id"],
}
After the sandbox terminates (or on error), revoke the vault key explicitly — don't rely solely on TTL expiration:
async def teardown_agent_sandbox(sandbox, vault_key_id: str) -> None:
try:
await sandbox.kill()
finally:
# Explicit revocation — TTL is the safety net, not the primary mechanism
httpx.delete(
f"https://api.keybrake.com/v1/keys/{vault_key_id}",
headers={"Authorization": f"Bearer {os.environ['KEYBRAKE_TOKEN']}"},
timeout=3.0,
)
Configuring the sandbox's Stripe client to use the proxy
Most Stripe SDKs support overriding the base URL via an environment variable or constructor argument. In Python:
# Inside the sandbox — this is the agent-generated or agent-executed code
import stripe
import os
# stripe-python uses STRIPE_API_BASE if set
# Or configure explicitly:
stripe.api_base = os.getenv("STRIPE_API_BASE", "https://proxy.keybrake.com/stripe")
stripe.api_key = os.environ["STRIPE_SECRET_KEY"] # vault key token
# All stripe calls now go to proxy.keybrake.com — enforced at the proxy level
intent = stripe.PaymentIntent.create(amount=5000, currency="usd")
For Node.js inside the sandbox:
const Stripe = require('stripe');
// Override base URL to Keybrake proxy
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
host: 'proxy.keybrake.com',
protocol: 'https',
basePath: '/stripe',
});
const intent = await stripe.paymentIntents.create({
amount: 5000,
currency: 'usd',
});
Sandbox execution platforms and vault key integration
| Platform | How to inject vault key | How to match TTL to lifetime |
|---|---|---|
| E2B (Sandbox.create) | env_vars={"STRIPE_SECRET_KEY": vault_key["token"]} |
Set expires_in to match timeout parameter on Sandbox.create |
| Modal (Function.spawn) | Pass via modal.Secret.from_dict({"STRIPE_SECRET_KEY": token}) |
Set expires_in to match modal function timeout |
| Fly Machines (fly.api.machines.create) | Set in config.env dict in the machine config |
Set vault TTL to match machine auto_stop_timeout |
| Docker (docker run) | -e STRIPE_SECRET_KEY=vk_xxx |
Set vault TTL to container max lifetime; revoke in docker stop handler |
Defense-in-depth: vault key is one layer
Vault keys limit what the sandbox's API key can do. They don't replace sandbox-level network policies. The recommended defense-in-depth stack for agent sandbox execution:
- Network egress policy — use network namespaces or egress rules to restrict outbound traffic to only the endpoints the agent needs (
proxy.keybrake.com, your own APIs, approved data sources). Block all other outbound connections at the network layer. - Vault key endpoint allowlist — even if network egress is restricted to
proxy.keybrake.com, the allowlist ensures the agent can only call the Stripe endpoints it's supposed to call, not admin or data deletion endpoints. - Vault key spend cap — the daily cap stops runaway loops that generate valid charges. Network policies don't know about Stripe spend.
- Vault key TTL — even if a key leaks to logs or the agent's output, it expires when the sandbox should have terminated.
- OS sandbox isolation — gVisor/Firecracker/seccomp prevents the agent from escaping to the host, reading other processes' memory, or making privileged syscalls.
Related questions
Can agent-generated code inside the sandbox revoke its own vault key?
Yes — any code inside the sandbox that has the vault key token can call the Keybrake revoke endpoint (DELETE /v1/keys/:id) with the same token as Bearer auth. The vault key ID is not injected into the sandbox by default in the pattern above (only the token is), so agent-generated code can't easily revoke its own key unless it can read the key ID from somewhere. This is intentional: the sandbox should not be able to extend its own expiry or accidentally revoke its key before the task is done. If you want the agent to signal completion and trigger early revocation, pass the key ID as a separate env var (KEYBRAKE_KEY_ID) and document this as an explicit capability in the agent's system prompt.
How do I prevent the sandbox from calling Stripe directly instead of via the proxy?
Use network egress policies to block direct access to api.stripe.com. In a Docker/Kubernetes environment, add an egress NetworkPolicy that allows traffic to proxy.keybrake.com:443 but denies api.stripe.com. In E2B, you can't currently configure per-sandbox egress rules, so the defense relies on the vault key: even if the sandbox calls api.stripe.com directly with the vault token, it will receive an authentication error (the vault token is only valid for the Keybrake proxy, not for Stripe directly — it's a different credential format). Set STRIPE_API_BASE to force the SDK to the proxy; a determined agent could override this, but at that point network policy is the right defense layer.
What's the right TTL for a sandbox vault key?
Match the vault key TTL to the maximum expected sandbox lifetime plus a 20% buffer. If your sandbox has a 10-minute timeout, set the vault key TTL to 12 minutes. The TTL is a safety net for cases where explicit revocation fails — if the sandbox crashes, hangs, or is reaped without calling your teardown hook, the vault key still expires shortly after the sandbox should have terminated. Never set the TTL longer than "maximum sandbox lifetime × 2" — a key that outlives the sandbox by hours defeats the purpose of per-session scoping. Also set the TTL shorter than your real Stripe key rotation interval so that even a brute-forced vault token can't cause damage during the rotation window.
Further reading
- AI agent vendor isolation — blast radius reduction via credential separation, endpoint allowlists, and spend caps — the conceptual foundation for sandbox credential patterns.
- AI agent API key lifecycle — issuance, enforcement, expiration, and revocation phases — matching each phase to the sandbox lifecycle.
- AI agent circuit breaker — spend-aware circuit breakers for the orchestrator layer, complementing vault key enforcement inside the sandbox.
- Multi-tenant isolation — per-tenant vault key issuance patterns for SaaS platforms where each user runs their own sandbox.
- AI agent compliance — audit trail and least-privilege requirements for sandboxed AI agent execution in regulated environments.