Skip to content

feat(sandbox): enforce Linux network isolation - #11659

Merged
marius-kilocode merged 5 commits into
mainfrom
feat/linux-network-sandbox
Jun 25, 2026
Merged

feat(sandbox): enforce Linux network isolation#11659
marius-kilocode merged 5 commits into
mainfrom
feat/linux-network-sandbox

Conversation

@marius-kilocode

Copy link
Copy Markdown
Collaborator

The Linux sandbox network setting currently blocks classified in-process HTTP tools but leaves spawned commands in the host network namespace. Shell commands and their descendants can therefore retain TCP and UDP access even when network restriction is enabled.

This change applies the scoped network profile to Bubblewrap launches. Deny mode creates a separate network namespace, while allow mode keeps host networking. The backend probes network namespace support separately from filesystem namespace support and fails explicit restricted launches closed when the host cannot provide the required capability. Unsupported proxy and host-allowlist profiles continue to fail closed.

Linux deny mode blocks host TCP and UDP over IPv4 and IPv6 while preserving loopback communication between processes inside the same sandbox namespace. This matches the macOS outbound-network guarantee, although Linux namespace isolation also prevents the host from initiating connections into the sandbox. Pathname Unix socket authority remains a separate boundary tracked in #11650.

Closes #11651.

Comment thread packages/core/test/kilocode/linux-sandbox.test.ts
Comment thread packages/core/test/kilocode/linux-sandbox.test.ts Outdated
@kilo-code-bot

kilo-code-bot Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (1 files)
  • packages/opencode/test/kilocode/sandbox/state.test.ts
Previous Review Summaries (3 snapshots, latest commit 10d4308)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 10d4308)

Status: 1 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
packages/opencode/test/kilocode/sandbox/state.test.ts 52 The new status regression test still assumes base Bubblewrap support on every Linux host, so it can fail for missing host capability instead of the allow-vs-deny support split under review.

Fix these issues in Kilo Cloud

Files Reviewed (2 files)
  • packages/opencode/src/kilocode/sandbox/policy.ts
  • packages/opencode/test/kilocode/sandbox/state.test.ts - 1 issue

Previous review (commit 2a02e75)

Status: 1 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
packages/core/test/kilocode/linux-sandbox.test.ts 57 requireNetwork() still asserts deny-mode support instead of skipping unsupported hosts, so these tests continue to fail for environment capability rather than the behavior under test.

Fix these issues in Kilo Cloud

Files Reviewed (1 file)
  • packages/core/test/kilocode/linux-sandbox.test.ts - 1 issue

Previous review (commit 7f4702b)

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
packages/core/test/kilocode/linux-sandbox.test.ts 171 Deny-mode Linux tests after the first case are not gated on backendSupport({ mode: "deny" }), so kernels without --unshare-net support will fail for environment reasons.
packages/core/test/kilocode/linux-sandbox.test.ts 186 The new IPv6 loopback assertion depends on ::1 being enabled, which is not guaranteed in CI containers.

Fix these issues in Kilo Cloud

Files Reviewed (6 files)
  • .changeset/isolate-linux-network.md
  • packages/core/test/kilocode/linux-sandbox.test.ts - 2 issues
  • packages/kilo-sandbox/src/backend.ts
  • packages/kilo-sandbox/src/bubblewrap.ts
  • packages/kilo-sandbox/test/backend.test.ts
  • packages/opencode/test/kilocode/sandbox/shell-network.test.ts

Reviewed by gpt-5.4-20260305 · Input: 52.4K · Output: 4.3K · Cached: 209.9K

Review guidance: REVIEW.md from base branch main

@marius-kilocode

Copy link
Copy Markdown
Collaborator Author

Linux network sandbox red-team conclusion

I completed adversarial validation of the Linux network sandbox using the packaged Linux ARM64 VSIX from implementation commit 7f4702bec9 in an Ubuntu OrbStack VM. PR head 2a02e757fb additionally hardens the integration tests without changing runtime behavior. The installed test extension was version 7.3.55, with bundled CLI version 0.0.0-feat-linux-network-sandbox-202606241754 and bundled Bubblewrap 0.11.2.

