
Build a Complete AI Agent Sandbox Platform with Governed Egress on Serverless MicroVMs
Open-source architecture that gives AI agents isolated Firecracker MicroVMs with governed egress, multi-tenant isolation, and Bedrock access — deployed to your AWS account via CDK.
Lambda MicroVMs give you isolated compute — a dedicated Firecracker VM per session, snapshot-based boot, suspend/resume. The AWS blog post Running self-hosted AI agent sandboxes with AWS Lambda MicroVMs shows how to use them as a sandbox provider for Anthropic's Claude Managed Agents work queue. That's the right starting point if CMA is your orchestrator.
But what if you need more? Multi-tenancy with infrastructure-level isolation. Governed egress where the sandbox has no internet routes at all. Bedrock access without sharing credentials. Persistent workspaces that survive session restarts. SDKs and MCP tools that work with any agent framework — Strands, LangChain, CrewAI, or your own.
That's what I built. The AWS Serverless Agent Sandbox is an open-source reference architecture that adds a full control plane on top of Lambda MicroVMs. It deploys to your account via CDK.
What it includes
- Control plane — API Gateway, Step Functions orchestrator, DynamoDB state management. The API handles session lifecycle (create, suspend, resume, terminate), connection credential minting, and idle/reaper policies.
- Governed egress — Sandbox subnets have zero internet routes. All outbound traffic goes through a Fargate-based forward proxy that evaluates every request against a DynamoDB-backed policy. Destinations you don't explicitly permit are rejected.
- SigV4 Bedrock proxy — Sandboxes call Amazon Bedrock through a resigning proxy. The sandbox never holds Bedrock credentials — the proxy signs requests with its own IAM role. The sandbox's execution role has an explicit IAM deny on all Bedrock actions, so even if code tries to call Bedrock directly via boto3, it fails.
- Multi-tenant isolation (opt-in) — Deploy with
-c deploymentProfile=multi-tenantto enable per-tenant DynamoDB partition keys, S3 prefix policies, and per-request IAM inline session policies. The default is single-tenant with AWS_IAM auth. - Suspend/resume — Full memory and disk snapshot, native to Lambda MicroVMs.
- Persistent workspace — S3 Files mount at
/mnt/workspace, per-user isolation via access points. - Python and TypeScript SDKs — Git operations, PTY support, streaming output, background processes.
- Six MCP tools — A single Lambda function exposing
execute_command,read_file,write_file,list_files,delete_file, andget_session_infoover MCP JSON-RPC. Session provisioning is automatic on first tool call — no explicit create/terminate tool calls needed.
The design decision that shaped everything else
The sandbox subnets have no route to the internet. That's the foundation.
Every other decision follows from it. Bedrock calls need a proxy because the sandbox can't reach the Bedrock endpoint directly. The proxy needs credential injection because the sandbox can't sign requests. Egress policy lives in DynamoDB because the proxy evaluates destinations per-request. The proxy runs on Fargate because it needs to be in the VPC but outside the sandbox subnet.
This is more restrictive than most sandbox architectures, and deliberately so. An untrusted agent can only reach destinations you've explicitly allowed.
Wiring up an agent
There are two integration patterns depending on where your agent runs.
Pattern 1: Agent drives the sandbox externally (MCP tools)
An agent running outside the sandbox (in your orchestrator, Lambda, or local machine) uses the six MCP tools over HTTP. The tools are deployed at
POST /tool on the same API Gateway, authenticated via SigV4. The agent calls execute_command, reads/writes files, and gets session info — without seeing credentials or session IDs.One thing that tripped me up: include an
x-session-key header on every call. Without it, each tool call provisions a new MicroVM. I learned this the hard way — a write_file followed by read_file hit different sandboxes, so the read came back empty. The session key ties consecutive calls to the same sandbox.1
2
3
4
5
6
# List available tools (note the x-session-key header)
curl -X POST https://xxx.execute-api.us-east-1.amazonaws.com/tool \
--aws-sigv4 "aws:amz:us-east-1:execute-api" \
-H "Content-Type: application/json" \
-H "x-session-key: my-conversation-001" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'Pattern 2: Agent code runs inside the sandbox
For agents that execute code inside the MicroVM, the repo includes an Agent Sandbox Guide . Feed this guide into your agent's system prompt or context so it knows the sandbox rules. The environment runs Amazon Linux 2023 on aarch64 with Python 3.13, Git, curl, and jq preinstalled.
Key things the guide covers:
- User code runs as uid 1000, not root. You can't
pip installglobally. UseHOME=/tmp pip install --target /tmp/pylibsand setPYTHONPATH. /tmpis ephemeral,/mnt/workspaceis persistent. Work in/tmpfor throwaway tasks,/mnt/workspacefor anything that should survive suspend/resume.- Bedrock calls go through the proxy, not boto3. The sandbox has no route to
.amazonaws.com, and the execution role has an explicit IAM deny on Bedrock actions. The guide includes a copy-paste Python helper that routes calls through the egress proxy's SigV4 resigning path.
Pattern 3: Direct SDK integration (no agent)
For applications that need isolated code execution without an agent framework — CI pipelines, data processing jobs, interactive platforms — use the Python or TypeScript SDK directly. No MCP, no agent loop. The SDK handles session lifecycle, connection credentials, command execution, file I/O, sandbox state and streaming.
1
2
3
4
5
6
7
8
from agent_sandbox import SandboxClient
client = SandboxClient(api_url="https://...", region="us-east-1")
with client.create_session() as session:
session.wait_ready()
session.write_file("/tmp/data.csv", csv_content)
result = session.execute("python3 /tmp/analyze.py")
output = session.read_file("/tmp/results.json")Five rounds of red-team audits
Security wasn't bolted on at the end. The architecture went through five rounds of autonomous red-team auditing, using AI agents (Claude and Kimi K3 via Strands Agents SDK) given full sandbox access with instructions to escalate privileges and exfiltrate data.
Important context: every finding below was in-guest privilege escalation — gaining the runtime's PID 1 capabilities inside the MicroVM. No finding crossed the Firecracker VM boundary or reached another session's data. The Firecracker isolation held throughout all five rounds. In-guest hardening still matters because PID 1 brokers privileged operations: NFS mounts, lifecycle hooks, and the egress proxy configuration. If untrusted code gains PID 1's capabilities, it can tamper with the runtime that mediates its own session.
Round 1 found that the IMDS endpoint (169.254.169.254) was reachable and the full Linux capability set was present. The iptables blocking for IMDS was attempted but later reverted because it broke S3 Files NFS resume (the efs-proxy shares the network namespace and needs IMDS). IMDS access is now an accepted residual risk — the execution role's credentials are narrowly scoped (tenant-prefix S3 only) and AWS API endpoints are network-unreachable from the sandbox (zero-route subnets). The capability set reduction stuck: bounding set drops for CAP_SYS_ADMIN, CAP_BPF, CAP_MKNOD, CAP_SYS_PTRACE, and others.
Round 2 was the scariest. Kimi K3 discovered the
core_pattern pipe handler — an in-guest privilege escalation where an attacker writes |/path/to/evil to /proc/sys/kernel/core_pattern, triggers a crash, and gets code execution with PID 1's capabilities. This worked because user code had CAP_SYS_ADMIN. Fix: read-only bind mount over core_pattern plus capset confinement.Round 3 verified the fixes. Confirmed
core_pattern returned EROFS. All bypass paths (umount, remount, fresh procfs, unshare, mknod) denied.Round 4 found a subtler in-guest bypass: user code ran as root, so it could overwrite system binaries in
/usr/bin. When the runtime later called one of those binaries (triggered via a suspend/resume cycle through the unauthenticated port 8080), the attacker's version ran with PID 1's full capabilities. Fix: user code now runs as uid 1000 with PR_SET_NO_NEW_PRIVS — the kernel clears all effective and permitted capabilities on the setuid transition, and NNP prevents restoring them through execve.Round 5 confirmed the uid-1000 hardening held. No privilege escalation achieved. The accepted residual findings are documented transparently in the repo's security posture .
What the audits taught me
The biggest lesson: the most dangerous capabilities are the ones that look necessary.
CAP_SYS_ADMIN was "needed" for NFS mounting — but user code didn't need it, only PID 1 did. The fix was to confine the child, not restrict the parent.The second lesson: red-team with AI agents, not just manual testing. The agents tried combinations I wouldn't have thought of — overwriting binaries then triggering a suspend/resume to get the runtime to exec them is not in any manual pentest checklist.
Multi-tenancy: addresses, not checks
The multi-tenant model doesn't check whether a session belongs to the caller. There is no
if tenant_id != session.tenant_id: return 404 anywhere in the code. Instead, every request's data access role pins dynamodb:LeadingKeys to exactly pk_for(principal) under an equality operator. Another tenant's partition is unaddressable at the IAM level.Property 13 in the test suite generates adversarial tenant pairs — non-ASCII identifiers, prefix-overlapping IDs, identifiers carrying the sort-key delimiter — and random sequences of interleaved API requests. It asserts that partition confinement holds at every point in every sequence. One finding from this testing: a tenant ID containing
/ is safe in DynamoDB (LeadingKeys compares by equality) but overlaps another tenant's S3 prefix. The property test caught this; the code now rejects / in tenant identifiers at the validation layer.Getting started
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
git clone https://github.com/aws-samples/sample-serverless-ai-sandbox.git
cd sample-serverless-ai-sandbox
uv sync --all-extras
# Build the MicroVM image (~3-5 min)
uv run python scripts/build-image.py --region us-east-1
# Prints: Image ARN: arn:aws:lambda:us-east-1:ACCOUNT:microvm-image:sandbox-runtime
# Build the egress proxy image (~3-5 min)
uv run python scripts/build-proxy.py --region us-east-1
# Prints: Image URI: ACCOUNT.dkr.ecr.us-east-1.amazonaws.com/sandbox-egress-proxy:latest
# Deploy all stacks with the image references
npx cdk deploy --all \
-c imageVersion=IMAGE_ARN \
-c proxyImageUri=IMAGE_URI \
--require-approval never
# Verify
uv run python -m demo \
--api-url https://xxx.execute-api.us-east-1.amazonaws.com \
--region us-east-1 \
--egress-endpoint xxx.elb.us-east-1.amazonaws.comThe Python SDK:
1
2
3
4
5
6
7
from agent_sandbox import SandboxClient
client = SandboxClient(api_url="https://...", region="us-east-1")
with client.create_session() as session:
session.wait_ready()
result = session.execute("echo 'Hello from sandbox'")
print(result.stdout)Custom MicroVM images let you preinstall toolchains (Node.js, R, data science stacks). Prewarming during image validation cuts first-command latency by about 41% — the OS pages in binaries from the snapshot during validation instead of on first use.
What this is and what it isn't
This is a reference architecture — a starting point, not a supported service. It deploys end-to-end and has been through multiple security review rounds, but work with your security team before production use.
Available where Lambda MicroVMs service is available.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article