Skip to content

Security model

shard assumes the code in a sandbox is hostile, and keeps the host, your credentials and your private networks out of its reach.

Give a sandbox an API key that it cannot read, and let it reach only one host:

Terminal window
printf '%s' "$OPENAI_API_KEY" | shard secret set --destination api.openai.com OPENAI_API_KEY
shard policy create --allow api.openai.com --deny any openai-only
shard create --name agent --memory 512MiB --secret OPENAI_API_KEY --policy openai-only alpine:3.20
shard exec agent sh -c 'echo "$OPENAI_API_KEY"'
shard remove --force agent

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

The sandbox prints a placeholder, not the key:

mock-OPENAI_API_KEY

When the sandbox sends that placeholder in a header of an HTTPS request to api.openai.com, the host replaces it with the real key. A request to any other host carries the placeholder. The policy allows api.openai.com and denies everything else.

shard treats the code in a sandbox as untrusted. That code might be an agent, a tool that a prompt injection took over, or a package nobody has read. It can read its own environment, dump its memory, and send every byte it finds to anywhere it can reach.

Four boundaries separate that code from what you want to protect:

Boundary What it keeps out Where it is enforced
The provider the host kernel and other sandboxes gVisor, a virtual machine, or a user namespace
Secrets the value of a credential the proxy on the host
Network egress your private networks, and hosts the policy does not allow the host, never the sandbox
The API anyone without access to the socket or a valid token the daemon socket and shard serve

The provider decides how strongly a sandbox is isolated from the host and from other sandboxes:

Provider The boundary Tenants on one host
gvisor gVisor, a kernel in user space between the sandbox and the host kernel many
firecracker a microVM with its own kernel many
vz a virtual machine with its own kernel, on Apple’s Virtualization framework many
sysbox a Linux user namespace one
runc namespaces and cgroups only, on the host kernel trusted code only

For each provider in detail, see the provider matrix.

A sandbox holds a placeholder for each secret it is granted. The default placeholder is mock-<NAME>, and --placeholder sets a different one. The value stays on the host in <root>/secrets/<NAME>, a file with mode 0600 inside a directory with mode 0700. shard writes the value nowhere else.

A secret is granted to destinations, never to a sandbox alone. --destination names each host that the value may be sent to. The host puts the value only into the headers of HTTPS requests to those hosts, and never into the URL or the body. A request to any other host, or over plain HTTP, carries the placeholder.

A grant does not open network access to any host. Only the policy decides what the sandbox can reach, so a sandbox with a policy needs an allow rule for each granted host.

The proxy does not stop misuse of the key. A sandbox that may call api.openai.com with the key can make any call that the key allows. Limit the key at the API provider, because shard only keeps the key out of the sandbox.

Pass the value to shard secret set on stdin or at its hidden prompt. Never pass it as a command argument, because command arguments show up in the process list.

On runc, the proxy is the only thing between the sandbox and the value, and code that escapes the sandbox can read the secret store just as the host can.

For grants, rotation and the placeholder rules, see secrets.

A sandbox has no network rules of its own. The rules live on the host: netfilter on Linux, and the daemon’s own network stack on a Mac. Code in the sandbox cannot change them, and the host writes them again on every create, start, resume, fork and policy change.

A sandbox without a policy can reach the internet but nothing private. The host drops its traffic to:

  • 10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16
  • 169.254.0.0/16, which holds the cloud metadata address
  • 127.0.0.0/8 and 100.64.0.0/10
  • every other sandbox

These drops are the egress floor. The floor applies under every policy, and no rule can open it. A policy is an ordered list of rules. The first rule that matches decides, and the host drops traffic that no rule allows.

A sandbox with a policy or a secret sends its traffic on TCP ports 80 and 443 through a proxy on the host. The proxy judges each request by host name, and it also refuses a host that resolves to one of the host’s own addresses.

For rules, wildcards and the egress log, see egress.

shard port add forwards a host port to one port of one sandbox. By default the host listens on 127.0.0.1, so only programs on the host can connect. With --public the host listens on 0.0.0.0.

A forward changes nothing about the sandbox’s own traffic. The egress floor and the policy still decide what the sandbox can reach.

The daemon listens only on a unix socket at <root>/shard.sock, which defaults to /var/lib/shard/shard.sock. It never serves its API on TCP. The only TCP ports it opens are the egress proxy and resolver on the sandbox gateway, and the host ports that an operator forwards with shard port add. A forwarded port carries bytes to one port of one sandbox, and never reaches the API.

The socket has mode 0600, so only the user the daemon runs as can use it. On Linux that user is root. If the host has a group named shard, the daemon hands the socket to that group with mode 0660 when it starts, and members of the group can then use the socket without sudo.

A client on another host reaches the daemon through shard serve, a separate process that checks a token on every request and then passes the bytes to the socket. By default it listens on 127.0.0.1:7850 and speaks plain HTTP.

Mint a token on the server:

Terminal window
shard tokens mint --name ci --duration 24h --scopes sandbox:read,sandbox:write,exec

The client sends the token as SHARD_API_KEY. shard serve answers 401 with the code unauthorized, before the request reaches the daemon, when a token:

  • is signed with another key or another algorithm than HS256
  • has expired
  • has been revoked, or is not in the token ledger on the server

A token minted without --duration never expires. shard tokens mint works only on the server and never over the API. The first run of shard tokens mint or shard serve creates the signing key, <root>/auth/signing-key, with mode 0600. shard never rotates that key.

A token’s scopes limit which routes it can reach:

Scope What it allows
sandbox:read list, inspect, the egress log, reading a process and its logs, and reading snapshots
sandbox:write create, start, stop, pause, resume, fork, and making a snapshot
sandbox:delete removing a sandbox or a snapshot
exec exec, run and kill a process, and every file route
secret:* every secret route, and grants on a sandbox
policy:* every policy route, and the policy of a sandbox
port:read listing the forwarded ports
port:write adding, changing and removing a forwarded port, a public one too

A token with no scopes, or with *, has every scope. A request to a route outside the token’s scopes gets 403 with the code forbidden. A create that names a secret also needs secret:*, a create that names a policy also needs policy:*, and a create that forwards a port also needs port:write. Through shard serve, server-only commands such as the image commands get 403 for every token.

Revoke a token by its id, or every token of one name:

Terminal window
shard tokens revoke --name ci

The next request with that token gets 401. shard serve checks the ledger once a second for each open connection and closes any connection that uses a revoked token. Closing the connection detaches the client, but it does not stop an exec or a sandbox.

To run shard serve as an unprivileged user and to set up the proxy, see How to connect to a remote host?.

  • shard never logs a secret value, and never writes one into a state file. A sandbox record names its secrets but never holds their values.
  • The proxy logs one line for each request, but never logs a header value.
  • shard serve and the CLI never log a token or the signing key. shard serve logs the name of the token for each request it lets through.
  • shard secret list prints names, destinations and placeholders, but never a value.