🔴 Boxes are NOT isolated from each other — sibling isolation does not hold (confirmed live) #16

Closed
opened 2026-07-13 21:12:35 +00:00 by dan-claude-bot · 4 comments
dan-claude-bot commented 2026-07-13 21:12:35 +00:00 (Migrated from github.com)

Split from #12 (part 2 of 3: #15 audit → isolation hardening#17 box + templates). Blocked by #15 — its results pin this issue's design points.

Boxes are already isolated from each other, but by accident

The question that prompted #12: multiple boxes share claudenet — can they reach each other?

By code reading: they cannot (the audit verifies it live). But for a reason nobody wrote down, and nothing tests. host/setup-host.sh:31 drops all egress to 10.0.0.0/8; the claudenet subnet is 10.87.0.0/24inside 10.0.0.0/8. So box A's packets to box B die on A's own NIC, and security.acls.default.ingress.action=drop (setup-host.sh:39) would drop them again at B. Belt-and-braces — and incidental. docs/claudebox-design.md:95-97 describes those drops as being about RFC1918 and the host's LAN; sibling boxes appear nowhere in the design, the code, or a test. Renumber the subnet to 192.168.50.0/24 for some perfectly good reason and box-to-box isolation silently evaporates — while every stated design goal still reads as satisfied. That is the bug: not today's behavior, but that today's behavior is not load-bearing on purpose.

The same shape of accident has a second instance: IPv6 is off (ipv6.address=none, setup-host.sh:24) and every ACL rule is IPv4-only. Airtight today; re-enable IPv6 and not one rule covers it.

Two gaps survive even with the drop working as intended:

  • DNS enumeration. The gateway carve-out (setup-host.sh:30) keeps dnsmasq on 10.87.0.1 serving DNS — and that dnsmasq holds records for every instance on the network. Box A can resolve box B's name and address: not connect, but enumerate (names, existence, count).
  • Layer 2. Incus ACLs are L3/L4; boxes share a bridge, ARP lives underneath. profiles/claude-dev.yaml:7-11 sets no security.mac_filtering / security.ipv4_filtering. A hostile box poisoning the gateway's ARP cannot MITM siblings (their replies die on its own ingress drop) but can blackhole them — a cross-box denial of service still breaks "isolated even between them".

Why not a network per box

  • Every Incus bridge spawns its own dnsmasq and needs its own subnet — allocating and reclaiming /24s out of 10.87.0.0/16 (256 of them, real bookkeeping, a leak every time an rm goes wrong).
  • host/claudebox-firewall.sh hardcodes NET=claudenet / GW=10.87.0.1; its UFW / nft / DOCKER-USER hole-punching would become per-network and re-run on every mint — today it is a one-shot boot unit.
  • It buys isolation we already have (audit A3).

The right forward-looking axis is named networks a box opts into, defaulting to the isolated one — not built here, just not foreclosed (and when it comes, it comes as a CLI flag, never a template key).

Proposed

  1. Make sibling isolation explicit — without hardcoding the subnet. A drop rule with 10.87.0.0/24 written into it recreates the original bug one renumbering later. Per the audit's B1: @internal as destination if bridge networks support it; else setup-host.sh derives the subnet at rule-creation time (masking the gateway CIDR that ipv4.address returns — 10.87.0.1/24, host bits included). Rule ordered after the gateway carve-out (order is load-bearing; the carve-out itself stays literal — if it drifts, DNS breaks loudly, the good failure mode). One comment stating intent: boxes must not reach boxes, independent of the RFC1918 drops.
  2. security.mac_filtering=true + security.ipv4_filtering=true on the profile NIC. Ends the L2 spoofing class. Caveat to document: pins each box to its own address; fine for every current workload including in-box Docker (NAT hides behind eth0), forecloses extra-MAC/IP-on-the-wire workloads. Existing boxes pick it up on profile re-apply but may need a restart — stated, not automated.
  3. dns.mode=none on claudenet. Closes the enumeration leak; instances keep upstream resolution (audit B3).
  4. IPv6-off stated as contract — a comment at ipv6.address=none and an assertion in the test.
  5. tests/isolation.sh — the repo's first test; the audit's section-A probes, mechanized:
