Skip to content

[Bubblewrap] Reject network policy that cannot be enforced on schema 0.8+ - #943

Closed
Soham Das (SohamDas2021) wants to merge 2 commits into
mainfrom
user/sodas/bwrap-reject-unsupported-network
Closed

[Bubblewrap] Reject network policy that cannot be enforced on schema 0.8+#943
Soham Das (SohamDas2021) wants to merge 2 commits into
mainfrom
user/sodas/bwrap-reject-unsupported-network

Conversation

@SohamDas2021

@SohamDas2021 Soham Das (SohamDas2021) commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Description

Summary

Two Bubblewrap network settings were accepted and then not enforced, so a run that asked for a restriction exited 0 having applied nothing — the worst failure shape for a security control, because it looks enforced.

  • enforcementMode: "firewall" / "both"NetworkIptablesManager scopes its chain to a container's host-side veth so it can be hooked into FORWARD. Unprivileged Bubblewrap has no veth, so bwrap_runner calls allow_missing_veth_interface() and the chain is built but never attached. A defaultPolicy: "block" allowlist filtered nothing; the warning went to stdout only, never to the exit code.
  • allowLocalNetwork — when the namespace cannot satisfy the request, this was a warning only.

Both now fail closed at parse/validate time. Schema 0.6/0.7 keep their current behaviour exactly, so existing callers are unaffected; the gate is 0.8+ only and is shared via wxc_common::config_parser::schema_enforces_network_strictly so there is no second copy of the version check to drift.

Also corrects three claims in bubblewrap-backend.md, including "still works but requires root", and replaces the stale CAP_NET_ADMIN rationale on the existing guard — the real reason is the missing veth, not privilege (namespace-scoped CAP_NET_ADMIN is rootless).

Validation

cargo test -p bwrap_common 69 pass · cargo test -p wxc_common 631+22+54 pass ·
fmt · clippy -D warnings · validate-configs.js 231 configs ·
run_bwrap_network_test.sh extended with both directions: schema 0.8 is rejected, schema 0.6 is explicitly not rejected.

Legacy behaviour is pinned by test, not just by claim:
bubblewrap_firewall_enforcement_is_accepted_before_0_8,
local_network_mismatch_is_not_rejected_before_0_8,
validate_accepts_the_same_request_before_0_8.

Notes for reviewers

Related Issues

  • AB#63099833 (the "reject unsupported" half; "map supported elements" remains blocked on the GA egress/ingress wire contract, which does not yet exist)

  • I have signed the Contributor License Agreement.

  • This pull request is related to an issue.

  • If this PR changes build commands, project architecture, or key conventions, I have updated .github/copilot-instructions.md.


GitHub Actions runs the PR validation build automatically. The ADO pipeline
(MXC-PR-Build) is the Azure version of the PR pipeline, kept in parity with the GitHub
Actions build; it runs on merge to main, and Microsoft reviewers with write access can trigger it
on a PR with /azp run. See docs/pull-requests.md.

If the dependency-feed-check check fails on a new dependency, the crate must be added to
the feed before the PR can pass. See docs/pull-requests.md
for the steps.

Microsoft Reviewers: Open in CodeFlow

Firewall enforcement modes build an iptables chain that is never hooked into
FORWARD, because unprivileged Bubblewrap has no veth to scope it to, so the run
reported success having filtered nothing. An allowLocalNetwork request the
namespace cannot satisfy was likewise only warned about.

Both now fail closed on schema 0.8 and later. Pre-0.8 configs keep their
current behavior so existing callers are unaffected.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ae9db9df-6439-46c3-9cb4-fd70e7cc43a3
@SohamDas2021
Soham Das (SohamDas2021) requested review from a team and a balanced review from Copilot August 19, 2026 00:17
@SohamDas2021
Soham Das (SohamDas2021) requested a review from a team as a code owner August 19, 2026 00:17
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds schema 0.8+ fail-closed validation for unsupported Bubblewrap network policies while preserving legacy behavior.

Changes:

  • Rejects unsupported firewall and local-network policies.
  • Adds regression tests for schema compatibility.
  • Corrects Bubblewrap networking documentation.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/core/wxc_common/src/config_parser.rs Adds schema-gated firewall validation.
src/backends/bubblewrap/common/src/bwrap_runner.rs Rejects unenforceable local-network requests.
src/backends/bubblewrap/common/src/bwrap_command.rs Centralizes namespace mismatch detection.
tests/scripts/run_bwrap_network_test.sh Adds firewall rejection E2E checks.
tests/configs/bubblewrap_network_firewall_rejected.json Adds schema 0.8 rejection fixture.
docs/bwrap-support/bubblewrap-backend.md Documents actual network limitations.
Suppressed comments (1)

src/core/wxc_common/src/config_parser.rs:1201

  • The immediately following guard still emits the old claim that iptables enforcement requires privilege (lines 1211–1212), contradicting this PR's corrected missing-veth rationale. Rootless user namespaces can provide namespace-scoped CAP_NET_ADMIN; the structural problem is that Bubblewrap exposes no host-side veth on which this manager can hook its chain. Update the emitted diagnostic so users are not directed toward elevation as a solution.
        // Bubblewrap's cooperative env-var proxy enforces hosts at the proxy
        // layer, which is mutually exclusive with iptables-based enforcement.
        // Rejected at every schema version, unlike the guard above.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/backends/bubblewrap/common/src/bwrap_runner.rs
