Runtime

Why your agent's sandbox shouldn't reach the whole internet

Give an agent's sandbox an allow list of the few hosts its task needs, because every other reachable host is a place your data can go.

Runtime (withruntime.com) enforces each sandbox's allow list in a proxy on the host, outside the Firecracker microVM, so even root inside the sandbox cannot widen it, and a change applies at once, to connections already open. This post explains what open internet access gives an attacker, why an allow list beats a block list, which hosts real agent tasks need, and why some allowed hosts are still a way out. It ends with a probe you can run against your own list.

What can an agent do with open internet access?

Anything a program on the internet can do, with your account's name on it. An agent with a shell runs commands a model chose, and some of those choices come from text an attacker wrote. With open outbound access, an injected command can:

What the command does What it costs you
Uploads the repository to a request catcher Your source code, on someone else's server
Sends env or .env files out Every key the sandbox could read
Downloads and runs a second-stage script Whatever that script does, with the agent's access
Joins a cryptocurrency mining pool Your CPU bill, and an abuse report
Scans or attacks other sites Abuse reports traced to your account
Installs a typo-squatted package Malware inside your build, then inside your product

Isolation stops a bad command from reaching your machine. Network rules stop it from reaching everyone else, and from carrying your data with it. The two are separate defenses, and an agent needs both (what egress control is).

Why is an allow list better than a block list?

A block list names the places code may not go, and the internet has more places than you can name. An attacker who finds pastebin.com blocked uses paste.ee, a fresh domain registered this morning, or a bare IP address. Every block list is one step behind.

An allow list names the places code may go, and the agent's task decides how short it is. A coding agent that installs Python packages and clones from GitHub needs four or five hosts. Anything else is refused, including the domain registered this morning.

The trade-off is maintenance: a task that needs a new host fails until you add it. That failure is loud and immediate, which is what you want. A block list's failure is silent and arrives later.

Which hosts does an agent task actually need?

Fewer than you expect. These are the hosts the common package managers and code hosts use for downloads:

Task Hosts to allow
pip install pypi.org, files.pythonhosted.org
npm install registry.npmjs.org
cargo build index.crates.io, static.crates.io
go mod download proxy.golang.org, sum.golang.org
git clone from GitHub github.com, plus *.githubusercontent.com for raw files and releases
docker pull from Docker Hub registry-1.docker.io, auth.docker.io, production.cloudflare.docker.com
Calling a model from inside api.anthropic.com or api.openai.com

To find a task's list, start narrow and run it. A refused HTTPS connection fails at once: curl prints CONNECT tunnel failed, response 403, and a plain HTTP request comes back with an X-Runtime-Egress: rule-not-allowed header. Add the host if the task truly needs it, and run again. Most lists settle after two or three runs.

On Runtime, allow takes domains, *.domain for every name under one, addresses and CIDR ranges, and deny always wins, so you can allow a wildcard and carve one name out of it (allow only some hosts).

Why is an allowed host still a way out?

Because some hosts accept uploads as well as downloads, and an injection can bring its own credentials. The text that tells your agent to run a command can also contain the attacker's token. Read your list with the attacker's eyes:

Allowed host What your task does there What an attacker could do there
pypi.org, files.pythonhosted.org Download packages Nothing useful: uploads go to upload.pypi.org, a different host
registry.npmjs.org Download packages npm publish your code as a package, with their own token
github.com Clone, maybe push Push your code to their repository, with their own token
*.githubusercontent.com Download raw files Nothing useful: it serves files, it does not accept them
registry-1.docker.io Pull images Push an image holding your code, with their own login
api.anthropic.com Call the model Send your code as a prompt to their account, with their own key

Three ways to close those channels, from strongest to weakest:

  1. Open the network for setup, close it for the agent. Install everything first, then cut the list down before the agent runs a single command. The next section shows it.
  2. Call the model from outside the sandbox. If your agent loop runs on your server and only sends commands into the sandbox, the sandbox never needs the model's host at all.
  3. Use a read-only mirror. A package mirror you run, which serves downloads and refuses uploads, replaces a public registry that does both.

For the hosts you must keep, store your own credentials as secrets the sandbox never holds, scoped by method and path (secrets sandboxes never see). That stops your token from being used for the wrong thing; the steps above stop the attacker's.

How do you open the network for setup and close it for the agent?

Create the sandbox with the hosts setup needs, do the setup, then change the rules before the agent's first command. On Runtime a change applies at once, to connections already open too, and the files and processes setup left behind are untouched:

