Sandbox Network Isolation¶
Hawk runs each sample's agent tool code in a Kubernetes sandbox whose network is deny-by-default: with no configuration, a sandbox pod can reach cluster DNS and its own sibling pods, and nothing else. Internet access is something you opt into per task, not something you opt out of.
This matters for any evaluation that runs untrusted or model-generated code, and it is the required posture for cyber-related evaluations, where the agent must not reach the public internet or the infrastructure around it unless the engagement explicitly scopes it in.
The trust boundary¶
Hawk keeps the model credentials out of the environment the agent can touch:
- The runner (the Inspect eval loop) is the trusted process. It makes all model API calls, talking to the middleman proxy with the caller's short-lived OAuth JWT — never a provider API key. Its egress is open (it needs the gateway plus package installs).
- The sandbox (where the agent executes bash/tools) is the untrusted
environment. It has no model API key and no gateway URL, and its egress is
deny-by-default. Setting
api_keyin an eval config is rejected outright.
So a provider key is never injected into a container the agent controls — the agent reaches the model only indirectly, through the trusted runner. This holds for every sandbox provider.
Sandbox providers¶
The rest of this page describes the Kubernetes sandbox provider — Hawk's
default and what production deployments use. It translates a task's Compose or
values.yaml definition into Cilium network policies, and it is what enforces
the per-eval configuration and always-on guarantees below.
Other providers isolate differently, and the specifics here do not carry over:
- Docker (
hawk local, local Compose runs) applies Docker's own network semantics directly — e.g.network_mode: noneremoves the container's network interface entirely. The Cilium VPC-deny and cross-sample guarantees below do not apply. - EC2, Modal, and other providers enforce whatever that provider and your
deployment configure. The
network_mode→ Cilium mapping and the always-on guarantees here are not automatic; verify isolation against the provider before relying on it.
The provider is selected per evaluation, by the task's Inspect sandbox type.
Specifying network access per eval (Kubernetes provider)¶
Network policy lives in the task's sandbox definition (the Docker Compose
file or Kubernetes values.yaml the task references), not in the eval-set YAML.
Hawk reads that definition and translates it into Cilium network policy:
| Compose declaration | Internet | Other services in the same sample |
|---|---|---|
network_mode: none |
no | no (deny-all) |
a network marked internal: true |
no | only services sharing that network |
| nothing specified | no (unless a sibling grants it) | all of them, but only if the file defines no networks |
network_mode: bridge |
yes | all of them, but only if the file defines no networks |
| one non-internal, bridge-driver network every service joins | yes | yes |
Internet is granted per sandbox, not per service
If any service sets network_mode: bridge (or the file defines a single
non-internal, bridge-driver network that every service joins), every
service in that sandbox reaches the internet — except any service set to
network_mode: none, which stays fully denied. bridge grants * (every
domain); for a scoped engagement, use the allowlist below instead.
Because the grant can't be scoped to one service, Hawk rejects a compose
that grants internet while some service sits only on an internal: true
network — that service asked to stay offline and Hawk cannot honour it here.
Use network_mode: none for a service that must stay offline regardless of
its siblings.
Reachability between services is a separate question
Internet access and sibling reachability are decided by different rules, so
read both columns independently. If the compose defines no networks, every
service can reach every other. If it defines networks, a service is reachable
only by peers sharing one of its networks — and a service that joins none,
including one whose only setting was network_mode: bridge, is unreachable
from all of them.
Hawk can only express that split when the file declares networks, so it
rejects a compose that sets network_mode: bridge on one service and
leaves another on no network at all while declaring none. Under Docker those
two sit on separate networks and cannot reach each other; declare a shared
network and put every service on it.
Full isolation: network_mode: none¶
Gives a service no network access — no internet, no cluster DNS, and no network traffic to or from sibling services. Use it for a container that must stay unreachable over the network, e.g. one holding reference solutions or hidden tests. It stays denied even while a sibling has internet:
services:
default:
image: my-task-image
scorer:
image: my-scorer-image
network_mode: none # deny-all, even though `default` may reach the internet
Combining network_mode: none with networks on the same service is rejected —
they ask for opposite things.
Isolation is at the network layer
none blocks communication initiated from inside the sandbox. The trusted
Inspect harness in the runner still reaches every container out-of-band via
the Kubernetes API (exec, file read/write) — that path drives scoring and
setup, and is how controlled cross-sandbox channels are built (e.g. a trusted
proxy relaying between an isolated container and the agent). The network
policy does not touch it.
Human evals force port 2222 open
network_mode: none denies all ingress, with one exception: on a human eval
below strict, Hawk re-opens TCP 2222 on the default service so a
baseliner can SSH in from the jumphost; submitting a human eval at strict
is rejected rather than left unreachable. Agent eval sets never get that
rule, so an isolated agent sandbox has no inbound port at all.
Siblings without internet: internal: true¶
To let services talk to each other but not the internet, put them on a network
marked internal: true:
services:
game:
image: my-registry/game:v1
networks: [task-net]
default:
image: my-registry/agent:v1
networks: [task-net]
networks:
task-net:
driver: bridge
internal: true
This only holds if no other service opens the sandbox up. Because the internet
grant is sandbox-wide, internal: true cannot keep one service offline while a
sibling sets network_mode: bridge — Hawk rejects that combination rather than
quietly putting the service online. To isolate a single service regardless of its
siblings, use network_mode: none.
Scoped internet allowlist (recommended for cyber engagements)¶
network_mode: bridge opens everything. For an engagement where only specific
targets should be reachable, use a Kubernetes values.yaml sandbox config and
name exactly what the agent may reach — everything else stays blocked:
allowDomains:
- "target.example.com"
allowCIDR:
- "10.20.0.0/24"
services:
default:
image: my-task-image
allowDomains, allowCIDR, and allowEntities are the inspect_k8s_sandbox
agent-env chart values. Only none and bridge are accepted for
network_mode; any other value fails the run (checked per service).
Always-on guarantees¶
On the Kubernetes provider these hold regardless of the per-task config above:
- VPC infrastructure is blocked. A cluster-wide Cilium policy denies every
sandbox pod egress to the primary VPC subnet and the hybrid node/pod networks,
plus EC2 IMDS (
169.254.169.254) and EKS Pod Identity (169.254.170.23). It denies these specific ranges rather than all VPC CIDRs, so cluster DNS keeps working. - Cross-sample isolation. One sample's pods cannot reach another's. The
sandbox allow rules are keyed to a per-sample Helm release name, and a
sandbox's egress permits only same-release endpoints plus
world— which does not cover in-cluster pod identities. Note this is enforced on the source side, and the shared sandbox namespace is not itself a boundary, so don't assume an in-cluster workload is fenced off by anything other than these rules. - SSH ingress on port 2222. On human evals below
strict, the sandbox'sdefaultservice accepts ingress on TCP 2222 from theworldentity, which is how the jumphost reaches the session; agent eval sets never get this rule. The jumphost connects from a VPC address outside the pod CIDR, which Cilium labelsreserved:world; in-cluster sources carry pod orremote-nodeidentities and do not match. Nothing listens on 2222 unless the task starts an SSH server.
gVisor is not universally on by default — opt in per eval set
The sandbox chart's implicit default service uses runtimeClassName: gvisor,
and Hawk preserves that value when a task does not define the service itself.
Task-authored services that do not name a runtime get CLUSTER_DEFAULT, which
the chart renders by omitting the field entirely, so those pods run under the
node's default runtime (runc) with no syscall boundary.
Kubernetes has no "default RuntimeClass" concept (unlike StorageClass) — a pod
must name it. Where the RuntimeClass is installed (hawk:enableGvisor), ask
for it from the eval-set config, without touching the task's sandbox
definition:
A task can also pin runtimeClassName on a service in its own values.yaml;
the eval-set setting wins where both are present. Don't assume gVisor is in
use without checking the rendered values. See
Hardening High-Risk Evals for the full posture.
Verify before a sensitive run¶
Network isolation is enforced structurally — Hawk does not yet run a
pre-flight sandbox-escape probe or auto-validate the config before a cyber eval.
Before a run where isolation matters, render the task's sandbox values and
confirm allowDomains / allowEntities / allowCIDR contain only what you
intend, and in particular that world / * appear only when you meant to open
the internet.
Human oversight¶
Network isolation bounds where the agent can go; it does not gate what actions it
takes. For a human to review and approve or reject tool calls live — and to
terminate a run that exceeds scope — use
human-in-the-loop approvals
(acp_server / approval on the eval-set config). The two are complementary:
isolation is the network boundary, approvals are the action boundary.
Note that the human-eval SSH carve-out described under
network_mode: none is not an enforcement
boundary either. A baseliner with shell access can bridge an isolated sandbox to
their own machine regardless of network policy. That is a tolerable trade for a
human you have chosen to give a shell, and is the reason the same port is not
opened for agent evals.
See also the Security reference for the deployment-level view.