Skip to content

How to lock down network access?

Let a sandbox reach api.openai.com and nothing else:

Terminal window
shard policy create --allow api.openai.com --deny any locked
shard create --name agent --memory 512MiB --policy locked python:3.12
shard exec agent curl -sS -o /dev/null -w '%{http_code}\n' https://api.openai.com/v1/models
shard exec agent curl -sS https://example.com
shard policy logs agent

The TypeScript and Python examples reach shard serve at the address in SHARD_REMOTE, with the API key in SHARD_API_KEY.

The call to api.openai.com goes through. The call to example.com fails before it starts, because the sandbox cannot even resolve the name. The log shows both decisions.

The host enforces the policy from outside the sandbox, so the sandbox has no rules of its own that it could change.

A sandbox with no policy can reach the internet, but nothing private. The host drops its traffic to:

  • private networks: 10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16
  • link-local addresses and cloud metadata: 169.254.0.0/16
  • loopback, 127.0.0.0/8, and carrier NAT, 100.64.0.0/10
  • every other sandbox

These drops also apply under every policy, and no rule can open them.

A policy is a name and an ordered list of rules. The first rule that matches decides, and traffic that no rule allows is denied.

A rule is a destination, then an optional protocol and ports: <destination> [tcp|udp[:<ports>]].

Destination Example What it matches
A name api.example.com that host, on tcp 80 and 443
Every name under a domain *.example.com each host under example.com, but not example.com
A domain and every name under it suffix:example.com example.com, and each host under it
An address or a range 203.0.113.7, 203.0.113.0/24 the address, or the range, on every port
Everything any every destination
DNS dns lookups through shard’s resolver

Ports are numbers and ranges, separated by commas, such as tcp:22,8000-8100. A name rule covers only HTTP and HTTPS, so for any other port, use an address: --allow "203.0.113.7 tcp:5432".

A sandbox with a policy can look up names only through shard’s resolver on the host. The resolver answers only the names the policy allows, and every other name fails to resolve. That is why curl https://example.com failed above.

A name rule opens the lookups it needs. A policy with only address rules opens none, so add --allow dns if the sandbox must resolve names:

Terminal window
shard policy create --allow "203.0.113.7 tcp:5432" --allow dns db-only

shard policy logs agent prints each decision as one JSON record. Add --follow to see new ones as they happen. In the SDKs, egressLog() and egress_log() return the same records, and followEgressLog() and follow_egress_log() stream them.

A record holds time, source, verdict, host, port, address, rule, rule_text and reason. It never contains a header, a body or a secret. source says what made the decision:

  • proxy: an HTTP or HTTPS request.
  • dns: a lookup.
  • host: a packet that the host dropped on any other port.

rule is the position of the rule that decided, in the order that shard inspect agent lists the rules under egress. Some decisions have a name instead: private for the drops that every sandbox gets, default when no rule matched, and none when the sandbox has no policy.

A denied HTTP or HTTPS request gets a 403 with a one-line JSON body that names the host, the port, the rule and the reason.

Run shard policy create again with the same name, or call policies.set in the SDKs. The new rules replace every old one. Every sandbox that has the policy enforces the new rules at once, whether it is running or paused. A connection that is already open stays open until it ends.

To attach a policy to an existing sandbox, stop the sandbox first:

Terminal window
shard stop agent
shard policy attach agent locked
shard start agent

shard policy detach takes the policy away from a stopped sandbox, which then goes back to the default. shard policy remove deletes a policy that no sandbox has.

A secret grant does not open a host, so the policy must also allow the host that a secret is granted to. Under deny any with no allow rule for that host, the request is denied and the key is never sent. See secrets.

A policy decides what a sandbox reaches. To reach a program inside a sandbox from the host, forward a host port to it:

Terminal window
shard create --name web --memory 512MiB python:3.12
shard run web --name server -- python -m http.server 8000
shard port add web 9000:8000
curl -sS http://127.0.0.1:9000/
shard port list
shard port remove web 9000

shard port add web 8000 forwards host port 8000 to port 8000 in the sandbox, and 9000:8000 forwards host port 9000 to it. The host listens on 127.0.0.1 only, so nothing beyond the host reaches the port. Each connection goes to the sandbox’s own 127.0.0.1, so the program must listen there or on every interface.

--public makes the host listen on 0.0.0.0 instead, and the command prints each address of the host that the port is now reachable on:

Terminal window
shard port add web 9000:8000 --public
lo 127.0.0.1:9000
eth0 192.0.2.20:9000

--public is the only way to listen beyond 127.0.0.1, and an address in the port is refused.

To change a forward, add the same host port again, with another guest port or with --public on or off.

A forward belongs to the sandbox. It survives a stop, a start, a daemon restart and a reboot of the host. The host listens only while the sandbox runs, and a pause or a stop closes the connections that are open. shard create --port 9000:8000 forwards a port from the first start. A fork does not copy the forwards.

A host port belongs to one sandbox at a time, and a second sandbox that asks for it gets 409 with the code in_use. If another program on the host holds the port while the sandbox starts, shard port list shows why the port does not listen, and the daemon tries it again every second.

Through shard serve, listing the forwards needs the scope port:read, and adding or removing one needs port:write.

  • IPv4 only. A sandbox gets an IPv4 address. The host drops every IPv6 packet and logs it with the rule ipv6. A forwarded port carries TCP over IPv4 only.
  • A name resolves on the host when the policy is applied. If a host’s addresses change later, a rule that names it can go stale. Store the policy again to resolve the names again.
  • Another firewall on the host is not in the log. shard lets the bridge through ufw and firewalld itself, as the egress reference says, but a drop by a rule of your own leaves no record, so the sandbox just sees a timeout.

The egress reference lists every rule.