claudebox new --name a && claudebox new --name b
IP_B=$(incus list ^b$ --format csv --columns 4 | tr -d '"' | tr ',' '\n' \
        | awk '/\(eth0\)$/ { print $1; exit }')
[ -n "$IP_B" ] || { echo "no eth0 address for b"; exit 1; }

claudebox exec a -- ping -c1 -W2 "$IP_B"    # MUST fail  (sibling isolation)
claudebox exec a -- getent hosts b          # MUST fail  (DNS enumeration — leaks today)
claudebox exec a -- getent hosts b.incus    # MUST fail  (same, via the default dns.domain)
claudebox exec a -- curl -sS --max-time 3 https://example.com >/dev/null  # MUST pass
incus network get claudenet ipv6.address | grep -qx none                  # MUST pass

Plain bash, set -euo pipefail, PASS/FAIL per line, nonzero exit on any MUST-fail that passed, trap cleanup of its two boxes. Not CI-wired (no Incus in CI); a header documents how to run it by hand.

  1. docs/claudebox-design.md — the isolation section gains the missing sentence: siblings mutually unreachable by design, DNS does not enumerate, IPv6-off and L2 filtering are contract, the test is the proof.

Acceptance

  • tests/isolation.sh in the repo and green on a live host (output pasted in the PR). The getent lines are the ones that currently fail to fail.
  • The sibling-drop rule contains no hardcoded subnet literal (@internal, or derived at creation).
  • Zero CLI behavior change — this touches no line of bin/claudebox.
  • Design doc states sibling isolation, DNS non-enumeration, IPv6-off, and L2 filtering as intent.

One PR, a morning's work once the audit is in. The box+templates issue then lands against a repo whose isolation is explicit and tested — the rename cannot silently regress what a test asserts.

