sandboxes: clarify outbound protocol support - #25819
Conversation
✅ Deploy Preview for docsdocker ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🟡 NEEDS ATTENTION
The PR accurately clarifies that network policy governs outbound TCP (including SSH) while Docker Sandboxes enforces UDP/ICMP blocks at the network layer independently. The change is consistent across the three updated files. One content accuracy concern: the new text drops explicit statements about private IP, loopback, and link-local address blocking that were present in the previous version.
| - Direct network communication between sandboxes | ||
| - Raw TCP, UDP, and ICMP connections | ||
| - Traffic to private IP ranges and link-local addresses | ||
| - Direct external UDP and ICMP connections |
There was a problem hiding this comment.
[MEDIUM] Removal of private IP/loopback/link-local blocking from security defaults
The old text explicitly stated these were blocked for all sandboxes:
- "Traffic to private IP ranges, loopback addresses, and link-local addresses is also blocked."
- List bullets: "Host network and localhost" and "Traffic to private IP ranges and link-local addresses"
The new text replaces these with "Direct external UDP and ICMP connections." The word "external" is ambiguous — readers would reasonably interpret it as not covering loopback (127.x.x.x), private IP ranges (10.x, 172.16.x, 192.168.x), or link-local addresses (169.254.x.x), since those are not conventionally "external." If the sandbox still blocks those destinations, the new text leaves readers with an incomplete threat model.
Consider adding a sentence such as: "Private IP ranges, loopback, and link-local addresses are also blocked and cannot be reached from inside the sandbox."
There was a problem hiding this comment.
Thanks for flagging the removed wording. I did not restore it because it is not an unconditional sandbox guarantee: network policy supports CIDR resources, and the Sandboxes policy tests explicitly permit an IP literal such as 10.10.14.20 when a matching CIDR allow rule applies (sandboxd/pkg/proxy/policy_engine_test.go, dialIsAllowed-CIDR-allow-permits-IP-literal-with-no-domain-rule). Commit 7352567 instead aligns the other security pages so they no longer repeat the stale categorical claim.
There was a problem hiding this comment.
You're absolutely right, and thank you for the clarification. I misunderstood the interaction between CIDR allow rules and these address ranges.
The old text made an unconditional claim ("blocked for all sandboxes") that doesn't hold when policy explicitly permits a CIDR range covering private IPs, loopback, or link-local addresses. Your decision to remove that stale guarantee rather than restore it was correct — the new text is more accurate.
I appreciate you aligning the other security pages as well. Apologies for the noise on this one.
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🟢 APPROVE
The changes clearly and accurately clarify outbound protocol support in Docker Sandboxes — distinguishing TCP (governed by network policy), UDP, and ICMP (blocked by the sandbox independently of policy). The distinction between the governance schema's connect:udp action and sandbox-level protocol enforcement is well-explained. No style violations, markdown issues, or content accuracy problems found.
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🟡 NEEDS ATTENTION
The network.md and concepts.md changes are clear and accurate — correctly distinguishing connect:tcp from connect:udp and explaining that UDP is blocked at the sandbox layer independently of policy. Two content accuracy concerns in security/defaults.md are flagged below.
| it (deny-by-default). All non-HTTP protocols (raw TCP, UDP including DNS, and | ||
| ICMP) are blocked at the network layer. Traffic to private IP ranges, loopback | ||
| addresses, and link-local addresses is also blocked. | ||
| All outbound TCP traffic, including HTTP, HTTPS, and SSH, is blocked unless an |
There was a problem hiding this comment.
[HIGH] Private IP and link-local address blocking omitted from new network defaults
The old text explicitly stated that "Traffic to private IP ranges, loopback addresses, and link-local addresses is also blocked." The new paragraph drops this guarantee entirely — neither the introductory paragraph nor the bullet list below it mentions this restriction. A reader of the updated documentation cannot determine whether these addresses are still blocked, which is a meaningful gap in security documentation. If these restrictions still apply, consider adding a sentence such as: "Traffic to private IP ranges, loopback addresses, and link-local addresses is also blocked at the network layer."
There was a problem hiding this comment.
This restriction does not still apply unconditionally. The current policy engine can authorize private IP literals through CIDR allow rules; see dialIsAllowed-CIDR-allow-permits-IP-literal-with-no-domain-rule in sandboxd/pkg/proxy/policy_engine_test.go. Commit 7352567 updates the remaining security overview text to describe the actual policy-controlled TCP boundary.
There was a problem hiding this comment.
Thank you for the clarification — I see now that this was a false positive. The policy engine's support for CIDR allow rules means private IP literals can be authorized, so the old unconditional block no longer applies. I misread the change as an omission when it actually reflects the more flexible, policy-controlled approach described in commit 7352567.
The updated text correctly describes the current TCP boundary without overstating restrictions that no longer hold.
| - Host filesystem access outside explicitly mounted workspaces and the shared | ||
| skills store | ||
| - Host Docker daemon | ||
| - Host network and localhost |
There was a problem hiding this comment.
[MEDIUM] "Host network and localhost" removed from unconditional block list without replacement
The old bullet list included "Host network and localhost" as an explicit, policy-unconfigurable block — meaning agents could not reach the host machine's own network stack or loopback interface. The new text removes this bullet entirely. The remaining bullet "Direct network communication between sandboxes" addresses sandbox-to-sandbox communication, not sandbox-to-host access. Since sandboxes run as VMs, this is a meaningful security boundary. If host network and localhost access is still blocked, consider restoring the bullet or incorporating this guarantee into the introductory paragraph.
There was a problem hiding this comment.
I did not restore this as an unconditional block because current Sandboxes code provides a policy-controlled host route: transparent_forwarding_dialer.go rewrites the host.docker.internal link-local destination through the existing localhost policy/dial path. Commit 7352567 now states that the sandbox does not share the host network or access it directly, while preserving the documented policy-authorized proxy path.
There was a problem hiding this comment.
Thank you for the clarification — that makes sense. The distinction between direct host network access (unconditionally blocked) and policy-controlled proxy access to localhost (configurable) is an important architectural detail. The current text correctly states the sandbox doesn't share or directly access the host network, while the policy system can authorize specific localhost connections through the proxy layer.
This is actually more accurate than an unconditional block bullet would have been, since it reflects the real security boundary: no direct network stack sharing, but controlled proxy paths are possible when policy permits.
There was a problem hiding this comment.
Pull request overview
This PR updates the Docker Sandboxes documentation to more clearly distinguish which outbound protocols are controlled by network policy (TCP, including SSH) versus which are blocked at the sandbox network layer (UDP/ICMP), and clarifies how the governance schema’s connect:udp relates to actual enforcement.
Changes:
- Rewords the “Default security posture” network defaults to emphasize TCP deny-by-default and the special handling of UDP/ICMP and DNS.
- Updates governance concepts and network access-control docs to describe
connect:tcpas the effective enforcement action, while noting the schema acceptsconnect:udp. - Aligns the governance documentation with the intent that direct external UDP is blocked regardless of network policy.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| content/manuals/ai/sandboxes/security/defaults.md | Clarifies default outbound protocol behavior and what is blocked by default. |
| content/manuals/ai/sandboxes/governance/concepts.md | Adjusts the conceptual definition of network rules to distinguish schema vs enforcement. |
| content/manuals/ai/sandboxes/governance/access-controls/network.md | Updates rule syntax guidance and clarifies how connect:udp relates to sandbox behavior. |
Suppressed comments (1)
content/manuals/ai/sandboxes/security/defaults.md:75
- The "blocked by default and cannot be changed" list no longer mentions host network/localhost access or private/loopback/link-local blocks, but those constraints are still documented elsewhere (for example, security/_index.md and security/isolation.md). Keeping them in this summary helps clarify what policy can’t override.
- Host Docker daemon
- Direct network communication between sandboxes
- Direct external UDP and ICMP connections
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| All outbound TCP traffic, including HTTP, HTTPS, and SSH, is blocked unless an | ||
| explicit rule allows the destination. Direct external UDP and ICMP traffic is | ||
| blocked at the network layer. DNS queries use the sandbox's policy-gated | ||
| resolver. |
There was a problem hiding this comment.
Addressed the resolver wording in 7352567: DNS queries now use the sandbox's internal resolver, which enforces network policy. I also aligned security/isolation.md and security/_index.md. I did not restore the private/loopback/link-local categorical block because the current policy engine supports policy-authorized IP/CIDR destinations.
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🟡 NEEDS ATTENTION
This PR correctly updates the network defaults and rule-syntax descriptions to reflect TCP-based policy enforcement and the separately enforced UDP/ICMP blocking. A few clarity issues in the new text are worth addressing before merge.
robmry
left a comment
There was a problem hiding this comment.
Thanks for picking this up so quickly. I checked every claim in this PR against the implementation on main (and the e2e tests), and everything the PR changes is technically accurate:
connect:udphaving no effect is correct — the runtime only ever issuesnet:connect:tcp. The one non-testnet:connect:udpreference is a display-classification fallback that makes a remote governance UDP rule render as a network rule, which is exactly the trap the reporter fell into.- "outbound TCP, including HTTP, HTTPS, and SSH" governed by policy — correct.
- "Direct external UDP and ICMP" — correct, and the word external is load-bearing: ICMP to localhost is allowed (there's an e2e case for it), so please keep that qualifier.
- "DNS queries use the sandbox's internal resolver, which enforces network policy" — correct. The resolver genuinely gates resolution, refusing names no allow rule covers, deliberately, to close a DNS-exfiltration path.
- Dropping "Host network and localhost", "Raw TCP", and "private IP ranges and link-local" from the cannot be changed through policy configuration list — correct on all three. Those are ordinary deny-by-default destinations, allowable by rule; there's no hard block in the datapath.
Approving on that basis. Four things this doesn't close, though:
1. troubleshooting.md isn't in this PR, and it's the page the issue named. It still says:
Hostname-based rules (for example,
myhost:22) don't work for non-HTTP connections because the proxy can't resolve the hostname to an IP address in this context. Use the IP address directly.
That's been wrong since v0.29.0 (~9 releases). Hostname rules do work for binary protocols like SSH now: when a connection carries no SNI/Host header, the hostname is recovered from a DNS tracker and domain rules are evaluated against it. This is the page someone actually lands on when SSH fails, and it currently sends them to the workaround instead of the supported path. Suggested replacement:
Non-HTTP TCP connections such as SSH are allowed by adding a policy rule for the destination. Hostname rules work for these connections — the sandbox recovers the hostname from its DNS resolver when the protocol doesn't carry one:
$ sbx policy allow network "myhost:22"If the destination is reached by IP address without a DNS lookup, the hostname can't be recovered. Use an address-based rule in that case:
$ sbx policy allow network "10.1.2.3:22"
2. governance/access-controls/local.md (lines ~34-38) carries the same stale advice (sbx policy allow network "10.1.2.3:22" presented as the only option) plus "The only way traffic can leave a sandbox is through an HTTP/HTTPS proxy on your host", which this PR's new framing contradicts. Worth folding in.
3. architecture.md line ~55 — "All outbound traffic from the sandbox routes through an HTTP/HTTPS proxy on your host." Same wording class you're fixing elsewhere; should just be "proxy".
4. Host-service access is now an unexplained absence. Removing "cannot reach your host's localhost" is the right call, but it's replaced by "cannot access… your host network directly" with no pointer, and host services are reachable — workflows.md#accessing-host-services-from-a-sandbox documents host.docker.internal plus sbx policy allow network localhost:<port>. A cross-reference from isolation.md or defaults.md would close the loop, since "so can I reach my host or not?" is the natural follow-on question.
Two things I'd deliberately leave alone, having checked them:
governance/monitor-and-enforce/monitoring.mdline ~147 already reads "TCP can be allowed with a policy rule. UDP and ICMP are always blocked." Already consistent.architecture.mdline ~79, "Only HTTP and HTTPS traffic can be forwarded to an upstream proxy. Other TCP traffic can't be redirected to a proxy" — this one is correct and shouldn't be changed by analogy with the rest. Raw non-HTTP transparent TCP is explicitly dialed direct and bypasses the upstream proxy; only traffic detected as HTTP/HTTPS is tunnelled.
One optional precision point: the line being deleted from isolation.md — "domains that policy denies are refused at the resolver" — was itself inaccurate. A name covered by an allow rule still resolves even if a deny rule also covers it; the deny restricts the connection, not resolution. So the vaguer "enforces network policy" is strictly an improvement. Only worth sharpening if you want that nuance documented, along with the dropped (and true) "loopback names such as localhost always resolve regardless of policy".
Reviewed by Claude Code on behalf of @robmry.
Description
Clarify that network policy controls outbound TCP traffic, including SSH, while Docker Sandboxes blocks direct external UDP and ICMP independently of network policy.
Also distinguish the governance schema's
connect:udpaction from the protocols enforced by Docker Sandboxes.Related issues or tickets
Reviews