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:

ThreatProtected 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

PlatformHow to inject vault keyHow 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:

  1. 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.
  2. 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.
  3. Vault key spend cap — the daily cap stops runaway loops that generate valid charges. Network policies don't know about Stripe spend.
  4. Vault key TTL — even if a key leaks to logs or the agent's output, it expires when the sandbox should have terminated.
  5. OS sandbox isolation — gVisor/Firecracker/seccomp prevents the agent from escaping to the host, reading other processes' memory, or making privileged syscalls.

Get early access

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