diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index bf1bb32..c0072f9 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -327,11 +327,24 @@ fabrics). **dc1's leftover nginx also removed** the same day (purged nginx + nginx-common, `/etc/nginx` gone, nothing listening on :80, proxy still PASS on :3142; it had been a MIRROR prereq only -- dc0 keeps its nginx and is unaffected). - Also queued to this close and NOT yet done: the SEC-024 retention question on the two - `pre-*` state-surgery snapshots (irreversible, operator-deferred), the `set-interface-v4` - reload amendment (an unruled OPTION), and the GA-R6 close-out set itself (GA-R2 - consolidation, skill sweep + snapshot regeneration, GA-R7 memory review, final gauntlet, - operator-gated merge to `main` as a merge commit, branch retirement). + - **`set-interface-v4` RELOAD AMENDMENT RULED + SHIPPED 2026-07-27** (operator utterance + "a" to the three presented options; OPS under GA-R3, governed by D-113 -- no new + D-number). `--commit` now runs `configctl filter reload` and reads the automatic NAT + rules back via `pfctl -s nat`, so the appendix-A "edge passes no LAN traffic after v4 + addressing" defect cannot be reached through the normal addressing path. Prose-only + prevention was rejected on evidence: DoD item 8 existed and did NOT fire at dc1. + PLACEMENT IS THE SUBSTANCE: the reload runs over a FRESH connection to the post-move + address, NOT on the next line of the `interface reconfigure` heredoc -- re-addressing the + interface you arrived on drops the session DURING the reconfigure, so a reload there + would never execute in exactly the dc1 scenario that caused the defect. The NAT read-back + is REPORTED not gated (a first addressing may legitimately have no gateway yet); the hard + gate stays address-on-the-kernel. Harness **59/59** (was 53), cases 15-15f. No live edge + touched -- both DC edges are already addressed, so this affects the NEXT one. + **REMAINING for the Stage 4 close: the GA-R6 close-out set ONLY** -- GA-R2 consolidation, + skill sweep + dated snapshot regeneration, GA-R7 memory review, final gauntlet + repo-lint, + operator-gated merge of `dc-dc-stage4-phase3-maas-deploy` to `main` as a MERGE commit (not + squash), branch retirement. Every substantive in-stage item is now closed or split to a + gate row (G17). - Position inside Stage 3: deploy step A EXECUTED 2026-07-19 (6/0/6 exact; convergence zero -- `docs/audit/outer-plan-20260719-postA-converged.txt`). **Deploy step B diff --git a/docs/changelog-20260727-creds-consolidation.md b/docs/changelog-20260727-creds-consolidation.md index 98c8fdb..eafa31a 100644 --- a/docs/changelog-20260727-creds-consolidation.md +++ b/docs/changelog-20260727-creds-consolidation.md @@ -402,6 +402,52 @@ preflight P5 is the detection. Recorded in both the SEC-024 disposition cell and CURRENT-STATE. Open SEC count unchanged at 21. +## 12. set-interface-v4 reload amendment RULED + implemented (the last queued stage-close item) + +**RULING (GA-R5), 2026-07-27.** Question as presented: (a) add `configctl filter reload` to the +script's `--commit` path; (b) leave the script and rely on DC standup DoD item 8; (c) add it +behind an opt-in flag. Operator answer, exact utterance: **"a"**. OPS under GA-R3 -- a script fix +completing an already-documented operation, governed by D-113; no new D-number. + +THE DEFECT (appendix-A, measured on the fresh dc1 edge 2026-07-23): `--commit` ran `configctl +interface reconfigure`, which applies the address to the kernel but does NOT regenerate the pf +ruleset, so OPNsense never creates its automatic outbound-NAT rules for the (re-)addressed LAN. +Forwarded LAN packets leave the WAN un-NAT'd and replies never return -- the edge passes NO LAN +traffic while looking perfectly configured. Self-masking: any later plugin install or config +apply reloads the filter as a side effect, so dc0's 07-20 qga work hid it there until dc1, which +had no later work, exposed it at the mirror build. + +Option (b) was rejected on evidence, not taste: DoD item 8 already existed and did not fire at +dc1, and prose-only controls have a measured failure record here (D-137's own enforcement sat as +prose at `phase-3:498` and missed BOTH DC standups). + +**THE PLACEMENT IS THE SUBSTANCE OF THE FIX, and my first attempt got it wrong.** I initially put +the reload on the next line of the `interface reconfigure` heredoc. That would have been a fix +that fixes nothing exactly when it matters: re-addressing the interface you arrived on drops the +session DURING the reconfigure, so a following command in that stream never executes on the edge +-- and that drop case IS this incident's scenario (dc1's LAN moved 192.168.1.1 -> 10.12.64.1 over +the LAN itself). Caught before shipping. As delivered, the reload runs over a FRESH connection to +`PROBE_HOST`, the address the edge answers on after the move. + +Also added a `pfctl -s nat` read-back with the `proto carp` boilerplate filtered out -- that +boilerplate-only state is precisely appendix-A's broken-state signature. It is REPORTED, not +gated: at an edge's very first addressing there may legitimately be no gateway and so no NAT to +generate, and hard-failing would block a correct run. The hard gate stays address-on-the-kernel. + +Harness **59/59** (was 53). Cases 15-15f: the reload exists; it is NOT inside the reconfigure +heredoc; it targets `PROBE_HOST`; the NAT read-back exists with carp excluded; the read-back does +not hard-exit; and appendix-A no longer calls the amendment an unruled queued option. **Case 15f +caught a real inconsistency** -- appendix-A still said "not yet ruled" -- and case 15e caught a +bug in my own first draft of it, which scanned to EOF and so failed the script for having the +very hard gate the case exists to preserve. + +appendix-A's entry rewritten: the FIX stays (for an edge already in this state), plus a +PREVENTED-IN-THE-TOOL block recording the placement detail and why prose prevention was rejected. + +Revert: `git revert` the `scripts/opnsense-set-interface-v4.sh` + +`tests/opnsense-set-interface-v4/run-tests.sh` + appendix-A changes. No live edge was touched -- +both DC edges are already addressed, so this affects only the NEXT edge addressing. + --- ## Findings LOGGED, not actioned (hard rule 1) diff --git a/runbooks/appendix-A-troubleshooting.md b/runbooks/appendix-A-troubleshooting.md index ee3e02f..5030c17 100644 --- a/runbooks/appendix-A-troubleshooting.md +++ b/runbooks/appendix-A-troubleshooting.md @@ -830,6 +830,25 @@ FIX: On the edge: `configctl filter reload`, then re-test from a LAN client (rack): ping internet IP 0% loss, `curl -sI http://1.1.1.1/` - -> HTTP 301. Standing prevention: DC standup DoD item 8 - (dc-dc-phase2 runbook); a set-interface-v4 amendment (reload in - the script itself) is a QUEUED stage-close option, not yet ruled. + -> HTTP 301. + +PREVENTED IN THE TOOL (2026-07-27, operator-ruled): `opnsense-set-interface-v4 + --commit` now runs `configctl filter reload` itself and reads the + automatic NAT rules back with `pfctl -s nat`. So a fresh edge cannot + reach this state through the normal addressing path. Prose-only + prevention was rejected: DC standup DoD item 8 existed and did NOT + fire at dc1, and this repo has a measured record of prose controls + not firing (D-137's own wiring sat as prose at phase-3:498 and + missed BOTH DC standups). + PLACEMENT DETAIL, because it is the easy thing to get wrong: the + reload runs over a FRESH connection to the post-move address, NOT on + the next line of the `interface reconfigure` heredoc. Re-addressing + the interface you arrived on drops the session DURING the + reconfigure, so a following command in that stream never executes -- + which is exactly this incident's own scenario (dc1's LAN moved + 192.168.1.1 -> 10.12.64.1 over the LAN itself). A reload on that + next line would look like a fix while fixing nothing precisely when + it matters. Harness cases 15-15f encode all of this. + The NAT read-back is REPORTED, not gated: at an edge's very first + addressing there may legitimately be no gateway yet and therefore no + NAT to generate. The hard gate remains address-on-the-kernel. diff --git a/scripts/opnsense-set-interface-v4.sh b/scripts/opnsense-set-interface-v4.sh index c51d603..5743841 100755 --- a/scripts/opnsense-set-interface-v4.sh +++ b/scripts/opnsense-set-interface-v4.sh @@ -126,6 +126,29 @@ echo "=== APPLY (configctl -- saving config does NOT reconfigure the kernel) ===" # Re-addressing the interface you are connected THROUGH drops this SSH session by # design; the reconfigure still completes on the edge. Do not treat that as a failure. +# +# `filter reload` ADDED 2026-07-27 (operator-ruled; appendix-A "edge passes no LAN +# traffic after v4 addressing"). MEASURED on the fresh dc1 edge 2026-07-23: `interface +# reconfigure` applies the address to the KERNEL but does NOT regenerate the pf ruleset, +# so OPNsense never creates its automatic outbound-NAT rules for the (re-)addressed LAN. +# Forwarded LAN packets then leave the WAN un-NAT'd and replies never return -- the edge +# passes NO LAN traffic while looking perfectly configured. Without this line the +# addressing operation is INCOMPLETE, not merely unpolished, which is why the reload +# belongs here rather than in a runbook step: DoD prose did not fire at dc1, and this +# repo has a measured record of prose-only controls not firing (D-137's own wiring sat as +# prose at phase-3:498 and missed BOTH DC standups). +# +# The defect is SILENT and self-masking: any later plugin install or config apply reloads +# the filter as a side effect, so dc0's 2026-07-20 qga/plugin work hid it there until dc1 +# -- which had no later work -- exposed it at the mirror build. +# +# IT IS DELIBERATELY *NOT* ON THE NEXT LINE OF THIS HEREDOC. Re-addressing the interface +# you arrived on drops the session DURING `interface reconfigure`, so a following command +# in the same stream would never execute on the edge -- and that drop case is precisely +# the dc1 scenario that produced the defect (the LAN was moved 192.168.1.1 -> 10.12.64.1 +# over the LAN itself). Putting the reload here would have looked like a fix while fixing +# nothing exactly when it matters. It runs below instead, over a FRESH connection to +# whichever address the edge now answers on. "${SSH[@]}" "root@${EDGE}" sh -s < inet from +# (:network) ...`; the broken state shows ONLY the `no nat/rdr proto carp` boilerplate. +# This is REPORTED, not gated: at the very first addressing of an edge there may legitimately +# be no gateway yet and so no NAT to generate, and a hard failure here would block a correct +# run. The script's hard gate stays the address-on-the-kernel check below -- a self-report is +# never evidence, but neither is an assertion the operation cannot yet satisfy. +NATOUT="$("${SSH[@]}" "root@${PROBE_HOST}" sh -s <<'EOF' 2>/dev/null || true +pfctl -s nat 2>/dev/null | grep -v 'proto carp' | head -5 +EOF +)" +if [ -n "$NATOUT" ]; then + printf '%s\n' "$NATOUT" | sed 's/^/ /' +else + echo " WARN: no automatic outbound NAT rules beyond the carp boilerplate." + echo " If this edge is meant to NAT a LAN, that is the appendix-A defect --" + echo " verify with: pfctl -s nat and re-test from a LAN client (0% loss, curl 301)." +fi OUT="$("${SSH[@]}" "root@${PROBE_HOST}" sh -s </dev/null || true ifconfig $DEV inet 2>/dev/null | grep 'inet ' || true EOF diff --git a/tests/opnsense-set-interface-v4/run-tests.sh b/tests/opnsense-set-interface-v4/run-tests.sh index 64133d6..fe27960 100755 --- a/tests/opnsense-set-interface-v4/run-tests.sh +++ b/tests/opnsense-set-interface-v4/run-tests.sh @@ -124,6 +124,54 @@ # losing that warning is how a future operator strands themselves. grep -qi 'WAN first' "$SH" && ok || bad "the WAN-before-LAN ordering warning is gone" +# 15. THE PF FILTER RELOAD (added 2026-07-27, operator-ruled). MEASURED on the fresh dc1 +# edge 2026-07-23: `interface reconfigure` applies the address to the kernel but does +# NOT regenerate the pf ruleset, so the automatic outbound-NAT rules for the +# (re-)addressed LAN are never created and the edge passes NO LAN traffic while looking +# correctly configured. Without the reload the operation is INCOMPLETE. +grep -q 'configctl filter reload' "$SH" \ + && ok || bad "the pf filter reload is gone -- the edge will pass no LAN traffic after addressing (appendix-A)" + +# 15b. THE PLACEMENT IS THE WHOLE POINT, and it is the easy thing to get wrong. The reload +# must NOT sit in the same heredoc as `interface reconfigure`: re-addressing the +# interface you arrived on drops the session DURING the reconfigure, so a following +# command in that stream never executes on the edge -- and that drop case is exactly +# the dc1 scenario that produced the defect. Assert the reconfigure heredoc contains +# ONLY the reconfigure. +recfg_block="$(awk '/^configctl interface reconfigure/{print; getline; print}' "$SH")" +printf '%s' "$recfg_block" | grep -q 'filter reload' \ + && bad "the filter reload is inside the reconfigure heredoc -- it will NOT run when the session drops (the dc1 case)" \ + || ok + +# 15c. ...and it must run against PROBE_HOST, the address the edge answers on AFTER the +# move, over a fresh connection -- not against the pre-move $EDGE. +awk '/REGENERATE THE PF RULESET/,/filter reload/' "$SH" | grep -q 'PROBE_HOST' \ + && ok || bad "the filter reload does not target PROBE_HOST -- it would use the stale pre-move address" + +# 15d. The reload is reported-then-verified: the NAT rules get read back, and the carp +# boilerplate is excluded because that is the exact broken-state signature appendix-A +# records (only `no nat/rdr proto carp` present = no automatic NAT generated). +grep -q 'pfctl -s nat' "$SH" && grep -q 'proto carp' "$SH" \ + && ok || bad "the NAT read-back (pfctl -s nat, carp boilerplate excluded) is gone" + +# 15e. That read-back must stay REPORTED, not gated: at an edge's very first addressing +# there may legitimately be no gateway and so no NAT to generate, and hard-failing +# would block a correct run. The address-on-the-kernel check remains the only hard gate. +# Scoped to the NATOUT branch itself -- an earlier draft of this case scanned to EOF and +# caught the ADDRESS gate's own `exit 2`, i.e. it failed the script for having the very +# hard gate it is supposed to keep. +awk '/^NATOUT=/,/^fi$/' "$SH" | grep -qE '(^|[[:space:]])exit [0-9]' \ + && bad "the NAT read-back hard-exits -- a first-addressing run with no gateway yet would fail wrongly" \ + || ok + +# 15f. The appendix-A entry must stay pointed at the script amendment rather than still +# calling it a queued option, or the runbook and the tool disagree about the control. +APX="$(cd "$(dirname "$SH")/.." && pwd)/runbooks/appendix-A-troubleshooting.md" +if [ -f "$APX" ]; then + grep -q 'not yet ruled' "$APX" \ + && bad "appendix-A still calls the set-interface-v4 reload a QUEUED unruled option" || ok +fi + echo total=$((pass+fail)) if [ "$fail" -eq 0 ]; then echo "opnsense-set-interface-v4: $pass/$total PASS"; exit 0; fi