Skip to main content
Two controls, both set from the SDK and enforced outside the machine, so nothing running inside it (root included) can loosen them:
  • An egress allowlist per machine: the hosts and addresses it may reach. Everything else is blocked.
  • Secrets bound to a host: the machine only ever holds a placeholder, and the real value is inserted into requests on their way out, only to the hosts you name.
Use them together for an agent that needs one API and nothing else, or separately.

The allowlist

Each call replaces the whole list. An empty list means no allowlist: the machine is unrestricted, which is how every machine starts.

What an entry can be

Entries are bare hosts and addresses: no scheme, port, or path. Wildcards are allowed when every host under the domain belongs to one party. *.npmjs.org only reaches hosts npm runs. They are refused on domains where anyone can get a subdomain, because that opens a way out to a host an attacker controls:
  • *.amazonaws.com would admit every S3 bucket
  • *.vercel.app every Vercel deployment
  • likewise *.cloudfront.net, *.herokuapp.com, *.workers.dev, *.github.io, *.pages.dev and other shared-hosting or CDN domains
Name the exact host instead, such as my-bucket.s3.amazonaws.com. Top-level wildcards like *.com are refused as well. Addresses must be public. Private, link-local, loopback and multicast ranges are refused, as is anything overlapping them, 0.0.0.0/0 included. IPv6 is refused too: machines have no IPv6 egress.

How it is enforced

Web requests (HTTP and HTTPS) are admitted by the hostname they name, so api.stripe.com works whichever address it resolves to. Everything else is admitted by destination address, and the addresses of an allowlisted name count as allowlisted too, so a plain curl to an allowlisted host works without you listing its addresses. What a blocked request sees depends on the protocol: HTTP gets a 403, HTTPS has its connection closed during the handshake, and anything else is dropped. The hosts of any bound secret the machine holds are always admitted, allowlist or not. A change takes effect within a second, on a running machine, without a restart. The machine can read its own policy but not change it:

Secrets bound to a host

A secret is normally injected into the machine as its real value. Bound to a host, it is not:
Inside the machine STRIPE_KEY is an opaque string starting with bxds_. A request to api.stripe.com that carries it, in a header, the query string, the body, or Basic auth, arrives at Stripe with the real key. A request anywhere else carries the useless placeholder. There is nothing to configure in the machine; your code uses the variable as it always did.
  • The placeholder is stable. Rotating the value with another set keeps the same placeholder, so a running process never sees its environment change.
  • domains is the full desired set. Setting the secret again without it makes it a plain secret again, injected as its real value.
  • Wildcards follow the same rules as allowlist entries: *.stripe.com is fine, *.amazonaws.com is refused.
For the substitution to happen, HTTPS to a bound host is handled by boxd with a certificate the machine already trusts. Standard tools and libraries just work. A tool that pins certificates or ships its own CA bundle will refuse the connection to a bound host; give it the system trust store or leave that secret unbound.

Forks, snapshots, and isolation

  • A fork inherits its source’s allowlist. A confined machine’s fork comes up confined.
  • A machine created from a snapshot starts unrestricted. A snapshot carries no allowlist, so set one after the restore if you need it.
  • Bound secrets are set once, at the organization level, and apply to every machine in scope, forks and restores included.
  • --isolated and the allowlist are independent. Isolation cuts a machine off from the rest of your account; the allowlist cuts it off from the internet. A sandbox for untrusted code usually wants both. See Sandboxes.
  • On an isolated machine, bound secrets arrive in exec and SSH sessions only, like every account-level variable: nothing is injected at boot.

Where to set it

Both controls are available in the TypeScript and Python SDKs and the gRPC API (SetVmEgressAllow, and the domains field on SetVar). They are deliberately not settable from inside a machine, and the laptop CLI does not expose them yet.