Concepts

Network policy

Control which hosts a sandbox may reach — set an allow/deny policy at creation, or change it at runtime with sb.egress.*.

By default a sandbox has ordinary, unrestricted outbound network access. Pass a networkPolicy to client.sandbox() and an egress sidecar is attached to the container: every DNS query and (in dns+nft mode) every raw-IP connection is checked against your rules before it leaves.

const sb = await client.sandbox({
  image: "ubuntu:22.04",
  resources: { cpu: "500m", memory: "256Mi" },
  networkPolicy: {
    defaultAction: "deny",
    egress: [
      { action: "allow", target: "api.github.com" },
      { action: "allow", target: "*.npmjs.org" },
    ],
  },
});

Server requirement

The egress sidecar needs the OpenSandbox server to have egress.image configured. alineo init sets this up by default (opensandbox/egress:v1.1.7, mode = "dns+nft"). On an older config or a uvx opensandbox-server host, add an [egress] section to ~/.config/alineo/server.toml (or ~/.sandbox.toml) and restart the server. Omit networkPolicy entirely and none of this applies — no sidecar is attached.

Not available under gVisor

A server whose [secure_runtime] is gvisor (runsc) rejects any networkPolicy or credentialProxy request with networkPolicy is not compatible with runtime 'gvisor'. The egress sidecar redirects DNS with an iptables nat rule, and gVisor does not implement that table. OpenSandbox reports the same limit in its own issue #934. Run these sandboxes on a server with the plain Docker runtime — a second server on another port is enough — or use Kata. The runtime is a server setting, so one server cannot serve both.

Sidecar did not become ready

Egress sidecar did not become ready within 30s … Connection refused on a server that runs in a container means [docker].host_ip is missing from server.toml: the server probes the sidecar at its own 127.0.0.1. alineo init sets host_ip = "host.docker.internal" and adds --add-host host.docker.internal:host-gateway to the container. Re-run alineo init to add both to an existing install.

NetworkPolicy

FieldTypeDescription
defaultAction"allow" | "deny"What to do when no rule matches. Defaults to "deny" server-side if omitted.
egressNetworkRule[]Ordered allow/deny rules.

Each NetworkRule is { action: "allow" | "deny"; target: string }.

Rule targets

target is one of:

  • An FQDN — "api.github.com". Matched against the DNS query name.
  • A wildcard domain — "*.openai.com". The *. prefix is the sidecar's only wildcard form (it matches one or more leading labels).
  • A bare IPv4/IPv6 address — "10.0.0.5", "2606:4700::1111".
  • A CIDR block — "10.0.0.0/8", "fd00::/8".

IP and CIDR rules are enforced at the nftables layer, so they only take effect when the server runs egress.mode = "dns+nft", and they gate raw-IP egress only. A CIDR rule does not authorize resolving a domain that happens to point into that range — for reach-by-name you still need a domain rule.

A malformed target (a URL, whitespace, a space-containing string) throws SandboxClientError locally before any server round-trip. The same check is exported as isValidEgressTarget from @alineo-labs/opensandbox.

Changing the policy at runtime

sb.egress.* adjusts a running sandbox's policy through its sidecar. The change applies immediately — no restart, no new sandbox.

// Allow a host on a sandbox that's already running:
await sb.egress.patch([{ action: "allow", target: "example.com" }]);

// Revoke it — the host is blocked again:
await sb.egress.delete(["example.com"]);

// Read back the live policy from the sidecar:
const { policy } = await sb.egress.get();
MethodBehavior
sb.egress.patch(rules)Merge rules in. An incoming rule replaces any existing rule with the same target; every other rule and defaultAction are untouched.
sb.egress.delete(targets)Remove rules by target. Unknown targets are silently ignored.
sb.egress.get()Returns the sidecar's status envelope: { status?, mode?, enforcementMode?, policy? }.

These only work on a sandbox created with a networkPolicy — without one there is no sidecar and the calls error.

Persistence across resume and fork

Runtime sb.egress.* changes are sidecar-local: they don't survive the sidecar restarting and OpenSandbox's snapshot/checkpoint doesn't capture them. Every patch / delete is written to the ledger (EgressRuleAdded / EgressRuleRemoved), and Sandbox.resume() folds whatever is still live back into the resumed sandbox's boot policy — so a still-wanted allowance is re-applied automatically.

sb.fork() does not carry runtime egress rules. A fork is a fresh branch and starts with a wide-open defaultAction: "allow" policy.

Relationship to credential injection

networkPolicy (which hosts are reachable) and credentialProxy (transparent credential injection) are separate concerns that ride the same sidecar. credentialProxy: true requires networkPolicy to also be set. See Credentials.

For an agent that should hold a specific host behind a human decision before it's reachable, see Permission gate.