Split from #12 (part 2 of 3: #15 audit → **isolation hardening** → #17 box + templates). **Blocked by #15** — its results pin this issue's design points. ## Boxes are already isolated from each other, but *by accident* The question that prompted #12: *multiple boxes share `claudenet` — can they reach each other?* By code reading: **they cannot** (the audit verifies it live). But for a reason nobody wrote down, and nothing tests. `host/setup-host.sh:31` drops all egress to `10.0.0.0/8`; the claudenet subnet is `10.87.0.0/24` — *inside* `10.0.0.0/8`. So box A's packets to box B die on A's own NIC, and `security.acls.default.ingress.action=drop` (`setup-host.sh:39`) would drop them again at B. Belt-and-braces — and **incidental**. `docs/claudebox-design.md:95-97` describes those drops as being about RFC1918 and the host's LAN; sibling boxes appear nowhere in the design, the code, or a test. **Renumber the subnet to `192.168.50.0/24` for some perfectly good reason and box-to-box isolation silently evaporates — while every stated design goal still reads as satisfied.** That is the bug: not today's behavior, but that today's behavior is not load-bearing *on purpose*. The same shape of accident has a second instance: **IPv6 is off** (`ipv6.address=none`, `setup-host.sh:24`) and every ACL rule is IPv4-only. Airtight today; re-enable IPv6 and not one rule covers it. Two gaps survive even with the drop working as intended: - **DNS enumeration.** The gateway carve-out (`setup-host.sh:30`) keeps dnsmasq on `10.87.0.1` serving DNS — and that dnsmasq holds records for every instance on the network. Box A can resolve box B's name and address: not connect, but *enumerate* (names, existence, count). - **Layer 2.** Incus ACLs are L3/L4; boxes share a bridge, ARP lives underneath. `profiles/claude-dev.yaml:7-11` sets no `security.mac_filtering` / `security.ipv4_filtering`. A hostile box poisoning the gateway's ARP cannot MITM siblings (their replies die on its own ingress drop) but *can* blackhole them — a cross-box denial of service still breaks "isolated even between them". ### Why not a network per box - Every Incus bridge spawns its own dnsmasq and needs its own subnet — allocating and reclaiming /24s out of `10.87.0.0/16` (256 of them, real bookkeeping, a leak every time an `rm` goes wrong). - `host/claudebox-firewall.sh` hardcodes `NET=claudenet` / `GW=10.87.0.1`; its UFW / nft / `DOCKER-USER` hole-punching would become per-network and re-run on every mint — today it is a one-shot boot unit. - It buys isolation we already have (audit A3). The right forward-looking axis is **named networks a box opts into, defaulting to the isolated one** — not built here, just not foreclosed (and when it comes, it comes as a CLI flag, never a template key). ## Proposed 1. **Make sibling isolation explicit — without hardcoding the subnet.** A drop rule with `10.87.0.0/24` written into it recreates the original bug one renumbering later. Per the audit's B1: `@internal` as destination if bridge networks support it; else `setup-host.sh` derives the subnet at rule-creation time (masking the gateway CIDR that `ipv4.address` returns — `10.87.0.1/24`, host bits included). Rule ordered *after* the gateway carve-out (order is load-bearing; the carve-out itself stays literal — if it drifts, DNS breaks loudly, the good failure mode). One comment stating intent: *boxes must not reach boxes, independent of the RFC1918 drops.* 2. **`security.mac_filtering=true` + `security.ipv4_filtering=true`** on the profile NIC. Ends the L2 spoofing class. Caveat to document: pins each box to its own address; fine for every current workload including in-box Docker (NAT hides behind eth0), forecloses extra-MAC/IP-on-the-wire workloads. Existing boxes pick it up on profile re-apply but may need a restart — stated, not automated. 3. **`dns.mode=none`** on `claudenet`. Closes the enumeration leak; instances keep upstream resolution (audit B3). 4. **IPv6-off stated as contract** — a comment at `ipv6.address=none` and an assertion in the test. 5. **`tests/isolation.sh`** — the repo's first test; the audit's section-A probes, mechanized: ```bash claudebox new --name a && claudebox new --name b IP_B=$(incus list ^b$ --format csv --columns 4 | tr -d '"' | tr ',' '\n' \ | awk '/\(eth0\)$/ { print $1; exit }') [ -n "$IP_B" ] || { echo "no eth0 address for b"; exit 1; } claudebox exec a -- ping -c1 -W2 "$IP_B" # MUST fail (sibling isolation) claudebox exec a -- getent hosts b # MUST fail (DNS enumeration — leaks today) claudebox exec a -- getent hosts b.incus # MUST fail (same, via the default dns.domain) claudebox exec a -- curl -sS --max-time 3 https://example.com >/dev/null # MUST pass incus network get claudenet ipv6.address | grep -qx none # MUST pass ``` Plain bash, `set -euo pipefail`, PASS/FAIL per line, nonzero exit on any MUST-fail that passed, `trap` cleanup of its two boxes. Not CI-wired (no Incus in CI); a header documents how to run it by hand. 6. **`docs/claudebox-design.md`** — the isolation section gains the missing sentence: siblings mutually unreachable by design, DNS does not enumerate, IPv6-off and L2 filtering are contract, the test is the proof. ## Acceptance - [ ] `tests/isolation.sh` in the repo and green on a live host (output pasted in the PR). The `getent` lines are the ones that currently fail to fail. - [ ] The sibling-drop rule contains no hardcoded subnet literal (`@internal`, or derived at creation). - [ ] Zero CLI behavior change — this touches no line of `bin/claudebox`. - [ ] Design doc states sibling isolation, DNS non-enumeration, IPv6-off, and L2 filtering as intent. One PR, a morning's work once the audit is in. The box+templates issue then lands against a repo whose isolation is explicit and tested — the rename cannot silently regress what a test asserts.
dan-claude-bot commented 2026-07-13 22:23:35 +00:00 (Migrated from github.com)

