M14.8: fail closed — a per-domain allowlist is refused, not approximated - #36
Merged
Conversation
docs/14 §policy specifies a per-domain egress allowlist served by a proxy. The proxy does not exist, so no tier can tell `api.github.com` from anything else — and both T2 tiers resolved that by opening the gate. T2 Linux read a non-empty allowlist as "do not pass `--unshare-net`". T2 macOS emitted `(allow network-outbound)` **and `(allow network-bind)`**. So a policy naming one host granted the host's entire network, the loopback services sitting beside the sandbox, the LAN, the cloud metadata endpoint — and, on macOS, inbound bind — while the field it came from read like a restriction. docs/14 described this as "an all-or-nothing switch", which understated it: the "on" position was everything. Never reachable — every caller in the tree passes `NetPolicy::default()`, and `plugin.toml`'s `net` never reached `SandboxPolicy`. One caller away on two tiers is not a margin worth keeping. `NetPolicy::enforceable()` now refuses a non-empty `allow` at `create`, wired into T0 and both T2 tiers, with a `PolicyViolation` naming the field and the reason. Both permissive branches are **deleted** rather than left unreachable behind the check, so removing the check cannot resurrect them; `--unshare-net` and `(deny network*)` are unconditional. Refused rather than downgraded to full deny, deliberately. Asking for one host and getting every host is the bug above; asking for network and silently getting none fails later, somewhere less obvious, as a timeout with no reason attached. An error is the only answer that is neither, and it arrives before anything is spawned. Falsified, both directions, per the brief: restoring `enforceable()` to a no-op and the macOS allowlist branch turns `net_policy_tests::a_named_host_is_refused_rather_than_approximated` red on the message, and the escape-suite case red with a live `SandboxHandle` — the sandbox cheerfully creating a session under a policy it cannot honour. Green again after restoring. docs/14 says the old description was wrong and why, rather than quietly correcting it. Not verified locally: `t2_linux.rs` is `#[cfg(target_os = "linux")]` and does not compile on this machine. CI is the authority for that file. Verified: fmt, clippy -D warnings, 1092 workspace tests, schemas, ts-sdk, sbom, deny. Claude-Session: https://claude.ai/code/session_017kFpYDqvz6sKGSkM4YKaRf
CI's Linux `check` job caught this; nothing on this machine could. Making `--unshare-net` unconditional left `Session.net` unread and `NetPolicy` imported only for a comment, both of which are errors under `-D warnings`. The field is deleted rather than silenced. It existed so `build_args` could decide whether to unshare the network namespace; that decision is gone — the answer is always yes — and `NetPolicy::enforceable` guarantees at `create` that `allow` is empty, so a stored copy would record a constant. When the egress proxy exists, what belongs there is the proxy's endpoint, not the policy. Worth recording for the next platform-conditional change: `cargo check --target x86_64-unknown-linux-gnu` does not work here either, and not for the reason handover.md gives. It is `zstd-sys` (pulled in through wasmtime) whose build script needs `x86_64-linux-gnu-gcc`, so even a link-free `check` fails in a build script. There is no local Linux verification of any kind for this crate; CI is the whole authority. Verified as far as this machine allows: `clippy -p panday-sandbox --all-targets -- -D warnings` clean. Claude-Session: https://claude.ai/code/session_017kFpYDqvz6sKGSkM4YKaRf
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
docs/14 §policy specifies a per-domain egress allowlist served by a proxy. The proxy does not exist, so no tier can tell
api.github.comfrom anything else — and both T2 tiers resolved that by opening the gate.net.allowdid--unshare-netnot passed → the host's entire network(allow network-outbound)+(allow network-bind)→ full egress and inbound bindSo a policy naming one host granted every host, the loopback services sitting beside the sandbox, the LAN, and the cloud metadata endpoint — while the field it came from read like a restriction. docs/14 called this "an all-or-nothing switch"; the "on" position was everything.
Never reachable, so this is hardening rather than an incident: every caller in the tree passes
NetPolicy::default(), andplugin.toml'snetnever reachedSandboxPolicy. One caller away on two tiers is not a margin worth keeping.What ships
NetPolicy::enforceable()refuses a non-emptyallowatcreate— wired into T0 and both T2 tiers — with aPolicyViolationnaming the field and the reason. Both permissive branches are deleted rather than left unreachable behind the check, so removing the check cannot resurrect them.--unshare-netand(deny network*)are unconditional.Refused rather than downgraded to full deny, deliberately. Asking for one host and getting every host is the bug above. Asking for network and silently getting none fails later, somewhere less obvious, as a timeout with no reason attached. An error is the only answer that is neither, and it arrives before anything is spawned.
Falsified, both directions
Per the brief — an unfalsified test proves nothing. Restoring
enforceable()to a no-op and putting the macOS allowlist branch back:net_policy_tests::a_named_host_is_refused_rather_than_approximated→ red on the message.t2_macos_escape::a_named_host_in_the_allowlist_is_refused_not_granted→ red with a liveSandboxHandle { id: "t2m-0" }, i.e. the sandbox cheerfully creating a session under a policy it cannot honour.Both green again after restoring: 30/30 lib, 16/16 escape suite.
Not verified locally
t2_linux.rsis#[cfg(target_os = "linux")]and does not compile on this machine. CI is the authority for that file (brief rule 12). The macOS half is verified locally.Verification
fmt · clippy
--workspace --all-targets -D warnings· 1092 workspace tests, 0 failed · schemas · ts-sdk · sbom (609 components) · deny.Second commit is
RUN-REPORT.md, kept out of the milestone commit.https://claude.ai/code/session_017kFpYDqvz6sKGSkM4YKaRf