feat(sandbox): enforce a URL denylist egress mode on docker and daytona - #1113
Conversation
a5ffbb2 to
df4f98b
Compare
There was a problem hiding this comment.
Devin Review found 2 potential issues.
2 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)
| egress_denylist = ( | ||
| None | ||
| if disallow_web_tools or role.agent == "oracle" | ||
| else _task_egress_denylist(getattr(self, "_task", None)) | ||
| ) |
There was a problem hiding this comment.
🟡 Exempt runs retain agent firewall
After a denylisted agent runs, connect_as skips proxy setup for an oracle but leaves its UID firewall active. Oracle roles and same-user verifiers then lose internet access.
Prompt for agents
The denylist owner rules installed by enforce_agent_egress_firewall persist for the sandbox user's UID. In src/benchflow/rollout/__init__.py, connect_as excludes oracle roles from egress_denylist, but it does not remove those rules after a preceding denylisted role. Verification also runs after the rules were installed, and VerifierConfig permits verifier.user to equal the sandbox user. Add lifecycle management that disarms only BenchFlow's denylist firewall rules before exempt oracle or verifier execution, while preserving no-network behavior and safely rearming the denylist for later agent roles. Cover a denylisted-agent-to-oracle transition and a verifier running as sandbox_user.
Was this helpful? React with 👍 or 👎 to provide feedback.
| try: | ||
| upstream = socket.create_connection((host, port), timeout=HEAD_TIMEOUT) |
There was a problem hiding this comment.
🟥 DNS aliases bypass network isolation
The root proxy checks hostname text, then socket.create_connection resolves it without validating the destination. An agent-controlled hostname can reach loopback or private services despite the UID firewall.
Was this helpful? React with 👍 or 👎 to provide feedback.
df4f98b to
ed3f789
Compare
|
Follow-up on the two Devin flags, both addressed in the latest push (ed3f789):
|
Problem
Research tasks built from a published paper need the agent online (docs, other papers, package indexes) but must keep it away from the paper itself, its mirrors and its code. Today
network_modeoffers onlypublic,no-network, and anallowlistthat is parsed but not enforced. benchflow-ai/FrontierPhysics#365 asks for a URL denylist that works across the ACP harnesses.What this adds
network_mode: denylistwithblocked_urls(URL prefixes) andblocked_hosts(hosts and their subdomains) onsandboxandagent:Enforcement, for the agent only (oracle runs and the verifier are untouched):
sandbox/_egress_denylist_proxy.py) runs as root on127.0.0.1:18628inside the sandbox. It refuses matching URLs and hosts with a 403, tunnels everything else, and only terminates TLS for hosts that appear inblocked_urls, using leaf certificates minted on the host withcryptography. The CA private key never enters the sandbox.lockdown.py) is armed by a second marker,BENCHFLOW_EGRESS_DENYLIST=1, so the sandbox user can reach loopback only. A client that ignores proxy variables fails closed instead of bypassing. Docker getscap_add: NET_ADMINfrom a benchflow compose overlay; Daytona direct sandboxes already allow iptables.HTTP_PROXY/HTTPS_PROXY,NO_PROXYfor loopback, and CA paths (SSL_CERT_FILE,REQUESTS_CA_BUNDLE,CURL_CA_BUNDLE,GIT_SSL_CAINFO,NODE_EXTRA_CA_CERTS). LiteLLM is forced sandbox-local, as in the no-web mode.disallow_hosted_search_*fields onAgentConfig: ClaudeWebSearch(WebFetch stays on, it runs inside the sandbox), Codextools.web_search, Geminigoogle_web_searchandweb_fetch, OpenCode/MiMowebsearch.trajectory/egress_denylist.jsonlat cleanup (ts,action,method,url,rule).modal,apple-container, andagentcorerefuse the mode at preflight (enforces_denyliston the provider registry). Session-factory agents raise at connect time.sandbox/docker.pygrows by 6 lines andsandbox/daytona.pyis untouched; the mechanism lives insandbox/egress_denylist.pyand reuses the lockdown firewall.Evidence
bench eval run --sandbox daytona --agent codex-acp --model azure-foundry-openai/gpt-5.6-solon a probe task that curls a blocked arXiv page, a sibling page, example.com, a blocked host, and the blocked page through python: verifier sawblocked=403 allowed=200 tunnel=200 host=403 python=403, reward 1.0, and the block log listed the three refused attempts.tests/test_egress_denylist.py(policy matcher, in-process proxy against a local TLS upstream, certificate material, env, start/stop with a fake sandbox, firewall gate),tests/test_network_denylist_config.py(validation, capability gate, rubric mirrors),tests/test_hosted_search_policy.py(per-harness switches executed with bash, planes, rollout wiring). Full suite,ruff check,ruff format --check, andty checkpass locally.Hardening from an adversarial review pass
.and..segments, duplicate slashes, backslashes, and;path parameters, so/abs/../abs/<id>,//abs/<id>and/abs/%2e%2e/abs/<id>hit the same rule as/abs/<id>(GitHub serves such spellings directly).3232235777,0xc0a80101,0300.0250.1.1,127.1.blocked_hostsentries are matched exactly plus subdomains; onlyblocked_urlshosts strip a leadingwww..docker-compose.yaml) gets the same NET_ADMIN overlay as Docker.denylistis sandbox-level only:agent.network_modeandverifier.network_modereject it instead of parsing a policy nothing enforces.1-2-3-4.sslip.io,1.2.3.4.nip.io, hex labels) are refused like addresses; a name the agent controls remains the inherent limit of a hostname denylist and is documented as such.--self-gen-no-internetwins over the denylist (no-web is stricter); the bash-primitiveTaskRuntimerefuses denylist tasks because it never arms the proxy or firewall;DockerSandbox.restorere-addsNET_ADMIN.Hostheader from the request target, forwards only the declared request body after the head (a pipelined second request is dropped), half-closes each relay direction independently, and treats onlyhttp(s)://request targets as absolute-form.cryptographyis imported lazily inside certificate minting with a clear error, since it is only anagentcoreextra today; adding it to the core dependencies is a one-line follow-up if preferred.Limits (documented in docs/sandbox-hardening.md)
python3; the run needs a non-rootsandbox_user.Refs: benchflow-ai/FrontierPhysics#365. Related: #785 (allowlist enforcement; this PR does not touch that mechanism and keeps the backend classes unchanged).