TypeScriptimport { Sandbox } from "withruntime";const SETUP = ["pypi.org", "files.pythonhosted.org", "github.com", "*.githubusercontent.com"];await using sbx = await Sandbox.create({ network: { internet: true, allow: SETUP } });await sbx.exec("git clone --depth 1 https://github.com/pallets/flask.git app", {  check: true,  timeoutMs: 300_000,});await sbx.exec("pip install --quiet -e ./app pytest", { check: true, timeoutMs: 300_000 });// Setup is over. From here on, the code the agent writes reaches nothing.await sbx.network.set({ internet: false });const tests = await sbx.exec("python3 -m pytest -q -x", {  cwd: "/workspace/app",  timeoutMs: 600_000,});console.log(tests.exitCode, await sbx.network.get());
Pythonfrom withruntime import SandboxSETUP = ["pypi.org", "files.pythonhosted.org", "github.com", "*.githubusercontent.com"]with Sandbox.create(network={"internet": True, "allow": SETUP}) as sbx:    sbx.exec("git clone --depth 1 https://github.com/pallets/flask.git app", check=True, timeout_ms=300_000)    sbx.exec("pip install --quiet -e ./app pytest", check=True, timeout_ms=300_000)    # Setup is over. From here on, the code the agent writes reaches nothing.    sbx.network.set(internet=False)    tests = sbx.exec("python3 -m pytest -q -x", cwd="/workspace/app", timeout_ms=600_000)    print(tests.exit_code, sbx.network.get())

If the agent later needs one more package, open the list for that install and close it again, from your code, not from the agent's. The agent asks; your code decides. That keeps the decision outside the machine where the injected text lives.

A sandbox can stay closed and still be useful for a long time. Tests, builds, linters and type checkers run offline once dependencies are installed, and those are most of what a coding agent runs.

Where should the rule be enforced?

Outside the machine the agent controls. A rule the agent's own machine enforces is a rule the agent's commands can remove:

Where the rule lives What undoes it
An HTTP_PROXY setting in the sandbox unset HTTP_PROXY, or any program that ignores it
Firewall rules inside the sandbox Root inside the sandbox, which most agent images grant
A filtering proxy container beside it Any other route out that nobody closed
A proxy on the host, outside the VM Nothing inside the sandbox

A Runtime sandbox has no network card of its own. Every connection leaves through the host's proxy, which applies the sandbox's rules to programs that honor proxy settings and to programs that open raw sockets alike. Root inside the sandbox can change its own firewall all day; the rules live somewhere it cannot reach. Private and internal addresses, the cloud metadata address among them, are refused for every sandbox whatever its list says (the network).

How do you test an allow list?

Probe it from inside, with the hosts an attacker would reach for. Request catchers, paste sites and DNS-over-HTTPS resolvers are the usual choices, because each one accepts arbitrary data over ordinary HTTPS:

TypeScriptimport { Sandbox } from "withruntime";const allow = ["pypi.org", "files.pythonhosted.org", "github.com"];const probes = [  "pypi.org", // on the list  "github.com", // on the list  "example.com", // not on the list  "webhook.site", // a request catcher  "pastebin.com", // a paste site  "dns.google", // DNS over HTTPS can carry anything  "1.1.1.1", // a bare address, which slips past many name filters];await using sbx = await Sandbox.create({ network: { internet: true, allow } });for (const host of probes) {  const command = `curl -s -o /dev/null -m 5 https://${host}/ && echo reachable || echo refused`;  const probe = await sbx.exec(command, { timeoutMs: 15_000 });  console.log(`${host.padEnd(14)} ${probe.stdout.trim()}`);}
Pythonfrom withruntime import Sandboxallow = ["pypi.org", "files.pythonhosted.org", "github.com"]probes = [    "pypi.org",  # on the list    "github.com",  # on the list    "example.com",  # not on the list    "webhook.site",  # a request catcher    "pastebin.com",  # a paste site    "dns.google",  # DNS over HTTPS can carry anything    "1.1.1.1",  # a bare address, which slips past many name filters]with Sandbox.create(network={"internet": True, "allow": allow}) as sbx:    for host in probes:        command = f"curl -s -o /dev/null -m 5 https://{host}/ && echo reachable || echo refused"        probe = sbx.exec(command, timeout_ms=15_000)        print(f"{host:<14} {probe.stdout.strip()}")

On Runtime the two listed hosts print reachable and the other five print refused, the bare address included, since an address is matched against the list like a name. Run the same probe wherever your agent runs today. Any reachable you did not expect is a place your data can go, and it belongs in a test that fails the day someone widens the list. For a fuller drill that plants an injection and plays a model that obeys it, see how prompt injection becomes code execution.

In short

  • Open internet access lets an injected command send your data anywhere and act in your name.
  • An allow list beats a block list, because an attacker always has a host you did not block.
  • Some allowed hosts accept uploads with an attacker's own token: read the list with their eyes.
  • Open the network for setup, then close it before the agent runs, and change it from your code, not the agent's.
  • Enforce the rule outside the machine the agent controls, and probe it from inside.

Run it on Runtime

Runtime costs 42% to 88% less than fourteen other sandbox providers for an agent that mostly waits on a model (compare costs). Every Runtime sandbox reaches the internet only through a proxy on its host, takes an allow list at create or at any time after, and refuses private addresses whatever the list says. A new sandbox runs its first command 221 ms after the create request on Runtime's servers, at $0.025 per vCPU-hour of CPU used. Start with 100 free hours, no card: sign in, or read turn off a sandbox's internet and get started.

Your first 100 hoursare on us.

  • No credit card
  • Eight sandboxes at once, 2 vCPU and 4 GiB each
  • Then prepaid credit from $10, no plan fee
Claim 100 hours free