The validation combined real package integration tests, controlled tests against a live contained host-side broker, and a broader ten-phase adversarial run. All listeners and canaries were disposable and confined to /home/marius/kilo-sandbox-network-redteam inside the VM. No public service, host credential, real authority socket, or macOS host resource was contacted.

Confirmed behavior

  • Deny mode created a distinct network namespace with no host Ethernet interface and no host IPv4 or IPv6 route.
  • A live unsandboxed control client reached the contained host TCP listener, while the same direct TCP connection from Bubblewrap deny mode failed.
  • Real Linux integration coverage blocked host TCP and UDP over IPv4 and IPv6, including localhost, IPv4-mapped IPv6, and descendant processes.
  • Allow mode retained host loopback connectivity to the prepared listeners.
  • TCP and UDP loopback communication between processes inside the same isolated namespace remained usable.
  • Child, grandchild, and detached processes did not regain host networking.
  • unshare, nsenter, nested Bubblewrap, and direct setns() attempts did not recover another network namespace.
  • PID 1 visible inside the sandbox was the Bubblewrap init process in the same isolated network namespace, so /proc/1/ns/net did not expose the host namespace.
  • No inherited socket descriptor was present in the tested process.
  • Raw IPv4 and packet socket creation failed without CAP_NET_RAW.
  • Classified in-process HTTP requests were rejected by Kilo's scoped network policy before reaching the prepared endpoint.
  • Filesystem confinement continued to hold while network deny mode was active.

These results provide strong evidence that the PR enforces its stated boundary: sandbox-created TCP and UDP sockets cannot reach services in the VM's host network namespace when network restriction is enabled.

Unix socket boundary

Controlled testing with the broker confirmed that Unix socket behavior must remain a separate capability boundary:

  • A host abstract Unix socket was not reachable from the isolated network namespace.
  • A pathname Unix socket exposed through the filesystem remained reachable while the broker was live.
  • The pathname broker could pass an already-connected host-network TCP descriptor with SCM_RIGHTS, and the sandboxed process could use that descriptor.

The pathname and passed-descriptor cases are delegated authority, not failures of --unshare-net to isolate sandbox-created IP sockets. They remain tracked in #11650. This PR must not claim that all host IPC or all possible network authority is removed.

Fixture caveat

The later ten-phase adversarial run started after the prepared broker process had exited. Its repeated ECONNREFUSED results and empty events.jsonl therefore cannot independently distinguish a blocked connection from a dead listener. Broker-dependent rows from that run are classified as inconclusive in the detailed matrix rather than as additional passes.

This fixture-lifecycle problem does not invalidate the earlier controlled live-broker checks or the real integration tests, but it does invalidate the broad run's claims that pathname Unix delegation was blocked or that zero broker events independently proved every direct connection case.

The following findings from the broad run remain valid because they do not depend on broker liveness:

  • isolated namespace topology and absence of host routes
  • internal TCP and UDP loopback
  • descendant confinement
  • namespace recovery failures
  • raw and packet socket denial
  • in-process WebFetch denial
  • absence of inherited socket descriptors

Remaining limitations

  • AF_VSOCK socket creation succeeded, but no connection was attempted by design, so VSOCK host reachability remains inconclusive.
  • Pathname Unix socket and descriptor delegation remain confirmed outside the IP network boundary and require Sandbox: contain delegated authority through Unix sockets #11650.
  • Linux namespace isolation is slightly stronger than macOS for inbound IP behavior: the host cannot initiate an ordinary connection into the isolated namespace without an explicitly configured bridge.

Conclusion

The Linux implementation passes the intended direct IP-network boundary for TCP, UDP, IPv4, IPv6, descendants, and common namespace recovery attempts. No direct IP breakout or namespace escape was found. The result supports this PR's stated scope while preserving explicit separation between IP namespace isolation and delegated authority through filesystem-visible Unix sockets.

@marius-kilocode

Copy link
Copy Markdown
Collaborator Author

Linux network sandbox red-team attack matrix

This matrix records the packaged-artifact controls, automated Linux integration coverage, and the broader ten-phase adversarial run for PR #11659. Runtime behavior was tested from implementation commit 7f4702bec9; PR head 2a02e757fb adds test portability hardening without changing the backend.

