plimsoll trainer · 08 / calling your API from the sandbox
Code you do not trust needs to read from your API, and your API needs a key. Everything on this page follows from one rule: the key never enters the room with the code, the room being the sandbox the code runs in. The code asks for a route; the broker, the part of plimsoll that runs outside the sandbox on the host machine, checks the route against a short list, attaches the key itself, and sends the call on to your API. Even fully hostile code in the room holds nothing worth stealing.
The room has no network. One private line leads out of it to the broker, which holds the run's grant, its permission to call a listed set of routes on one API, and a credential minted for this run, meaning created fresh for it alone. The room changes with the provider, the backend that runs the code; the broker does not. Pick one below: a docker container; a JavaScript engine compiled to WebAssembly, a portable bytecode format, running inside the plimsoll process (the wasm provider); or a small virtual machine on E2B, a hosted service that starts one per run (e2b).
The room
A session keeps one sandbox open for many calls. A cell, code
the session runs in an interpreter it keeps alive between calls, carries no grant at
all: the interpreter outlives the call, and its code with it. A snippet or project call
in a docker session can carry one, with one difference from a single run. A running
container cannot gain a mount, so the session mounts the broker's socket once, when it
opens. The socket serves a call's grant only while that call runs, and answers 503
between calls and to a call without a grant. While it serves a grant, it serves
anything in the container, so code an earlier call left running (a timer inside an
interpreter, a process it started between calls) can use a later call's grant. On
openshell the relay a granted call starts inside the sandbox can be
reached the same way. So a session refuses a granted call before anything runs unless
its profile sets allow_in_sessions; turn that on only for an API whose
access may be shared with every call of the session.
The broker approves a path only if it is, byte for byte, the path that will be sent. Type a path, or tap one of the attempts, and read the verdict the broker would give against the list of allowed routes below. The page applies the daemon's own rules, in the daemon's order.
Why the path checked has to be the path sent. A guest, the code
running in the sandbox, could write /v1/items/%2e%2e/admin: the broker
would see a harmless-looking segment, and your API's server would decode it to
.., the parent directory. Or it could write
/v1/items/42?x=/stock: the list would match it against the longer
template /v1/items/*/stock (a route pattern, where * stands
for one segment), while the request actually reaches the shorter route, because Go's
net/http strips everything after the ?. So any path not
already in its final form (decoded, clean, and with nothing an HTTP client would
re-encode) is refused before matching, and the path approved is the path sent. The
call trace, the broker's log of the run's calls, records the template that matched,
never the path itself.
If a word on this page loses you, it is one of these.
Each limit is generous for honest code and stops a loop that has run away; telling those two apart is all a per-run cap can usefully do.
max_calls, never past 100,000.With docker the room is on the same machine as plimsoll, so a socket can reach under the door. An E2B microVM runs on E2B's servers, so there is no hallway to run a line down. The rule does not change, so the plumbing has to.
E2B_GUARD_URL. The guard hands each call to the same broker every other provider uses.E2B_GUARD_URL unset, an E2B grant is refused outright rather than downgraded to something weaker.make e2b-guard-live proves the path through the guard, and it fails rather than skips when its settings are missing, so a pass can only come from having actually used that path.Docker Cloud Sandboxes reaches the guard too. The other VM provider,
dockercloud, runs each sandbox on Docker-managed compute with its network
policy at deny-all, read back and checked on every run. With SANDBOX_DOCKERCLOUD_GUARD_URL set, a run
with a grant gets exactly one network rule, the guard’s host:443, and the provider reads
it back before any code runs. Two things differ from E2B. The rule is applied through a
Docker call outside its published API (the one its own sbx CLI makes). And
Docker’s proxy only injects credentials for its own fixed list of services, so the guest
holds its own run’s guard credential: short-lived, good only at the guard, only for this
run’s routes and budget, dead when the run ends. The API key still never enters the VM.
OpenShell, NVIDIA’s agent sandbox runtime, keeps its sandbox at no
network rules at all, grant or not. The openshell provider reaches in
instead of letting the guest reach out: a small relay program in the sandbox listens
on a loopback port and on the Unix socket the injected host client talks
to, the same client as under docker. For each connection the guest opens, plimsoll
dials into that port through ForwardTcp, a call on the OpenShell gateway
(the server that creates the sandboxes). The broker, the minted credential and the
route check stay in plimsoll’s process; the network policy read back before the run
is the no-grant policy, unchanged.
Controlling the outbound calls of code you do not fully trust is a solved problem. plimsoll uses two standard designs, and refuses a third, the one cloud providers use to hand credentials to their own VMs.
The network blocks everything by default, and every outbound call must pass one proxy that holds the rules. Egress gateways in Envoy, Istio and Linkerd are this design. plimsoll's guard is exactly this.
A broker hands out a narrow, expiring credential and attaches it at the proxy. SPIFFE and SPIRE (a standard and a server for short-lived workload identities) and OAuth client credentials work this way. plimsoll's JWT profile is this, with the calling principal, the authenticated caller, as the token's subject.
An address that only the VM itself can reach (a link-local endpoint) hands the credential into the VM, the way AWS's instance metadata service (IMDS) and the GCP metadata server do, which is why IMDSv2 had to add defences against code inside the VM asking for it. For hostile code the key must never enter the room, so this design is out.