v2.0.0next
Sandboxed workers

Network policy

How the hosts a sandboxed task may reach are listed, how Kraft enforces them, and what the policy does not cover.

A network policy limits which hosts a sandboxed task reaches. This page covers how to write the host lists, how Kraft enforces them, and where the policy stops.

config/repos.yaml
repos:
  - path: /home/you/code/my-service
    sandbox:
      kind: docker
      image: ghcr.io/acme/agent@sha256:...
      network:
        install: {allow: [registry.npmjs.org, pypi.org, files.pythonhosted.org]}
        runtime: {allow: [api.github.com], deny: []}

With network set, a sandboxed task reaches only what its lists allow. Without it, a sandboxed task has the runtime's default network and can reach anything, the cloud metadata address included, so Kraft refuses the launch (see Without a network policy).

Phases and host lists

FieldTypeApplies to
installmapping with allow and denyThe repository's setup_command.
runtimemapping with allow and denyEvery task session: agents, commands, test scopes, reviews and escalations.
<phase>.allow, <phase>.denylist of hosts, [] by defaultThe hosts the phase may, and may not, reach.

A host in allow or deny is written as:

  • api.github.com, which names that host exactly, or api.github.com:443, which names it on one port.
  • *.example.com, which covers exactly one label in front of example.com, not example.com itself.
  • * or **, which cover everything.

CIDRs are not accepted.

Deny wins over allow. A phase that is missing or has empty lists allows nothing, so network: {runtime: {}} denies everything. Only network: {} with no phase at all is the same as no network. The lists are part of the sandbox, so a chain, node or task cannot change them.

Without a network policy

A launch into a sandbox with no network is refused, whatever the harness. The task stops as a configuration error before anything runs, and the session's log names the repository and both fixes: add a network policy, or set unrestricted_network: true on the sandbox.

config/repos.yaml
repos:
  - path: /home/you/code/my-service
    sandbox:
      kind: docker
      image: ghcr.io/acme/agent@sha256:...
      unrestricted_network: true

unrestricted_network: true says you accept that the worker's network is not restricted and that gates are not enforced: Kraft sets up no channel, and nothing keeps the container from dialing the host.

The launch then runs, and Kraft logs a warning at each one. kraft admin doctor reports the sandbox's egress row as a warning instead of a failure. The key is refused together with network, and by a kit sandbox. See Callbacks from a sandbox for what a task without a channel does differently.

Harness hosts

An agent session's runtime allow list also gets the hosts its harness file declares under network.requires, and a deny still wins over them. Only api.anthropic.com (Claude) and api.openai.com (Codex) have been checked. The rest are unverified: if a CLI needs a host its harness does not list, allow it in runtime. OpenCode declares none, because its hosts depend on its provider.

Always denied

Whatever the lists say, these are denied:

  • loopback;
  • link-local (169.254.0.0/16, fe80::/10);
  • the unspecified (0.0.0.0, ::) and multicast addresses;
  • cloud metadata names and addresses;
  • the host's own addresses, which are what its hostname resolves to.

How network policy works

Each session gets a relay container with no network of its own, from relay_image in sandbox.yaml. The worker joins the relay's network namespace, so loopback is its only interface. The relay forwards 127.0.0.1:3128 there to a unix socket that belongs to that session alone, and the Kraft server serves the proxy on it.

The worker gets HTTP_PROXY and HTTPS_PROXY, in both cases, set to http://127.0.0.1:3128, an empty NO_PROXY, and NODE_USE_ENV_PROXY=1.

The server makes each allowed connection itself, through its own HTTPS_PROXY, HTTP_PROXY and NO_PROXY when it has them, so a proxy on the host's loopback works here. After a server restart, a running session's channel is reopened with the lists it launched under, not the current configuration.

Docker Desktop and Podman machine

These run containers in a VM, where a container cannot connect to a host unix socket. Kraft checks once per runtime whether a container can connect to a host socket. When it cannot, and the runtime reports that it runs in a VM (Docker Desktop's OperatingSystem, Podman's ServiceIsRemote), Kraft carries the channel in two hops instead:

  • The relay the worker joins, still with no network, forwards to a socket in a volume made for that session.
  • A second relay, on the runtime's default network, forwards that socket to one mutual-TLS listener the server keeps on 127.0.0.1, which it reaches as host.docker.internal (Docker) or host.containers.internal (Podman).

Both relays run relay_image. The listener trusts only a client certificate from Kraft's own CA, kept under run/ca/, and takes the session from that certificate, one per session, which only the second relay mounts. The worker shares no network, volume or mount with it.

kraft admin doctor names the transport in the repository's sandbox row and checks that the listener answers.

How addresses are checked

Kraft checks the address a name resolves to, resolving it once on the host, and then connects to that checked address. Through an upstream proxy (the server's own HTTP_PROXY or HTTPS_PROXY), Kraft still checks the address, but the upstream resolves the name again and connects to whatever it gets. If any address a name resolves to is denied, the whole name is refused.

An IPv4 address carried inside an IPv6 one (IPv4-mapped, IPv4-compatible, 6to4, NAT64 64:ff9b::/96) is checked as that IPv4 address as well. Any other non-public address (RFC 1918, IPv6 ULA, the shared 100.64.0.0/10 range, reserved ranges) is reachable only through an allow entry naming its host exactly, never through a wildcard.

Refusals

A refused request is answered 403 with a one-line reason. Kraft records one sandbox_egress_refused event ({session_id, host, port, phase, reason}) per session and host, so a retry loop does not flood the timeline: kraft view events --type sandbox_egress_refused. After 100 hosts, one last event with suppressed: true says later refusals in that session are not recorded; they are still refused.

Clients that ignore the proxy

Anything that does not use HTTP(S)_PROXY, such as git over SSH or a raw socket, has no route at all. A harness whose file declares proxy_aware: false (Cursor) is refused under a network policy as a configuration error before it starts. It runs in a sandbox only with unrestricted_network: true instead.

It fails closed

A session that cannot get its route stops as a configuration error and runs nothing. This happens when:

  • the socket check does not run,
  • a container cannot connect to the socket on a runtime that does not report a VM (an SELinux denial, or the permissions on run/sn/),
  • the relay image is not pulled, or
  • a relay does not start.

kraft admin doctor fails the repository's sandbox row for the first three, naming docker pull <relay_image> for a missing image, so pull it before filing work.

Network policy limits

  • Docker Desktop and Podman machine: seven days per session. A session's client certificate expires after seven days; a session that runs longer loses its route and fails closed.
    • The listener keeps its port across a server restart.
    • If another program has taken that port by then, the listener moves to a new one, and sessions started before the restart have no route until they are retried.
  • Domain fronting. The allow list is enforced on the name the client asks for and on the address Kraft dials. It does not see what travels inside the TLS connection. An allowed name on a shared CDN address can reach other names that address serves, by TLS SNI or the HTTP Host header. Kraft terminates TLS only for a managed credential's host.
Copyright © 2026