Classification used below:

  • BLOCKED: the relevant boundary was exercised against a live oracle or did not depend on broker liveness, and access failed.
  • ALLOWED_EXPECTED: behavior intentionally allowed inside the isolated namespace or in allow mode.
  • DELEGATED_UNIX: a filesystem-visible pathname Unix socket delegated host authority. Tracked separately in Sandbox: contain delegated authority through Unix sockets #11650.
  • INCONCLUSIVE: the prepared broker was not alive during that specific broad-run attempt, or no connection was attempted.
  • OBSERVATION: topology or capability evidence rather than an independent breakout attempt.

Packaged artifact and live-broker controls

These checks used the installed Linux ARM64 VSIX with the contained broker confirmed listening immediately before the sandboxed operation.

Attack Exact command or tool Observed result Broker/canary result Classification
Unsandboxed TCP fixture control Python/curl to prepared 127.0.0.1 port Returned NETWORK_REDTEAM_TCP_OK Broker received request ALLOWED_EXPECTED control
Sandboxed direct TCP Packaged bwrap --unshare-net ... probe.py tcp4 ECONNREFUSED Live broker received no direct request BLOCKED
Internal TCP loopback Packaged deny-mode Bubblewrap running internal_loopback.py INTERNAL_LOOPBACK_OK Host broker not involved ALLOWED_EXPECTED
Host abstract Unix socket probe.py unix-abstract in deny-mode Bubblewrap ECONNREFUSED Live host abstract listener not reached BLOCKED
Host pathname Unix socket probe.py unix-path in deny-mode Bubblewrap NETWORK_REDTEAM_UNIX_OK Live broker received PING DELEGATED_UNIX
Pathname Unix canary write probe.py unix-write in deny-mode Bubblewrap Broker acknowledged write Only disposable pathname canary changed DELEGATED_UNIX
Host TCP FD via pathname Unix probe.py passed-fd using SCM_RIGHTS Returned NETWORK_REDTEAM_TCP_OK Broker logged /passed-fd; disposable canary changed DELEGATED_UNIX

The last three rows do not show a sandbox-created IP socket crossing --unshare-net. They show a separate filesystem-visible Unix IPC channel delegating authority from the host namespace.

Automated real Linux integration coverage

These cases execute the actual Bubblewrap backend and process spawner on Linux rather than only inspecting generated arguments.

Attack or behavior Exact test path Observed result Classification
Filesystem confinement baseline Spawned process writes inside and outside profile roots Inside write succeeded; outside write remained unchanged BLOCKED outside / ALLOWED_EXPECTED inside
Network allow mode Host TCP listener plus allow profile Listener received sandbox-tcp-ok exchange ALLOWED_EXPECTED
Network deny TCP Same host TCP listener plus deny profile Listener received nothing BLOCKED
Network deny UDP Prepared UDP4 listeners under allow and deny profiles Allow received one datagram; deny received none BLOCKED in deny mode
localhost alias Host IPv4 listener addressed as localhost Listener received nothing BLOCKED
IPv6 loopback Host ::1 listener when host IPv6 is available Listener received nothing BLOCKED
IPv6 unavailable host Capability-gated IPv6 integration case IPv6-only case skips; IPv4 and namespace coverage still run Environment capability handling
Internal namespace loopback Parent server and child client inside one deny-mode command Exchange succeeded ALLOWED_EXPECTED
Descendant process Child launches grandchild client under deny profile Host listener received nothing BLOCKED
Filesystem plus network deny Project and outside writes while deny mode active Project write succeeded; outside canary unchanged Filesystem boundary preserved
Network namespace capability Behavioral probe includes --unshare-net Available host reports support OBSERVATION
Blocked network namespace Wrapper permits ordinary Bubblewrap probe but rejects --unshare-net Allow support remains available; deny support unavailable; confined deny launch fails closed BLOCKED / fail closed
Missing Bubblewrap Explicit unavailable-helper fixture Confined launch fails closed BLOCKED / fail closed
User configuration path sandbox_restrict_network through actual shell tool and process spawner Disabled setting reaches listener; enabled setting does not End-to-end policy enforcement
In-process network policy Classified HTTP tool under deny profile Request rejected before transport BLOCKED
Generated Bubblewrap allow arguments Unit test of allow profile No --unshare-net Expected allow semantics
Generated Bubblewrap deny arguments Unit test of deny profile Includes --unshare-net Expected deny semantics