Two of this issue's four proposed changes are now settled by the live drill (#15, run 2) — design points, before any code:

  • dns.mode=none is vetoed as written. On a real bridge it didn't just stop sibling-record resolution — it broke all DNS in the box (curl: Resolving timed out to a public host). So proposal 3 can't be dns.mode=none alone; it needs a mechanism that drops instance records while keeping the upstream resolver. Options to evaluate: dns.mode=none plus an explicit resolver handed to the box (static /etc/resolv.conf or a dns.nameservers equivalent), or dropping only the enumeration path rather than the whole DNS mode. This is exactly the kind of thing the rehearsal exists to catch.
  • @internal is unsupported on bridge networksError: Unsupported nftables subject "@internal". So proposal 1 takes the fallback already written into it: derive the subnet in setup-host.sh by masking the gateway CIDR that incus network get claudenet ipv4.address returns (10.87.0.1/2410.87.0.0/24), so the drop rule and the network can't disagree.

Confirmed on the way: L2 filtering (security.mac_filtering + security.ipv4_filtering) leaves box networking intact (proposal 2 is safe); IPv6 is off; and the DNS-enumeration leak this issue targets is real and reproduced (a box resolves its sibling by name). Still pending run 3: A3, the sibling-reachability baseline itself.

**Two of this issue's four proposed changes are now settled by the live drill (#15, run 2) — design points, before any code:** - **`dns.mode=none` is vetoed as written.** On a real bridge it didn't just stop sibling-record resolution — it **broke all DNS in the box** (`curl: Resolving timed out` to a public host). So proposal 3 can't be `dns.mode=none` alone; it needs a mechanism that drops *instance* records while keeping the upstream resolver. Options to evaluate: `dns.mode=none` **plus** an explicit resolver handed to the box (static `/etc/resolv.conf` or a `dns.nameservers` equivalent), or dropping only the enumeration path rather than the whole DNS mode. This is exactly the kind of thing the rehearsal exists to catch. - **`@internal` is unsupported on bridge networks** — `Error: Unsupported nftables subject "@internal"`. So proposal 1 takes the fallback already written into it: **derive the subnet in `setup-host.sh`** by masking the gateway CIDR that `incus network get claudenet ipv4.address` returns (`10.87.0.1/24` → `10.87.0.0/24`), so the drop rule and the network can't disagree. Confirmed on the way: L2 filtering (`security.mac_filtering` + `security.ipv4_filtering`) leaves box networking **intact** (proposal 2 is safe); IPv6 is off; and the DNS-enumeration leak this issue targets is **real and reproduced** (a box resolves its sibling by name). Still pending run 3: A3, the sibling-reachability baseline itself.
dan-claude-bot commented 2026-07-13 22:41:20 +00:00 (Migrated from github.com)

⚠️ Retracting half of my previous comment — the dns.mode=none veto is not established.

Run 3 contradicted run 2 on exactly that point:

  • Run 2: B3 egress under dns.mode=none: BROKEN — design veto
  • Run 3: B3 egress under dns.mode=none: intact — safe to ship

Setting dns.mode restarts the network's dnsmasq, and both runs probed egress ~2s later — run 2 caught the restart window, run 3 didn't. So the "veto" was very likely an artifact of when the probe fired, not a property of the knob. PR #21 makes the probe distinguish a transient outage (recovers within 30s) from a real break, so the next run gives a verdict this issue can actually design on.

Unchanged and still solid: @internal is genuinely unsupported on bridge ACLs (Unsupported nftables subject) ⇒ proposal 1 derives the subnet by masking the gateway CIDR. L2 filtering leaves box networking intact ⇒ proposal 2 is safe. The DNS-enumeration leak is real and reproduced on every run ⇒ this issue's premise holds.

The lesson is one worth keeping in the issue itself: a design verdict from a single observation of a system with restart semantics is not a verdict. Whatever mechanism proposal 3 lands on, its test must tolerate the dnsmasq restart window rather than race it.

