Skip to content

M14.8: fail closed — a per-domain allowlist is refused, not approximated - #36

Merged
codeitlikemiley merged 3 commits into
mainfrom
m14.8-fail-closed-egress
Aug 23, 2026
Merged

M14.8: fail closed — a per-domain allowlist is refused, not approximated#36
codeitlikemiley merged 3 commits into
mainfrom
m14.8-fail-closed-egress

Conversation

@codeitlikemiley

Copy link
Copy Markdown
Owner

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.

TierWhat a non-empty net.allow did
T2 Linux--unshare-netnot passed → the host's entire network
T2 macOS(allow network-outbound)+ (allow network-bind) → full egress and inbound bind

So 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(), and plugin.toml's net never reached SandboxPolicy. One caller away on two tiers is not a margin worth keeping.

What ships

NetPolicy::enforceable() 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 — 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 live SandboxHandle { 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.rs is #[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

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
@codeitlikemiley
codeitlikemiley marked this pull request as ready for review August 23, 2026 18:45
@codeitlikemiley
codeitlikemiley merged commit 54d94ce into mainAug 23, 2026
6 checks passed
@codeitlikemiley
codeitlikemiley deleted the m14.8-fail-closed-egress branch August 23, 2026 18:45
Sign up for freeto 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.

1 participant

@codeitlikemiley
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
M14.8: fail closed — a per-domain allowlist is refused, not approximated by codeitlikemiley · Pull Request #36 · codeitlikemiley/panday · GitHub
Skip to content

M14.8: fail closed — a per-domain allowlist is refused, not approximated - #36

Merged
codeitlikemiley merged 3 commits into
mainfrom
m14.8-fail-closed-egress
Aug 23, 2026
Merged

M14.8: fail closed — a per-domain allowlist is refused, not approximated#36
codeitlikemiley merged 3 commits into
mainfrom
m14.8-fail-closed-egress

Conversation

@codeitlikemiley

Copy link
Copy Markdown
Owner

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.

TierWhat a non-empty net.allow did
T2 Linux--unshare-netnot passed → the host's entire network
T2 macOS(allow network-outbound)+ (allow network-bind) → full egress and inbound bind

So 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(), and plugin.toml's net never reached SandboxPolicy. One caller away on two tiers is not a margin worth keeping.

What ships

NetPolicy::enforceable() 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 — 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 live SandboxHandle { 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.rs is #[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

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
@codeitlikemiley
codeitlikemiley marked this pull request as ready for review August 23, 2026 18:45
@codeitlikemiley
codeitlikemiley merged commit 54d94ce into mainAug 23, 2026
6 checks passed
@codeitlikemiley
codeitlikemiley deleted the m14.8-fail-closed-egress branch August 23, 2026 18:45
Sign up for freeto 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.

1 participant

@codeitlikemiley
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' M14.8: fail closed — a per-domain allowlist is refused, not approximated by codeitlikemiley · Pull Request #36 · codeitlikemiley/panday · GitHub
Skip to content

M14.8: fail closed — a per-domain allowlist is refused, not approximated - #36

Merged
codeitlikemiley merged 3 commits into
mainfrom
m14.8-fail-closed-egress
Aug 23, 2026
Merged

M14.8: fail closed — a per-domain allowlist is refused, not approximated#36
codeitlikemiley merged 3 commits into
mainfrom
m14.8-fail-closed-egress

Conversation

@codeitlikemiley

Copy link
Copy Markdown
Owner

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.

TierWhat a non-empty net.allow did
T2 Linux--unshare-netnot passed → the host's entire network
T2 macOS(allow network-outbound)+ (allow network-bind) → full egress and inbound bind

So 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(), and plugin.toml's net never reached SandboxPolicy. One caller away on two tiers is not a margin worth keeping.

What ships

NetPolicy::enforceable() 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 — 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 live SandboxHandle { 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.rs is #[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

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
@codeitlikemiley
codeitlikemiley marked this pull request as ready for review August 23, 2026 18:45
@codeitlikemiley
codeitlikemiley merged commit 54d94ce into mainAug 23, 2026
6 checks passed
@codeitlikemiley
codeitlikemiley deleted the m14.8-fail-closed-egress branch August 23, 2026 18:45
Sign up for freeto 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.

1 participant

@codeitlikemiley
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' M14.8: fail closed — a per-domain allowlist is refused, not approximated by codeitlikemiley · Pull Request #36 · codeitlikemiley/panday · GitHub
Skip to content

M14.8: fail closed — a per-domain allowlist is refused, not approximated - #36

Merged
codeitlikemiley merged 3 commits into
mainfrom
m14.8-fail-closed-egress
Aug 23, 2026
Merged

M14.8: fail closed — a per-domain allowlist is refused, not approximated#36
codeitlikemiley merged 3 commits into
mainfrom
m14.8-fail-closed-egress

Conversation

@codeitlikemiley

Copy link
Copy Markdown
Owner

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.

TierWhat a non-empty net.allow did
T2 Linux--unshare-netnot passed → the host's entire network
T2 macOS(allow network-outbound)+ (allow network-bind) → full egress and inbound bind

So 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(), and plugin.toml's net never reached SandboxPolicy. One caller away on two tiers is not a margin worth keeping.

What ships

NetPolicy::enforceable() 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 — 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 live SandboxHandle { 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.rs is #[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

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
@codeitlikemiley
codeitlikemiley marked this pull request as ready for review August 23, 2026 18:45
@codeitlikemiley
codeitlikemiley merged commit 54d94ce into mainAug 23, 2026
6 checks passed
@codeitlikemiley
codeitlikemiley deleted the m14.8-fail-closed-egress branch August 23, 2026 18:45
Sign up for freeto 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.

1 participant

@codeitlikemiley
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' M14.8: fail closed — a per-domain allowlist is refused, not approximated by codeitlikemiley · Pull Request #36 · codeitlikemiley/panday · GitHub
Skip to content

M14.8: fail closed — a per-domain allowlist is refused, not approximated - #36

Merged
codeitlikemiley merged 3 commits into
mainfrom
m14.8-fail-closed-egress
Aug 23, 2026
Merged

M14.8: fail closed — a per-domain allowlist is refused, not approximated#36
codeitlikemiley merged 3 commits into
mainfrom
m14.8-fail-closed-egress

Conversation

@codeitlikemiley

Copy link
Copy Markdown
Owner

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.

TierWhat a non-empty net.allow did
T2 Linux--unshare-netnot passed → the host's entire network
T2 macOS(allow network-outbound)+ (allow network-bind) → full egress and inbound bind

So 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(), and plugin.toml's net never reached SandboxPolicy. One caller away on two tiers is not a margin worth keeping.

What ships

NetPolicy::enforceable() 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 — 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 live SandboxHandle { 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.rs is #[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

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
@codeitlikemiley
codeitlikemiley marked this pull request as ready for review August 23, 2026 18:45
@codeitlikemiley
codeitlikemiley merged commit 54d94ce into mainAug 23, 2026
6 checks passed
@codeitlikemiley
codeitlikemiley deleted the m14.8-fail-closed-egress branch August 23, 2026 18:45
Sign up for freeto 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.

1 participant

@codeitlikemiley
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' M14.8: fail closed — a per-domain allowlist is refused, not approximated by codeitlikemiley · Pull Request #36 · codeitlikemiley/panday · GitHub
Skip to content

M14.8: fail closed — a per-domain allowlist is refused, not approximated - #36

Merged
codeitlikemiley merged 3 commits into
mainfrom
m14.8-fail-closed-egress
Aug 23, 2026
Merged

M14.8: fail closed — a per-domain allowlist is refused, not approximated#36
codeitlikemiley merged 3 commits into
mainfrom
m14.8-fail-closed-egress

Conversation

@codeitlikemiley

Copy link
Copy Markdown
Owner

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.

TierWhat a non-empty net.allow did
T2 Linux--unshare-netnot passed → the host's entire network
T2 macOS(allow network-outbound)+ (allow network-bind) → full egress and inbound bind

So 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(), and plugin.toml's net never reached SandboxPolicy. One caller away on two tiers is not a margin worth keeping.

What ships

NetPolicy::enforceable() 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 — 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 live SandboxHandle { 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.rs is #[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

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
@codeitlikemiley
codeitlikemiley marked this pull request as ready for review August 23, 2026 18:45
@codeitlikemiley
codeitlikemiley merged commit 54d94ce into mainAug 23, 2026
6 checks passed
@codeitlikemiley
codeitlikemiley deleted the m14.8-fail-closed-egress branch August 23, 2026 18:45
Sign up for freeto 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.

1 participant

@codeitlikemiley
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' M14.8: fail closed — a per-domain allowlist is refused, not approximated by codeitlikemiley · Pull Request #36 · codeitlikemiley/panday · GitHub
Skip to content

M14.8: fail closed — a per-domain allowlist is refused, not approximated - #36

Merged
codeitlikemiley merged 3 commits into
mainfrom
m14.8-fail-closed-egress
Aug 23, 2026
Merged

M14.8: fail closed — a per-domain allowlist is refused, not approximated#36
codeitlikemiley merged 3 commits into
mainfrom
m14.8-fail-closed-egress

Conversation

@codeitlikemiley

Copy link
Copy Markdown
Owner

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.

TierWhat a non-empty net.allow did
T2 Linux--unshare-netnot passed → the host's entire network
T2 macOS(allow network-outbound)+ (allow network-bind) → full egress and inbound bind

So 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(), and plugin.toml's net never reached SandboxPolicy. One caller away on two tiers is not a margin worth keeping.

What ships

NetPolicy::enforceable() 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 — 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 live SandboxHandle { 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.rs is #[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

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
@codeitlikemiley
codeitlikemiley marked this pull request as ready for review August 23, 2026 18:45
@codeitlikemiley
codeitlikemiley merged commit 54d94ce into mainAug 23, 2026
6 checks passed
@codeitlikemiley
codeitlikemiley deleted the m14.8-fail-closed-egress branch August 23, 2026 18:45
Sign up for freeto 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.

1 participant

@codeitlikemiley
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); M14.8: fail closed — a per-domain allowlist is refused, not approximated by codeitlikemiley · Pull Request #36 · codeitlikemiley/panday · GitHub
Skip to content

M14.8: fail closed — a per-domain allowlist is refused, not approximated - #36

Merged
codeitlikemiley merged 3 commits into
mainfrom
m14.8-fail-closed-egress
Aug 23, 2026
Merged

M14.8: fail closed — a per-domain allowlist is refused, not approximated#36
codeitlikemiley merged 3 commits into
mainfrom
m14.8-fail-closed-egress

Conversation

@codeitlikemiley

Copy link
Copy Markdown
Owner

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.

TierWhat a non-empty net.allow did
T2 Linux--unshare-netnot passed → the host's entire network
T2 macOS(allow network-outbound)+ (allow network-bind) → full egress and inbound bind

So 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(), and plugin.toml's net never reached SandboxPolicy. One caller away on two tiers is not a margin worth keeping.

What ships

NetPolicy::enforceable() 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 — 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 live SandboxHandle { 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.rs is #[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

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
@codeitlikemiley
codeitlikemiley marked this pull request as ready for review August 23, 2026 18:45
@codeitlikemiley
codeitlikemiley merged commit 54d94ce into mainAug 23, 2026
6 checks passed
@codeitlikemiley
codeitlikemiley deleted the m14.8-fail-closed-egress branch August 23, 2026 18:45
Sign up for freeto 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.

1 participant

@codeitlikemiley