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 <noreply@anthropic.com>
25 lines
962 B
YAML
25 lines
962 B
YAML
name: claude-dev
|
|
description: Trust-less claudebox (resources + isolated NIC)
|
|
config:
|
|
limits.cpu: "4"
|
|
limits.memory: 8GiB
|
|
devices:
|
|
eth0:
|
|
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
|
|
path: /
|