box-firewall: converge the UFW carve-out off the live bridge; fail closed at boot (#86 follow-up) #89
No reviewers
Labels
No labels
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-triage
ready
release
scope:cli
scope:drill
scope:host
scope:installer
scope:templates
scope:tiers
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/box#89
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/ufw-subnet-converge"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Follow-up to #86, closing the two non-blocking concerns from
claude-bot-andresmgsl's final review there — the same class of bug #86 fixed for the ACL, left unfixed in the UFW/boot path.The two review items
This PR does both halves of item 1 (converge and doctor visibility), since once the UFW allows converge, the old create-once guard's failure would otherwise merely move to whatever state the converge can't reach.
What changed
host/box-firewall.sh— the UFW carve-out converges off the live bridge (ca1a759). TheDENY on boxnetcreate-once gate is gone. On every run: DNS allows onboxnetaimed at anything but the live bridge address are deleted (parsed offufw status, tcp+udp), then the live rule set is ensured.ufwskips rules that already exist, so a fresh host gets the identical five historical rules, and a re-run/agreeing host is a no-op (a few "Skipping adding existing rule" lines aside). This mirrors #86's ACL fix: full-state converge, not create-once.host/box-firewall.sh— the boot fallback fails closed (ca1a759).GW=10.88.0.1is deleted. If the bridge has no address when the script runs (the boot window), the UFW gateway-dependent block is skipped with a stderr note — no carve-out rather than a wrong one. Why fail-closed over the alternatives:iifname boxnet udp dport { 53, 67 }), as are the bridge table, expose-snat, and DOCKER-USER rules. They all still apply in the window.10.88.0.1at boot would have deleted aBOX_SUBNEThost's correct allows and installed wrong ones — strictly worse. Fail-closed is the only semantic under which converge is safe in the window.After=incus.service, and incus addresses its networks asynchronously after that. The unit is unchanged.drill/doctor.sh— the blind spot closes (fbad141). New pure functionufw_dns_findings(thegw_squat_signatureseam: text in, findings out, silence is agreement) readsufw statusitself and flags: a DNS allow that does not matchboxnet's live gateway; our deny present with no DNS allow at all; stale allows left beside a live one. A UFW hostbox-firewallnever touched has nothing to judge and stays clean, as do no-UFW hosts (interface-scoped nft, no gateway address to go stale) and fresh hosts (no bridge to compare against). Wired into the admin Firewall section; the fix line points at the now-convergingsudo /usr/local/sbin/box-firewall.Verification
test/cli.sh: 297 passed, 0 failed (was 265 on main; +32). New driven coverage (ce9ec68), through the real scripts under shims (a fakeufwserving canned status tables + logging mutations; fakenft/sysctl/iptables; the existing shimipfor the live-bridge read):ufw_dns_findingsdriven: agreement silent; mismatch flagged naming the stale address; deny-with-no-allow flagged; untouched UFW host clean; stale-beside-live named; other interfaces ignored — plus wiring grep-guardstest/labels-reconcile.sh: 19 passed, 0 failedshellcheck -xoverbin/* **/*.sh(CI's invocation): cleandrill/doctor.shsmoke-run read-only on a non-UFW host: the new block is inert there (by design)Not verified live: a real UFW host (this box has no UFW; the nested stack here must not be mutated). The shim tables are modeled on real
ufw statusoutput; CI'srehearsaljob (no UFW on the runner either) exercises the non-UFW path end to end.🤖 Generated with Claude Code
Verdict: Approve — I agree with this as-is.
Closes both non-blocking items from #86's final review on the UFW path: create-once → full-state converge for the DNS allows, and fail-closed instead of
GW=10.88.0.1when the bridge is unaddressed. Doctor'sufw_dns_findingscloses the ACL-only blind spot with the same pure-function seam asgw_squat_signature.What lands cleanly
1. Converge, not create-once (
host/box-firewall.sh)Stale
53/{tcp,udp}allows onboxnetthat do not match the live gateway are deleted, then the live set is ensured. Mirrors #86's ACL lesson. Fresh host still gets the same five historical mutations; agreeing host is a no-op for deletes (ufw skips re-adds of existing rules).2. Fail-closed boot window
Empty
GWskips the whole UFW gateway block with a stderr note — no guessed subnet. Correct under converge: a wrong fallback would now delete good rules. UFW rules persist across boots; nft path stays interface-scoped and still applies.3. Doctor visibility (
ufw_dns_findings)Text in → findings out; silence is agreement. Flags wrong gateway, deny-with-no-allow, and stale-beside-live; untouched/no-UFW hosts stay clean. Wiring fix points at the converging
box-firewall.4. Tests
Shim
ufw+ canned status tables drive remapped / agreeing / fresh / boot / no-UFW paths end-to-end, plus extractedufw_dns_findingscases. Load-bearing, not decorative.checkgreen.No blockers. No material nits worth holding the PR.
✅ Reviewed — I agree with all of this, no concerns.
Clean close of both non-blocking items from my #86 final review, on the UFW path. I re-derived the load-bearing logic rather than trust the tests, and it holds:
host/box-firewall.sh:35) — the$2 ~ /^53\// && $3=="on" && $4==net && $1!=gwparse correctly picks stale gateway allows offufw status; ran it against the10.88-carve-out table with a live10.89gateway and it yields exactly10.88.0.1to delete, theninsert 1puts the live allows above the deny. The old create-once gate that pinned every host to the first run's gateway is gone. This is the ACL lesson from #86 applied where it was still missing.host/box-firewall.sh:23) — droppingGW=10.88.0.1is the right call, and your reasoning that converge made the guess actively dangerous (it would now delete aBOX_SUBNEThost's correct allows) is the decisive argument. Nothing else in the script needs the gateway — the nft carve-out, bridge table, and expose-snat are all interface-scoped and still apply in the window. Confirmed.drill/doctor.sh:73ufw_dns_findings) — the ACL-only blind spot is what made item 1 invisible; reading UFW's own table closes it. The pure text→findings seam is right, and the three branches (wrong-gateway / deny-with-no-allow / stale-beside-live) are mutually sensible.ufw_gwis sourced fromincus network get boxnet ipv4.address, matching the existing ACL check atdrill/doctor.sh:345— internally consistent, and in steady state it agrees with box-firewall's liveip addrread since incus manages the bridge.test/cli.shlocally: 297 passed, 0 failed, all 32 new cases green. The shim-driven coverage exercises the real scripts (remapped / agreeing / fresh / boot / no-UFW), not stubs. I also checked the parse against(v6)companion lines and other-interface rules — no false positives.One honest scope note (not a blocker, and you already flagged it): the canned tables assume the real
ufw statuscolumn shape. The parse is robust to what varies — it ignores the Action column, soALLOWvsALLOW INand v6 companions don't matter — but like you, I have no live UFW host here to confirm the<dest> <port>/<proto> on <iface>layout end-to-end. Worth a one-time eyeball on a real UFW host at some point; the design degrades safely (fail-closed / silence-is-agreement) if the format ever surprises us.Both items resolved. No further concerns.
— automated review by
claude-bot-andresmgsl· heavy-duty-review-botVerdict: I agree with everything and have no additional feedback.
The UFW rules now converge against the live bridge without guessing during the unaddressed boot window, and the doctor independently detects stale or missing DNS carve-outs. The implementation, coverage, and current checks are all sound.
Round 1: unanimous (codex ✅ / grok ✅ / claude ✅-agree — both #86 follow-up items confirmed resolved, no changes requested). Claude's scope note about eyeballing a real
ufw statuslayout once is acknowledged in the PR body; the design fails closed if the format surprises. Requesting @danmt for final review.Clarification for the record (asked off-thread): this PR does not restrict nested box. The refusal of a claimed subnet shipped in #86; the sanctioned nested path (
BOX_SUBNET=10.89.0.0/24 box setup-host) keeps working, and this PR is what makes the UFW rules and doctor actually FOLLOW such a remap instead of keeping stale10.88rules — it strengthens the nested story. The bigger ergonomics ask — setup-host auto-picking a free subnet when the default is claimed, so nested drills/rehearsals need zero flags — is coming as its own PR on top.