From 3bed7c2f2af858e2df29d576a7bed33f12f6f624 Mon Sep 17 00:00:00 2001 From: claude-hdb Date: Tue, 14 Jul 2026 01:41:18 +0000 Subject: [PATCH] fix: isolate boxes with the bridge's port-isolation flag, not an nft rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The nft bridge-family rule from the previous commit is LIVE on the host and boxes still reach each other: table bridge claudebox { chain forward { ... meta ibrname "claudenet" meta obrname "claudenet" drop } } FAIL BOX A REACHES BOX B — sibling isolation does NOT hold [tcp: refused] So the rule is not wrong about intent, it is wrong about mechanism — whatever path these frames take, that hook does not stop them. Rather than reason harder about netfilter (reasoning is what put the hole there in the first place), use the mechanism Incus provides for exactly this: security.port_isolation on the bridged NIC, which sets the kernel bridge port's isolated flag so two isolated ports cannot exchange frames at all. The nft rule stays as a second layer — it costs nothing — but the profile flag is what carries the guarantee. doctor.sh checks it, because the absence of this one is invisible: everything works and boxes can simply reach each other. Co-Authored-By: Claude Fable 5 --- drill/doctor.sh | 9 +++++++++ profiles/claude-dev.yaml | 11 +++++++++++ 2 files changed, 20 insertions(+) diff --git a/drill/doctor.sh b/drill/doctor.sh index 9f8ba33..464ef59 100755 --- a/drill/doctor.sh +++ b/drill/doctor.sh @@ -91,6 +91,15 @@ fi head_ "Profile — claude-dev (the NIC is the isolation contract)" if incus profile show claude-dev >/dev/null 2>&1; then + iso="$(incus profile device get claude-dev eth0 security.port_isolation 2>/dev/null)" + if [ "$iso" = "true" ]; then + ok "security.port_isolation = true — boxes cannot reach each other at L2" + else + no "security.port_isolation is NOT set — BOXES CAN REACH EACH OTHER" + inf "an L3 ACL cannot do this: two boxes on one bridge are on the same L2" + inf "segment, so their frames are switched, never routed past the ACL." + inf "fix: re-run ~/.local/share/claudebox/host/setup-host.sh" + fi for k in security.mac_filtering security.ipv4_filtering; do v="$(incus profile device get claude-dev eth0 "$k" 2>/dev/null)" if [ -z "$v" ]; then diff --git a/profiles/claude-dev.yaml b/profiles/claude-dev.yaml index 4b4850d..1273fdd 100644 --- a/profiles/claude-dev.yaml +++ b/profiles/claude-dev.yaml @@ -8,6 +8,17 @@ devices: type: nic network: claudenet name: eth0 + # Boxes must not reach each other. This is the mechanism that actually does + # it: the kernel bridge's port-isolation flag, which stops two isolated + # ports exchanging frames at L2. + # + # It is not an ACL rule, and it cannot be. Incus ACLs are L3/L4, and two + # boxes on one bridge are on the same L2 segment — their frames are switched + # between ports and never traverse the netfilter path an ACL lives on. That + # is why the ACL's drop on 10.0.0.0/8 (which contains claudenet) and its + # default ingress drop BOTH looked airtight while box→box was wide open: a + # live probe found box A's SYN arriving at box B and B answering with a RST. + security.port_isolation: "true" root: type: disk pool: default