-
Notifications
You must be signed in to change notification settings - Fork 509
Expand file tree
/
Copy pathsetup_sandbox.sh
More file actions
executable file
·197 lines (183 loc) · 9.49 KB
/
Copy pathsetup_sandbox.sh
File metadata and controls
executable file
·197 lines (183 loc) · 9.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
#!/usr/bin/env bash
# Copyright 2026 Anthropic PBC
# SPDX-License-Identifier: Apache-2.0
# One-time setup for the agent sandbox: gVisor runtime, egress-only network,
# per-target agent images, and verification. After this, `bin/vp-sandboxed`
# is the supported entrypoint.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_ROOT"
step() { printf '\n\033[1;34m== %s ==\033[0m\n' "$*"; }
ok() { printf '\033[1;32m ok\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m warn\033[0m %s\n' "$*"; }
die() { printf '\033[1;31m fail\033[0m %s\n' "$*" >&2; exit 1; }
DAEMON_JSON=/etc/docker/daemon.json
RUNSC_BIN=/usr/local/bin/runsc
RUNSC_RELEASE=${RUNSC_RELEASE:-20260420}
NET=vp-internal
PROXY_NAME=vp-egress-proxy
PROXY_TAG=vuln-pipeline-egress-proxy:latest
# ── 1. gVisor (runsc) ───────────────────────────────────────────────────────
step "gVisor (runsc)"
if [ -x "$RUNSC_BIN" ]; then
ok "$("$RUNSC_BIN" --version | head -1)"
else
[ "$(uname -s)" = "Linux" ] || die "gVisor (runsc) requires a Linux host. On macOS/Windows, run the pipeline inside a Linux VM, or use 'vuln-pipeline ... --dangerously-no-sandbox' (no syscall isolation)."
case "$(uname -m)" in x86_64|aarch64) ARCH=$(uname -m) ;;
*) die "gVisor ships for Linux x86_64/aarch64 only ($(uname -m) unsupported). Use a supported host, or 'vuln-pipeline ... --dangerously-no-sandbox'." ;;
esac
base="https://storage.googleapis.com/gvisor/releases/release/${RUNSC_RELEASE}/${ARCH}"
tmp=$(mktemp -d)
curl -fsSL "${base}/runsc" -o "$tmp/runsc"
curl -fsSL "${base}/runsc.sha512" -o "$tmp/runsc.sha512"
( cd "$tmp" && sha512sum -c runsc.sha512 )
sudo install -m 0755 "$tmp/runsc" "$RUNSC_BIN"
rm -rf "$tmp"
ok "installed $("$RUNSC_BIN" --version | head -1)"
fi
# ── 2. Register runsc runtime ───────────────────────────────────────────────
step "Docker runtime (runsc)"
# --overlay2=none: agent-written PoC files must be visible to the orchestrator's
# `docker exec cat`; the default in-sandbox tmpfs overlay would hide them.
# --ignore-cgroups: added by the verification probe below when runsc can't
# manage cgroups with dockerd's credentials (rootless or nested docker).
# Re-runs keep the flag once detected, so the daemon config doesn't churn.
RUNSC_ARGS=(--overlay2=none)
if [ -f "$DAEMON_JSON" ] && grep -q 'ignore-cgroups' "$DAEMON_JSON"; then
RUNSC_ARGS=(--ignore-cgroups "${RUNSC_ARGS[@]}")
fi
register_runsc() {
rc=0
sudo python3 - "$DAEMON_JSON" "$RUNSC_BIN" "${RUNSC_ARGS[@]}" <<'PY' || rc=$?
import json, pathlib, shutil, sys, time
path, runsc = pathlib.Path(sys.argv[1]), sys.argv[2]
want = {"path": runsc, "runtimeArgs": sys.argv[3:]}
cfg = json.loads(path.read_text()) if path.exists() else {}
if cfg.get("runtimes", {}).get("runsc") == want:
sys.exit(0)
if path.exists():
shutil.copy(path, f"{path}.bak.{int(time.time())}")
path.parent.mkdir(parents=True, exist_ok=True)
cfg.setdefault("runtimes", {})["runsc"] = want
path.write_text(json.dumps(cfg, indent=4) + "\n")
sys.exit(10)
PY
case "$rc" in
0) ok "runsc already registered (${RUNSC_ARGS[*]})" ;;
10) sudo kill -HUP "$(pgrep -xo dockerd)" || die "dockerd not running"
for _ in $(seq 10); do
docker info 2>/dev/null | grep -q 'runsc' && break
sleep 1
done
docker info 2>/dev/null | grep -q 'runsc' || die "runtime reload failed"
ok "runsc registered + reloaded (${RUNSC_ARGS[*]})" ;;
*) die "daemon.json update failed (exit $rc)" ;;
esac
}
register_runsc
# ── 3. Egress-only network + proxy ──────────────────────────────────────────
step "Egress-only network (${NET}) + proxy"
docker network inspect "$NET" >/dev/null 2>&1 || \
docker network create --internal "$NET" >/dev/null
docker build -q -t "$PROXY_TAG" -f scripts/Dockerfile.proxy scripts >/dev/null
docker rm -f "$PROXY_NAME" >/dev/null 2>&1 || true
# VP_EGRESS_ALLOW is read by egress_proxy.py at runtime from the *container's*
# env, so it must cross the docker run boundary explicitly. Default matches
# egress_proxy.py's own fallback.
ALLOW=${VP_EGRESS_ALLOW:-api.anthropic.com:443}
docker run -d --name "$PROXY_NAME" --restart=unless-stopped \
-e VP_EGRESS_ALLOW="$ALLOW" \
--network bridge "$PROXY_TAG" >/dev/null
docker network connect "$NET" "$PROXY_NAME"
proxy_ip=$(docker inspect "$PROXY_NAME" --format \
'{{(index .NetworkSettings.Networks "'$NET'").IPAddress}}')
ok "proxy ${PROXY_NAME} up on ${NET} (${proxy_ip}:3128, allow: ${ALLOW})"
# ── 4. Target + agent images ────────────────────────────────────────────────
step "Target + agent images"
[ -x .venv/bin/vuln-pipeline ] || { python3 -m venv .venv; .venv/bin/pip install -q -e .; }
for d in targets/*/; do
[ -f "$d/config.yaml" ] || continue
tag=$(.venv/bin/python3 -c 'import sys,yaml;print(yaml.safe_load(open(sys.argv[1]))["image_tag"])' "$d/config.yaml")
docker build -q -t "$tag" "$d" >/dev/null
.venv/bin/python3 -c 'import sys; from harness import agent_image; print(" ", agent_image.ensure(sys.argv[1]))' "$tag"
done
ok "target + agent images built"
# ── 5. Verification ─────────────────────────────────────────────────────────
step "Verification"
# Derive the same agent-image tag agent_image.ensure() produced in step 4
# (e.g. vuln-pipeline-canary-latest-agent:latest). Hardcoding drifts.
ATAG=$(.venv/bin/python3 -c 'import sys, yaml; from harness.agent_image import agent_tag; t=agent_tag(yaml.safe_load(open(sys.argv[1]))["image_tag"]); print(t.rsplit(":", 1)[0] + ":latest")' targets/canary/config.yaml)
host_kver=$(uname -r)
# The first container doubles as a cgroup probe. runsc writes cgroup files
# with dockerd's credentials, which no host-side static check can simulate
# (e.g. rootless docker's "root" is an unprivileged host uid; nested docker
# hits cgroup delegation limits). If cgroup setup is the only failure, fall
# back to --ignore-cgroups: container --memory caps are not enforced under
# runsc, but syscall, network, and filesystem isolation are unaffected.
#
# The probe's stderr goes to a separate file: docker can emit warnings even
# on a successful run, and any stderr text mixed into guest_kver would let
# the guest-vs-host kernel check below pass vacuously.
probe_err=$(mktemp)
if ! guest_kver=$(docker run --rm --runtime=runsc "$ATAG" uname -r 2>"$probe_err"); then
if [[ " ${RUNSC_ARGS[*]} " == *--ignore-cgroups* ]] || \
! grep -qi cgroup "$probe_err"; then
die "runsc container failed: $(cat "$probe_err")"
fi
warn "runsc can't manage cgroups here (rootless/nested docker); re-registering with --ignore-cgroups (no --memory cap under runsc)"
orig_args=("${RUNSC_ARGS[@]}")
RUNSC_ARGS=(--ignore-cgroups "${RUNSC_ARGS[@]}")
register_runsc
recovered=
for _ in $(seq 10); do # SIGHUP config reload is async; retry briefly
if guest_kver=$(docker run --rm --runtime=runsc "$ATAG" uname -r 2>"$probe_err"); then
recovered=1; break
fi
sleep 1
done
if [ -z "$recovered" ]; then
# The flag didn't fix it. Roll it back out of daemon.json: the
# persistence check above would otherwise hand it to every future
# run, which then skips the probe and never enforces --memory caps.
err=$(cat "$probe_err")
warn "--ignore-cgroups did not help; restoring previous runsc registration"
RUNSC_ARGS=("${orig_args[@]}")
register_runsc
die "runsc still failing with --ignore-cgroups: $err"
fi
fi
rm -f "$probe_err"
[ "$guest_kver" != "$host_kver" ] || die "guest kernel == host kernel; gVisor not active"
ok "gVisor active (guest $guest_kver, host $host_kver)"
docker run --rm --runtime=runsc "$ATAG" claude --version >/dev/null \
|| die "claude CLI not runnable in agent image"
ok "claude CLI runs under gVisor"
# Probe the first allowlisted host:port (not a hardcoded default) so the check
# stays meaningful when VP_EGRESS_ALLOW is customized.
PROBE=${ALLOW%%,*}
docker run --rm -i --runtime=runsc --network="$NET" \
-e HTTPS_PROXY="http://${proxy_ip}:3128" "$ATAG" python3 - "$PROBE" <<'PY' || die "egress check failed"
import urllib.request, socket, sys
allowed = sys.argv[1] # host:port — keep the port so the proxy CONNECT matches
try:
urllib.request.urlopen(f"https://{allowed}/", timeout=10).read(1)
except urllib.error.HTTPError:
pass
try:
urllib.request.urlopen("https://example.com/", timeout=5); sys.exit("example.com reachable")
except Exception:
pass
try:
socket.create_connection(("8.8.8.8", 53), timeout=3); sys.exit("direct egress reachable")
except OSError:
pass
PY
ok "egress: ${PROBE} reachable; example.com + direct egress blocked"
sentinel=/tmp/host-sentinel-$$
echo host > "$sentinel"
out=$(docker run --rm --runtime=runsc "$ATAG" cat "$sentinel" 2>&1 || true)
rm -f "$sentinel"
echo "$out" | grep -qi 'no such file' || die "agent container can read host /tmp"
ok "host filesystem unreachable from agent container"
step "Done"
echo " next: bin/vp-sandboxed run canary --model <model-id> --runs 3 --parallel --stream"