Every deny-mode integration case now explicitly asserts that the host supports the required network namespace. IPv6 is a separate capability-gated case, so hosts with IPv6 disabled do not fail before exercising the IPv4 boundary.

Broader ten-phase adversarial run

The contained broker process exited before this run began. Rows that depended on receiving a broker response are marked INCONCLUSIVE, even when the observed failure is consistent with correct isolation. The rows that exercise namespace topology, internal loopback, process containment, local capabilities, or in-process policy remain valid.

Phase Attack Exact command or tool Exit/error Classification
1 Identity and working directory pwd, id Project directory; unprivileged mapped identity OBSERVATION
1 User namespace readlink /proc/self/ns/user Isolated namespace inode OBSERVATION
1 Mount namespace readlink /proc/self/ns/mnt Isolated namespace inode OBSERVATION
1 PID namespace readlink /proc/self/ns/pid Isolated namespace inode OBSERVATION
1 Network namespace readlink /proc/self/ns/net net:[4026533069] OBSERVATION
1 Interface audit /proc/self/net/dev, /sys/class/net, ip address Loopback and down tunnel devices only; no host Ethernet OBSERVATION
1 IPv4 route audit /proc/self/net/route, ip route No host route OBSERVATION
1 IPv6 route audit /proc/self/net/ipv6_route, ip -6 route Loopback-local entries only OBSERVATION
1 Inherited descriptor audit probe.py fds No inherited socket descriptor OBSERVATION
2 TCP4 literal probe.py tcp4 ECONNREFUSED INCONCLUSIVE in this run; live-broker control above is BLOCKED
2 TCP4 localhost probe.py tcp4-localhost ECONNREFUSED INCONCLUSIVE in this run; automated test is BLOCKED
2 Decimal IPv4 probe.py tcp4-decimal ECONNREFUSED INCONCLUSIVE in this run
2 Curl TCP4 curl --noproxy '*' to prepared port Exit 7, connection refused INCONCLUSIVE in this run
2 Netcat TCP4 HTTP request piped to nc Exit 1, connection refused INCONCLUSIVE in this run
2 Node TCP4 net.connect() to prepared port ECONNREFUSED INCONCLUSIVE in this run
3 UDP4 probe.py udp4 Timeout INCONCLUSIVE in this run; automated test is BLOCKED
3 UDP6 probe.py udp6 Timeout INCONCLUSIVE in this run
3 TCP6 probe.py tcp6 ECONNREFUSED INCONCLUSIVE in this run; automated IPv6 test is BLOCKED
3 IPv4-mapped IPv6 probe.py tcp6-mapped ECONNREFUSED INCONCLUSIVE in this run
3 DNS getent ahosts network-redteam.invalid No result BLOCKED by absent route/resolver response
3 Reserved external address nc 192.0.2.1 9 Network unreachable BLOCKED
3 Host IP ping Prepared host_ips loop No host IP was exposed by fixture INCONCLUSIVE
3 Loopback ICMP ping 127.0.0.1 EPERM, no CAP_NET_RAW INCONCLUSIVE for ICMP; TCP/UDP internal loopback tested separately
4 Internal TCP loopback internal_loopback.py INTERNAL_LOOPBACK_OK ALLOWED_EXPECTED
4 Internal UDP loopback One command starts server/client descendants UDP_LOOPBACK_OK ALLOWED_EXPECTED
5 Child TCP/UDP Child runs prepared probes Refused/timeout INCONCLUSIVE against dead broker; namespace inheritance supported by automated live-listener test
5 Grandchild TCP/UDP Grandchild runs prepared probes Refused/timeout INCONCLUSIVE against dead broker; automated descendant test is BLOCKED
5 Detached delayed process setsid() child waits then probes Refused and remained in namespace OBSERVATION; no namespace escape seen
5 Process-group teardown Non-detached children after parent exit Terminated before logging BLOCKED by lifecycle confinement
6 New network namespace unshare -n /bin/true EPERM BLOCKED
6 New user and network namespaces unshare -Urn /bin/true ENOSPC BLOCKED
6 Enter PID 1 network namespace nsenter -t 1 -n EPERM BLOCKED
6 Enter parent network namespace nsenter -t "$PPID" -n EPERM BLOCKED
6 Procfs namespace visibility Compare /proc/1/ns/net and /proc/self/ns/net Same isolated namespace Host namespace not exposed
6 Nested Bubblewrap without network unshare Bundled bwrap nested launch ENOSPC No recovery path
6 Nested Bubblewrap with network unshare Bundled bwrap --unshare-net nested launch ENOSPC No recovery path
6 Direct setns() Open /proc/1/ns/net then call setns() EPERM BLOCKED
7 Raw IPv4 socket AF_INET, SOCK_RAW, ICMP EPERM BLOCKED
7 Packet socket AF_PACKET, SOCK_RAW EPERM BLOCKED
7 VSOCK creation Create AF_VSOCK socket only Creation succeeded; no connect attempted INCONCLUSIVE
7 Host abstract Unix socket probe.py unix-abstract ECONNREFUSED INCONCLUSIVE in dead-broker run; live-broker control is BLOCKED
7 Host pathname Unix socket probe.py unix-path and unix-write ECONNREFUSED INCONCLUSIVE in dead-broker run; live-broker control confirms DELEGATED_UNIX
7 Host TCP FD delegation probe.py passed-fd using SCM_RIGHTS Control connection refused INCONCLUSIVE in dead-broker run; live-broker control confirms DELEGATED_UNIX
8 Built-in WebFetch Prepared localhost URL Sandbox denied outbound network access BLOCKED independently of broker liveness
9 Proxy variables Inline HTTP/HTTPS/ALL proxy to prepared endpoint Connection refused INCONCLUSIVE in this run; direct namespace boundary covered by live-broker and integration tests
9 Hostname aliases localhost, localhost., 127.1, decimal IPv4, IPv6 aliases Refused or unresolved INCONCLUSIVE where broker-dependent
10 Canary verifier Compare before/after JSON Canaries unchanged; event log empty Fixture state only, not proof because broker was dead

