diff --git a/opentofu/modules/opnsense-edge/main.tf b/opentofu/modules/opnsense-edge/main.tf index bf3e8c0..965db44 100644 --- a/opentofu/modules/opnsense-edge/main.tf +++ b/opentofu/modules/opnsense-edge/main.tf @@ -206,28 +206,32 @@ # OPNsense nano images are SERIAL-console only -- the image ships /boot.config with # `-S115200 -h`, so its loader expects a serial device. # - # `unix` socket + `log`, NOT a plain `file` source. The two roles are different and we - # need BOTH: - # source.unix -> an INTERACTIVE, BIDIRECTIONAL console. Required by D-112(c): the - # bootstrap is a one-time console login to enable SSH. A `file` source is WRITE-ONLY - # capture -- it cannot be typed into, so it cannot bootstrap anything. A unix socket - # is driven non-interactively with `socat - UNIX-CONNECT:`, unlike a `pty`, - # which needs a controlling TTY and is therefore awkward to script. - # log -> keeps the full boot capture we had before. Output emitted BEFORE any - # socket client connects would otherwise be lost; `log` records everything from - # power-on regardless. This capture is what made the 2026-07-12 boot bugs legible at - # all -- do not drop it. + # `pty` source + `log` file. Both roles are needed and they are DIFFERENT: + # source.pty -> an INTERACTIVE, BIDIRECTIONAL console, reached with `virsh console`. + # D-112(c) needs this: the one-time bootstrap is a console login. A `file` source is + # WRITE-ONLY capture and cannot be typed into, so it can never bootstrap anything. + # log -> preserves the full boot capture. `virsh console` only shows output from + # the moment you attach, so without this, everything emitted before attach is lost. + # That capture is what made both 2026-07-12 boot bugs legible; do not drop it. + # + # PTY, not a unix socket -- MEASURED, not preferred. A unix socket looks more scriptable + # (socat, no TTY), but `virsh console` REFUSES it: + # error: internal error: character device serial0 is not using a PTY + # and going direct to the socket is not an option either: libvirt creates it + # libvirt-qemu:kvm, and the staging dir's default ACL stamps a named-user entry that + # outranks group perms, so we get r-- and cannot write. Going through libvirtd (which + # runs as root) via `virsh console` needs no extra privilege -- membership of the + # `libvirt` group is enough. PTY satisfies both constraints; unix satisfies neither. + # pexpect drives `virsh console` fine, which is what the bootstrap uses. # # (DOCFIX-189 correction, for the record: adding a console in DOCFIX-187 did not CHANGE # the original triple-fault, it only revealed it. The domain faulted with and without.) + # NOTE: `source` is deliberately OMITTED. The provider's `source.pty` requires a `path`, + # but a pty's path is ALLOCATED BY LIBVIRT at start time (/dev/pts/N) and cannot be known + # in advance -- so it is unusable here. Omitting source yields libvirt's default + # ``, which is exactly what we want. serials = [ { - source = { - unix = { - mode = "bind" - path = "/var/lib/libvirt/vr1/staging/${var.vm_name}-console.sock" - } - } log = { file = "/var/lib/libvirt/vr1/staging/${var.vm_name}-serial.log", append = "on" } target = { port = "0" } } diff --git a/opentofu/templates/opnsense-config.xml.tmpl b/opentofu/templates/opnsense-config.xml.tmpl index a21498c..a0047d6 100644 --- a/opentofu/templates/opnsense-config.xml.tmpl +++ b/opentofu/templates/opnsense-config.xml.tmpl @@ -37,6 +37,16 @@ yes 1 + + serial + 115200 1 monthly diff --git a/tests/opnsense-render-config/run-tests.sh b/tests/opnsense-render-config/run-tests.sh index c7e5b02..81492bc 100644 --- a/tests/opnsense-render-config/run-tests.sh +++ b/tests/opnsense-render-config/run-tests.sh @@ -126,5 +126,23 @@ unset ROOT_AUTHORIZED_KEYS run_fail 1 "T8 missing ROOT_AUTHORIZED_KEYS FAILS (rc 1) -- no silent lockout" "$TMP/t8.xml" +# T9: the edge is a VM with NO VIDEO DEVICE, so the SERIAL console is its only break-glass +# path. A config that does not select the serial console silences it permanently after the +# first reboot (measured 2026-07-12: console went dead, box reachable only because sshd was +# on). Assert the rendered config keeps serial alive. +base_env +run_ok "T9 renders" "$TMP/t9.xml" +if grep -q "serial" "$TMP/t9.xml" && grep -q "115200" "$TMP/t9.xml"; then + echo " PASS T9b serial console retained (break-glass survives; no video device exists)"; PASS=$((PASS+1)) +else + echo " FAIL T9b rendered config does not select the serial console -- console would go dead"; FAIL=$((FAIL+1)) +fi +# There is no `enableserial` in OPNsense (that is a pfSense key). Guard against re-adding it. +if ! grep -q " (pfSense-ism, not an OPNsense key)"; PASS=$((PASS+1)) +else + echo " FAIL T9c present -- not an OPNsense key"; FAIL=$((FAIL+1)) +fi + echo; echo "RESULT: PASS=$PASS FAIL=$FAIL" [[ "$FAIL" -eq 0 ]] && { echo "ALL PASS"; exit 0; } || exit 1