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.
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: not every rule in the namespace is release-scoped (the SSH ingress rule below is not), so don't assume any in-cluster workload is fenced off by default. - SSH ingress on port 2222. Every sandbox's
defaultservice accepts ingress from all entities on TCP 2222, which is how the jumphost reaches a human-eval session. It is reachable from hosts outside the pod network, though not from other sandboxes or runners, whose own egress rules stop them. Nothing listens on 2222 unless the task runs an SSH server.
gVisor is not on by default — opt in per eval set
The sandbox chart's own default is runtimeClassName: gvisor, but Hawk
overrides it before the chart sees it: every service that doesn't name a
runtime gets CLUSTER_DEFAULT, which the chart renders by omitting the field
entirely — so the pod runs 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.
See also the Security reference for the deployment-level view.