Overall result

No direct IP breakout or namespace escape was found. The live-broker controls and real integration tests validate deny-mode isolation for sandbox-created TCP and UDP sockets. Internal loopback remains intentionally usable. Pathname Unix sockets and descriptors delegated through them remain a confirmed separate authority channel under #11650. The broad run adds useful namespace and capability evidence, but its dead-broker rows are deliberately not counted as independent passes.

@St0rmz1

St0rmz1 commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

Moderate, Functional: SandboxPolicy.status() still calls backendSupport() without the configured network profile. On Linux hosts where the base Bubblewrap probe succeeds but --unshare-net is unavailable, this reports the sandbox as enabled and available even though commands using the default deny mode will fail during confine().

Please derive the configured network mode in status() using the same rule as execute() and pass it to backendSupport({ mode, allowedHosts: [] }). A regression test should cover a host where base Bubblewrap support succeeds but the network namespace probe fails, confirming that deny mode reports unavailable while allow mode remains available.

Comment thread packages/opencode/test/kilocode/sandbox/state.test.ts
@marius-kilocode
marius-kilocode disabled auto-merge June 24, 2026 19:57
@marius-kilocode
marius-kilocode enabled auto-merge (squash) June 24, 2026 20:01
@marius-kilocode
marius-kilocode disabled auto-merge June 24, 2026 20:02
@marius-kilocode
marius-kilocode merged commit e3ad3bc into main Jun 25, 2026
24 checks passed
@marius-kilocode
marius-kilocode deleted the feat/linux-network-sandbox branch June 25, 2026 08:27
NJ-2020 pushed a commit to NJ-2020/kilocode that referenced this pull request Jul 4, 2026
t7tran pushed a commit to t7tran/kilocode that referenced this pull request Aug 14, 2026
t7tran pushed a commit to t7tran/kilocode that referenced this pull request Aug 14, 2026
…ndbox

feat(sandbox): enforce Linux network isolation
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sandbox: enforce network isolation for Linux commands

2 participants