Comment thread src/core/wxc_common/src/config_parser.rs Outdated
Comment thread tests/scripts/run_bwrap_network_test.sh

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (3)

src/backends/bubblewrap/common/src/bwrap_runner.rs:116

  • The fail-closed firewall check is still parser-only. ExecutionRequest and SandboxBackend are public, and spawn() explicitly supports requests that bypass config_parser; such a schema-0.8 request with Firewall/Both reaches needs_iptables_rules() and the missing-veth path unchanged. Mirror the firewall-mode rejection in backend validation as well (using the shared schema predicate), so every execution entry point fails closed.
        if let Some(reason) = bwrap_command::local_network_rejection(request) {
            return Err(ScriptResponse::error(reason));

tests/scripts/run_bwrap_network_test.sh:36

  • This test discards the process status and passes solely on matching text, so a regression that logs the warning but exits 0 would still be reported as a successful fail-closed rejection. Preserve and assert the non-zero status as well as the diagnostic.
FIREWALL_OUTPUT=$("$LXC_EXEC" --experimental \
    "$REPO_DIR/tests/configs/bubblewrap_network_firewall_rejected.json" 2>&1 || true)

src/backends/bubblewrap/common/src/bwrap_command.rs:156

  • This is now a hard validation error, but the diagnostic omits host rules even though either host list also makes uses_private_netns() false. For requests like the new validator test (defaultPolicy=block, no proxy, but blockedHosts), the message blames neither actual cause and recommends settings the caller already has. Include host rules in both the cause and remediation.
            "Bubblewrap: network.allowLocalNetwork=false is not enforced while the \

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Suppressed comments (1)

docs/sandbox-policy/0.8.0/networking/networking.md:396

  • Calling Model 2 “enforced” is incorrect on this branch. Bubblewrap’s current proxy path only injects proxy environment variables while sharing the host network namespace, so raw sockets and clients that ignore those variables can bypass it, as docs/bwrap-support/bubblewrap-backend.md:310-318 documents. The private-netns enforcement from #930/#931 is explicitly not part of this PR, so this should describe the proxy as cooperative rather than enforced.
> 0.6/0.7 keeps the previous warn-and-continue behavior). Model 2 — the proxy
> endpoint — is the enforced path today. Delivering the chain described above

Comment thread src/backends/bubblewrap/common/src/bwrap_command.rs
Comment thread src/core/wxc_common/src/config_parser.rs Outdated
Copilot AI review requested due to automatic review settings August 19, 2026 04:39
@SohamDas2021
Soham Das (SohamDas2021) force-pushed the user/sodas/bwrap-reject-unsupported-network branch from d384303 to a5501bf Compare August 19, 2026 04:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

src/backends/bubblewrap/common/src/bwrap_command.rs:139

  • Direct ExecutionRequest callers remain fail-open here: an enabled Bubblewrap proxy is cooperative, not an enforcing network boundary, so non-proxy-aware clients can bypass these host rules. Gate this exemption on an actually enforcing proxy network mode (or reject it on the current implementation), mirroring the parser change; network_proxy.is_enabled() alone is insufficient.
    let unenforced = matches!(
        request.policy.network_enforcement_mode,
        NetworkEnforcementMode::Capabilities
    ) && !request.policy.network_proxy.is_enabled();

docs/sandbox-policy/0.8.0/networking/networking.md:396

  • This says model 2 is enforced today, but the current Bubblewrap proxy is cooperative and leaves direct egress available to clients that ignore HTTP_PROXY/HTTPS_PROXY (docs/bwrap-support/bubblewrap-backend.md:310-319). That enforced proxy-only boundary belongs to the follow-up work, so the implementation-status text should not claim it already exists.
> endpoint — is the enforced path today. Delivering the chain described above

Comment on lines +1256 to +1257
&& !policy.network_proxy.is_enabled()
&& (!policy.allowed_hosts.is_empty() || !policy.blocked_hosts.is_empty())

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, and reproduced. With version: 0.8.0-alpha, defaultPolicy: block, allowedHosts: ["api.github.com"] and the builtin proxy, the sandbox shares the host network namespace and a direct request to a host that is not on the allowlist succeeds:

NETNS=net:[4026531833]   <- identical to the host netns
DIRECT_RC=200            <- http://example.com, not on the allowlist

Host lists suppress --unshare-net (uses_private_netns requires allowed_hosts.is_empty()), and the proxy is cooperative env-var injection only, so curl --noproxy '*' walks straight out. So is_enabled() is not an enforcement mechanism here, exactly as you describe.

Deferring the fix to the follow-up firewall-enforcement PR (Bundle B) rather than taking it here, because the two changes are coupled. Bundle B makes enforcementMode: "firewall" genuinely enforce, via iptables programmed inside the sandbox's own netns by a supervisor holding namespace-scoped CAP_NET_ADMIN (no elevation). Removing the proxy carve-out in isolation now would make host lists entirely unusable on Bubblewrap at 0.8 — firewall mode rejected, capabilities-without-proxy rejected, proxy rejected — leaving no accepted configuration. Landing it together with Bundle B means the carve-out is replaced by a mechanism that actually applies the lists, instead of by a dead end.

Bundle B also narrows this PR's blanket firewall rejection to hostname rule addresses only, per D3, so both gates are revised in the same change. Tracked there; pre-0.8 behaviour is unchanged either way.

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.

2 participants