Skip to content

Egress ​

Egress is the allowlist bounding what of the internet a box may reach. Set one and the box can talk to the hosts you named and nothing else.

It lives in a box profile, as one of its defaults:

json
{
  "build":    { "base": "ubuntu-22.04" },
  "defaults": { "egress": { "domains": ["github.com", "api.anthropic.com"] } }
}
sh
ssh cli@box.hopbox.dev profile set agent < agent.json
ssh cli@box.hopbox.dev profile build agent
ssh build:agent@box.hopbox.dev     # box "build", fenced by the agent profile

This is what makes an unattended agent something you can leave running. Without it a box that fetches a dependency can also reach anything else on the internet, and the only thing standing between "it built the branch" and "it posted your source somewhere" is the agent's good behaviour.

It is yours, not the operator's ​

The allowlist belongs to your account, because the person who knows what a project needs to reach is the person running it. A customer on a hosted fleet can fence their own work without asking anyone.

There is deliberately no second namespace for it — no separate "egress profile" object to name and keep in step with the box it applies to. A box profile is already the named thing that carries an image and its defaults together, so egress is one of its fields. A box that names no profile, or one whose profile sets no egress, is unrestricted.

The default is unrestricted ​

A box with no fenced profile reaches the public internet exactly as it always has, minus the fleet-wide fence that already blocks private ranges, the tailnet and cloud metadata. Nothing changes for boxes that do not ask.

domains is suffix-matched on label boundaries, so github.com also allows api.github.com and codeload.github.com — and never notgithub.com. A profile whose egress lists no domains is refused when you save it rather than stored: it would block everything while looking like a working policy, which is the worst of both.

It needs --owner-network (and therefore --compute microvm), because the fence is enforced at the gateway resolver. Without that, a profile's egress is stored and simply not enforced.

Editing it reaches boxes that are already running ​

Change defaults.egress and the new allowlist applies within one DNS lookup — to running boxes, not just the next one you spawn. The box record stores the profile's name, and the resolver looks the policy up per query.

sh
ssh cli@host profile show agent > agent.json
$EDITOR agent.json                      # add the host it turned out to need
ssh cli@host profile set agent < agent.json

That is deliberate, and it is the difference between a fence people use and one they work around: when an agent two hours into a run turns out to need one more host, you add it without killing the run. It is not an escalation — you could have spawned an unfenced box in the first place.

The rest of the profile does not work that way, and the split is the point: build.* needs a rebuild, and flavor / lifetime / ttl were fixed when the box started.

Deleting a profile un-fences its running boxes

The policy is looked up live, so removing the profile removes the fence rather than blackholing a box mid-run. Boxes already booted keep running on the image they got.

How it is enforced ​

Two halves, and neither is sufficient alone:

By name, at the resolver. Every box already resolves through the gateway, which identifies the asking box by its address. A box under a policy that asks for a name outside it gets NXDOMAIN — the address never reaches the box at all.

By address, in the packet fence. When an allowed name is resolved, the addresses in that reply are admitted for that box, for as long as the DNS record says they are good. Everything else leaving a fenced box is dropped.

The second half is what stops a box skipping the resolver and dialling a literal address; the first is what lets the policy be written in names at all. Because the fence learns from the answer the box actually received, a host that moves between CDN addresses keeps working with no configuration change.

The gateway itself is always reachable, or a fenced box could not reach the resolver that grants it access — nor the metadata plane and the agent hub it needs to function.

What it does not do ​

  • IP ranges are not expressible yet. The allowlist is a list of names. Allowing a destination by CIDR needs a different kind of set in the packet fence, and is not built.
  • A box that never resolves anything is fully fenced, which is correct but can be surprising: a hard-coded address in a script fails under a policy even if the same host would be allowed by name.
  • It does not apply to a profile's own build. A build runs unrestricted — it needs distro mirrors a runtime allowlist will not name, and a fenced apt-get is a miserable thing to debug. There are no secrets in a build box, so there is nothing there to leak.
  • It is not a proxy. hopbox does not see inside the connection, and does not inspect or log what is sent — only which destinations are reachable.

Checking it ​

Inside a fenced box, an allowed host resolves and connects; a disallowed one fails to resolve at all:

$ curl -sS https://api.github.com/ -o /dev/null && echo ok
ok
$ curl -sS https://example.com/
curl: (6) Could not resolve host: example.com

The profile a box came from travels with the box record, so the fence is reprogrammed from the store on a daemon restart.

Instant isolated compute — for humans and AIs