⚠️ **Retracting half of my previous comment — the `dns.mode=none` veto is not established.** Run 3 contradicted run 2 on exactly that point: - Run 2: `B3 egress under dns.mode=none: BROKEN — design veto` - Run 3: `B3 egress under dns.mode=none: intact — safe to ship` Setting `dns.mode` restarts the network's dnsmasq, and both runs probed egress ~2s later — run 2 caught the restart window, run 3 didn't. So the "veto" was very likely an artifact of *when* the probe fired, not a property of the knob. PR #21 makes the probe distinguish a transient outage (recovers within 30s) from a real break, so the next run gives a verdict this issue can actually design on. **Unchanged and still solid:** `@internal` is genuinely unsupported on bridge ACLs (`Unsupported nftables subject`) ⇒ proposal 1 derives the subnet by masking the gateway CIDR. L2 filtering leaves box networking intact ⇒ proposal 2 is safe. The DNS-enumeration leak is real and reproduced on every run ⇒ this issue's premise holds. The lesson is one worth keeping in the issue itself: **a design verdict from a single observation of a system with restart semantics is not a verdict.** Whatever mechanism proposal 3 lands on, its test must tolerate the dnsmasq restart window rather than race it.
dan-claude-bot commented 2026-07-14 01:25:06 +00:00 (Migrated from github.com)

🔴 The premise of this issue is wrong — and the truth is worse

The #15 audit ran on a live host. Sibling isolation does not hold.

probing archive (10.87.0.33) → peer (10.87.0.183), no listener
FAIL  BOX A REACHES BOX B — sibling isolation does NOT hold [tcp: refused]

refused means A's SYN arrived at B and B answered with a RST. Boxes reach each other, bidirectionally, on a stock host, today.

So this issue's framing must change:

Part 2 — boxes are already isolated from each other, but by accident
They cannot [reach each other]. But for a reason nobody wrote down.

Boxes are not isolated from each other at all. This issue is a FIX.

The code reading in #12 was plausible and wrong: yes, the ACL drops egress to 10.0.0.0/8, and yes 10.87.0.0/24 sits inside it — but two boxes on one bridge share an L2 segment, so their traffic is switched, not routed, and never meets the rule that would drop it. The "belt and braces" was neither.

The tool's stated contract is currently false. "A box reaches the public internet and nothing else" — it also reaches every other box on the host. Two agents, in two boxes, can talk to each other and neither the docs, the code, nor a test says otherwise.

The four proposals, now settled by live evidence

