> ## Documentation Index
> Fetch the complete documentation index at: https://docs.boxd.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Egress control

> Decide what a machine may reach on the internet, and keep your secrets out of the machine entirely.

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

<CodeGroup>
  ```typescript TypeScript theme={"theme":"github-dark"}
  await boxd.machines.setEgressAllow("alpha", ["api.stripe.com", "*.npmjs.org", "203.0.113.0/24"]);
  (await boxd.machines.get("alpha")).egressAllow;      // what is in effect
  await boxd.machines.setEgressAllow("alpha", []);      // clear: unrestricted again
  ```

  ```python Python theme={"theme":"github-dark"}
  boxd.machines.set_egress_allow("alpha", ["api.stripe.com", "*.npmjs.org", "203.0.113.0/24"])
  boxd.machines.get("alpha").egress_allow                # what is in effect
  boxd.machines.set_egress_allow("alpha", [])            # clear: unrestricted again
  ```
</CodeGroup>

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

| Entry           | Example          | Matches                                                                   |
| --------------- | ---------------- | ------------------------------------------------------------------------- |
| A hostname      | `api.stripe.com` | that host exactly                                                         |
| A wildcard      | `*.npmjs.org`    | any host under that domain (`registry.npmjs.org`), not `npmjs.org` itself |
| An IPv4 address | `203.0.113.7`    | that address                                                              |
| An IPv4 range   | `203.0.113.0/24` | every address in the range                                                |

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](#secrets-bound-to-a-host) 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:

```bash theme={"theme":"github-dark"}
boxd machine egress          # inside the machine: the allowlist and the secret-bound hosts
boxd info                    # the same, as a one-line summary
```

## Secrets bound to a host

A [secret](/guides/env-secrets) is normally injected into the machine as its real value. Bound to a host, it is not:

<CodeGroup>
  ```typescript TypeScript theme={"theme":"github-dark"}
  await boxd.secrets.set("STRIPE_KEY", "sk_live_...", { scope: "all", domains: ["api.stripe.com"] });
  ```

  ```python Python theme={"theme":"github-dark"}
  boxd.secrets.set("STRIPE_KEY", "sk_live_...", scope="all", domains=["api.stripe.com"])
  ```
</CodeGroup>

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](/use-cases/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](/reference/typescript-sdk) and [Python](/reference/python-sdk) SDKs and the [gRPC API](/reference/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.
