Merge pull request #38 from claude-hdb/fix/firewall-restart

fix: isolate boxes with the bridge's port-isolation flag
This commit is contained in:
Daniel Marin 2026-07-14 02:42:35 +01:00 committed by GitHub
commit ff10325fb7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 52 additions and 7 deletions

View file

@ -78,8 +78,28 @@ else
inf "claudenet does not exist (a fresh host — setup-host.sh will create it)"
fi
head_ "Firewall — the box-to-box drop"
if nft list table bridge claudebox >/dev/null 2>&1 || sudo -n nft list table bridge claudebox >/dev/null 2>&1; then
ok "nft bridge table 'claudebox' is present — boxes cannot reach each other"
else
no "the box-to-box drop is MISSING — boxes can reach each other"
inf "an L3 ACL never sees frames switched between two ports of one bridge;"
inf "the drop is an nft BRIDGE-family rule, and without it siblings are wide open."
inf "fix: sudo /usr/local/sbin/claudebox-firewall"
inf " (or: sudo systemctl restart claudebox-firewall.service)"
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

View file

@ -204,18 +204,16 @@ KEEP="${KEEP:-0}"
# clean-ish slate and setup-host is the no-op it should be.
# A host still carrying a previous run's phase-D mutations mints boxes with no
# DNS, and then reports the resulting breakage as a finding. Refuse to run.
# NOTE: dns.mode=none is now part of the SHIPPED stack (it closes the sibling
# DNS-enumeration leak), so it is no longer "dirt" from a rehearsal — do not
# revert it. Only the vetoed NIC filtering counts as leftover.
dirty=""
[ -n "$(incus network get claudenet dns.mode 2>/dev/null)" ] && dirty="dns.mode"
[ -n "$(incus profile device get claude-dev eth0 security.ipv4_filtering 2>/dev/null)" ] && dirty="$dirty ipv4_filtering"
[ -n "$(incus profile device get claude-dev eth0 security.mac_filtering 2>/dev/null)" ] && dirty="$dirty mac_filtering"
if [ -n "$dirty" ]; then
note "this host still carries a previous run's phase-D mutations:$dirty — reverting them now"
incus network unset claudenet dns.mode >/dev/null 2>&1
note "this host carries the VETOED NIC filtering from an old rehearsal:$dirty — reverting"
incus profile device unset claude-dev eth0 security.mac_filtering >/dev/null 2>&1
incus profile device unset claude-dev eth0 security.ipv4_filtering >/dev/null 2>&1
incus network acl rule remove claude-isolate egress action=drop destination=@internal >/dev/null 2>&1
still="$(incus network get claudenet dns.mode 2>/dev/null)"
[ -n "$still" ] && { echo "drill: could not revert dns.mode ('$still'). run: bash drill/doctor.sh --fix" >&2; exit 1; }
fi
inf "clearing anything a previous run left behind…"

View file

@ -70,7 +70,14 @@ fi
sudo install -m 755 "$here/host/claudebox-firewall.sh" /usr/local/sbin/claudebox-firewall
sudo install -m 644 "$here/host/claudebox-firewall.service" /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now claudebox-firewall.service
sudo systemctl enable claudebox-firewall.service
# RESTART, not 'enable --now'. The unit is RemainAfterExit, so once it has run
# it stays "active" forever — and 'enable --now' does nothing to an active unit.
# Re-running setup-host after upgrading claudebox therefore installed the new
# rules to /usr/local/sbin and never applied them: the host kept the old
# firewall, silently, and the box→box hole stayed open through a release that
# claimed to close it. Restart re-runs the script, which is idempotent by design.
sudo systemctl restart claudebox-firewall.service
# Profile
if ! incus profile show claude-dev >/dev/null 2>&1; then
@ -78,4 +85,13 @@ if ! incus profile show claude-dev >/dev/null 2>&1; then
fi
incus profile edit claude-dev < "$here/profiles/claude-dev.yaml"
# The sibling drop is the one rule whose absence is invisible: everything keeps
# working, and boxes can simply reach each other. Assert it landed.
if nft list table bridge claudebox >/dev/null 2>&1; then
echo "Isolation: box-to-box drop is live (nft bridge table 'claudebox')."
else
echo "WARNING: the box-to-box drop is NOT active — boxes can reach each other." >&2
echo " check: sudo /usr/local/sbin/claudebox-firewall ; sudo nft list table bridge claudebox" >&2
fi
echo "Host ready. Launch with: claudebox new --name <box>"

View file

@ -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