# Proposal Verdict
1 Explicit sibling drop MANDATORY — and it must be verified, not assumed. @internal is rejected on bridge ACLs (Unsupported nftables subject "@internal"), so derive the subnet in setup-host.sh (mask the gateway CIDR: ipv4.address returns 10.87.0.1/24, you want 10.87.0.0/24). ⚠️ But an egress ACL rule may not see intra-bridge traffic either — that is precisely the bug above. If it does not, this needs a bridge-family/ebtables rule, or security.acls applied at the NIC, or per-box networks after all. The acceptance test is that refused becomes timeout. Do not ship this on a code reading; that is what got us here.
2 security.mac_filtering + security.ipv4_filtering 🔴 ipv4_filtering VETOED as written — it breaks the box's networking. Measured on a baseline that had already passed the egress check, so this verdict is sound (an identical FAIL in an earlier run was a broken box lying, and is void). Symptom: box loses networking; dockerd is up but cannot pull or run a container — consistent with ipv4_filtering permitting only the instance's own address as source, which in-box Docker's 172.17.x NAT does not satisfy. Split it: mac_filtering alone is probably fine and still kills ARP spoofing; ipv4_filtering needs a way to accommodate the box's own container NAT, or it is out.
3 dns.mode=none VIABLE. Closes the enumeration leak (sibling names stop resolving) and public egress survives, immediately, with no outage window. ⚠️ I posted a design veto against this earlier on the strength of one run — that was wrong and is retracted; it was an artifact of probing during the dnsmasq restart. The DNS-enumeration leak itself is real and reproduced on every run: a box resolves its sibling by name and address.
4 IPv6-off as contract Already true (ipv6.address: none), just needs stating — every ACL rule is IPv4-only, so IPv6-on would be an uncovered path.
5 tests/isolation.sh The drill's phase C already is this test, and it is the thing that caught the bug. Lift it out of drill/drill.sh.
  • A box on a Tailscale host resolves tailnet names (#33) — the ACL blocks the 100.64.0.0/10 addresses but the gateway DNS carve-out forwards to MagicDNS. Same shape as the sibling leak: connection blocked, enumeration wide open. Whatever mechanism fixes proposal 3 should fix this too.
  • A network Incus reports as Created may have nothing serving it (#35) — worth knowing before you debug an isolation rule that "does nothing".

Priority

This is no longer hardening. It is a security fix against a false claim in the README, and it should be treated as such. #17 (the box rename) can wait behind it; this cannot.

# 🔴 The premise of this issue is wrong — and the truth is worse The [#15](https://github.com/heavy-duty/claudebox/issues/15) audit ran on a live host. **Sibling isolation does not hold.** ``` probing archive (10.87.0.33) → peer (10.87.0.183), no listener FAIL BOX A REACHES BOX B — sibling isolation does NOT hold [tcp: refused] ``` `refused` means A's SYN **arrived** at B and B answered with a RST. Boxes reach each other, bidirectionally, on a stock host, today. So this issue's framing must change: > ~~**Part 2 — boxes are already isolated from each other, but *by accident***~~ > ~~They cannot [reach each other]. But for a reason nobody wrote down.~~ > > **Boxes are not isolated from each other at all. This issue is a FIX.** The code reading in #12 was plausible and wrong: yes, the ACL drops egress to `10.0.0.0/8`, and yes `10.87.0.0/24` sits inside it — but **two boxes on one bridge share an L2 segment**, so their traffic is *switched*, not routed, and never meets the rule that would drop it. The "belt and braces" was neither. **The tool's stated contract is currently false.** *"A box reaches the public internet and nothing else"* — it also reaches every other box on the host. Two agents, in two boxes, can talk to each other and neither the docs, the code, nor a test says otherwise. ## The four proposals, now settled by live evidence | # | Proposal | Verdict | | --- | --- | --- | | 1 | Explicit sibling drop | **MANDATORY — and it must be verified, not assumed.** `@internal` is **rejected** on bridge ACLs (`Unsupported nftables subject "@internal"`), so derive the subnet in `setup-host.sh` (mask the gateway CIDR: `ipv4.address` returns `10.87.0.1/24`, you want `10.87.0.0/24`). ⚠️ **But an egress ACL rule may not see intra-bridge traffic either** — that is precisely the bug above. If it does not, this needs a bridge-family/ebtables rule, or `security.acls` applied at the NIC, or per-box networks after all. **The acceptance test is that `refused` becomes `timeout`.** Do not ship this on a code reading; that is what got us here. | | 2 | `security.mac_filtering` + `security.ipv4_filtering` | 🔴 **`ipv4_filtering` VETOED as written — it breaks the box's networking.** Measured on a baseline that had already passed the egress check, so this verdict is sound (an identical FAIL in an earlier run was a broken box lying, and is void). Symptom: box loses networking; `dockerd` is up but cannot pull or run a container — consistent with `ipv4_filtering` permitting only the instance's own address as source, which in-box Docker's `172.17.x` NAT does not satisfy. **Split it:** `mac_filtering` alone is probably fine and still kills ARP spoofing; `ipv4_filtering` needs a way to accommodate the box's own container NAT, or it is out. | | 3 | `dns.mode=none` | ✅ **VIABLE.** Closes the enumeration leak (sibling names stop resolving) **and public egress survives, immediately, with no outage window.** ⚠️ I posted a design veto against this earlier on the strength of one run — **that was wrong and is retracted**; it was an artifact of probing during the dnsmasq restart. The DNS-enumeration leak itself is **real and reproduced on every run**: a box resolves its sibling by name and address. | | 4 | IPv6-off as contract | ✅ Already true (`ipv6.address: none`), just needs stating — every ACL rule is IPv4-only, so IPv6-on would be an uncovered path. | | 5 | `tests/isolation.sh` | The drill's **phase C already is this test**, and it is the thing that caught the bug. Lift it out of `drill/drill.sh`. | ## Two related findings from the same audit - **A box on a Tailscale host resolves tailnet names** ([#33](https://github.com/heavy-duty/claudebox/issues/33)) — the ACL blocks the `100.64.0.0/10` addresses but the gateway DNS carve-out forwards to MagicDNS. Same shape as the sibling leak: *connection blocked, enumeration wide open.* Whatever mechanism fixes proposal 3 should fix this too. - **A network Incus reports as `Created` may have nothing serving it** ([#35](https://github.com/heavy-duty/claudebox/issues/35)) — worth knowing before you debug an isolation rule that "does nothing". ## Priority This is no longer hardening. **It is a security fix against a false claim in the README**, and it should be treated as such. #17 (the `box` rename) can wait behind it; this cannot.
dan-claude-bot commented 2026-07-14 12:10:25 +00:00 (Migrated from github.com)

Closing on run 11's evidence. The hardening this issue asked for is shipped and measured, not reasoned:

  • Sibling isolation is deliberate now: security.port_isolation: "true" in the profile (#38) + the nft bridge-family drop (#37, kept as the second belt), with the design written down in claudebox-firewall.sh's comment block — the L2 explanation this issue said existed nowhere.
  • Tested against the real stack: run 11 phase C — A3 sibling: BLOCKED — tcp dropped + no icmp reply (security.port_isolation), plus box→host, RFC1918, and host→box all dropped. The kernel's view (isolated on per tap) is read directly by the doctor since #39/#40.
  • DNS enumeration closed: dns.mode=none shipped; run 11 confirms a box cannot resolve its sibling's name, and public-name forwarding survives.
  • IPv6: off and now stated as a contract in setup-host.sh (the IPv4-only ACLs have no uncovered path) — run 11 asserts it.
  • The two design points measured along the way: security.ipv4_filtering VETOED (breaks the box — measured on a passing baseline, run 10), @internal unsupported on bridge ACLs.

The detour was the finding: the original 10.0.0.0/8 belt-and-braces reasoning was wrong (L2 frames never meet an L3 ACL — confirmed live in run 10 by a RST from a sibling), and the fix that closed it is the port-isolation flag this issue's audit trail produced. #17 is unblocked.

🤖 Generated with Claude Code

Closing on run 11's evidence. The hardening this issue asked for is shipped and measured, not reasoned: - **Sibling isolation is deliberate now**: `security.port_isolation: "true"` in the profile (#38) + the nft bridge-family drop (#37, kept as the second belt), with the design written down in `claudebox-firewall.sh`'s comment block — the L2 explanation this issue said existed nowhere. - **Tested against the real stack**: run 11 phase C — `A3 sibling: BLOCKED — tcp dropped + no icmp reply (security.port_isolation)`, plus box→host, RFC1918, and host→box all dropped. The kernel's view (`isolated on` per tap) is read directly by the doctor since #39/#40. - **DNS enumeration closed**: `dns.mode=none` shipped; run 11 confirms a box cannot resolve its sibling's name, and public-name forwarding survives. - **IPv6**: off and now stated as a contract in `setup-host.sh` (`the IPv4-only ACLs have no uncovered path`) — run 11 asserts it. - The two design points measured along the way: `security.ipv4_filtering` VETOED (breaks the box — measured on a passing baseline, run 10), `@internal` unsupported on bridge ACLs. The detour was the finding: the original 10.0.0.0/8 belt-and-braces reasoning was wrong (L2 frames never meet an L3 ACL — confirmed live in run 10 by a RST from a sibling), and the fix that closed it is the port-isolation flag this issue's audit trail produced. #17 is unblocked. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/box#16
No description provided.