| 2026-07-13 |

SAFETY SWEEP: mark the config.xml push path as a LIVE HAZARD (no behaviour change)
...
After D-113(a2) was proven (Office1's config is now API-managed), every instruction in the repo
that still said "render a config.xml and push it to the edge" became ACTIVELY DANGEROUS: a full
config.xml push replaces /conf/config.xml wholesale and drops ~667 migration-populated elements
(measured), including the only 2 firewall pass rules on the box. Following the old runbook steps
against the live edge would clobber it.
Warnings and headers only. No behaviour changes, no live system touched.
Marked (5 files):
runbooks/dc-dc-phase1-office1-standup.md DANGER banner; the config.xml render + config-ISO
sub-steps removed as RUNNABLE instructions (they
would clobber the live edge). Image-prep retained;
history preserved in git + the build changelog.
runbooks/dc-dc-phase2-tofu-dc-substrate.md STOP banner on Step 4.
scripts/opnsense-render-config.sh DANGER header at point-of-use. NOT dead: under
D-113(a2) it is to be reduced to a MINIMAL bootstrap
render. Until then, safe only for a brand-new,
not-yet-booted edge.
scripts/opnsense-build-config-iso.sh RETIRED header quoting the upstream source proving
the importer can never fire on a nano image.
docs/dc-dc-deployment-workflow.md STALE-CONTENT warning atop the tooling gap register.
THE SWEEP ALSO CAUGHT A PRE-EXISTING LANDMINE, unrelated to today's work: dc-dc-phase2 Step 4
still instructs building a CONFIG ISO for DC1's edge -- but D-112 established that ISO can never
be read (opnsense-importer probes for a read-only root; on a pre-installed nano the root is
writable and a factory /conf/config.xml already exists, so it bootstrap_and_exit 0's without
enumerating a single device). That runbook has been telling anyone who follows it to build an
inert artifact and then wonder why the edge came up on factory defaults -- which is EXACTLY the
day lost on 2026-07-12. It was never corrected at the source. Now it is. Its WAN_IF/LAN_IF
"chicken-and-egg" discussion is moot for the same reason: that problem only exists if you try to
seed a full config before first boot, and D-112(c) measures vtnetN AFTER boot.
Changelogs deliberately NOT touched -- they are history, not instructions. Marking history is
noise; marking instructions is safety.
Verification: opnsense-render-config 24 PASS, opnsense-build-config-iso 2 PASS, opnsense-api
21 PASS; repo-lint 0 fail.
STILL OPEN (the actual D-113(a2) migration): reduce the template to a minimal bootstrap, retire
the config-ISO path and the opnsense-edge module's config_seed/cdrom wiring, and rewrite Stage 3's
edge steps around the API. This sweep makes the repo SAFE, not FINISHED.
Revert: git revert this commit (restores the previous, dangerous instructions verbatim).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6Fzre1CxCY8tzwFCudu57
|
| 2026-07-12 |

DOCFIX-193: LAN DHCP was never implemented -- add Kea to the edge config template
...
The operator's goal for the Office1 edge was "router + DHCP for office1-local". The
router half works; the DHCP half was NEVER BUILT. The template had no DHCP section at
all -- no <dhcpd>, no <Kea> -- and nothing served DHCP (measured on the live edge: no
kea/dhcpd/dnsmasq process, nothing on udp/67). The ledger's claim of "DHCP .100-.199"
was false; corrected in place.
OPNsense 26.1 uses KEA. ISC dhcpd is gone, so an old-style <dhcpd> block would have been
inert -- which is the trap this could easily have fallen into.
The Kea shape is NOT invented: it mirrors what OPNsense itself PERSISTS on the live 26.1
box (read back from its config.xml, which its own migrations had populated with an
enabled=0 skeleton), and the field names match the upstream model
src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv4.xml.
Render script now derives and VALIDATES, rather than trusting inputs:
- LAN_NETWORK_CIDR derived from LAN_IPADDR/LAN_SUBNET_BITS (the subnet must be the
NETWORK, not the host address -- 10.12.8.1/22 -> 10.12.8.0/22).
- subnet4 uuid is a DETERMINISTIC uuid5 of the LAN CIDR, so re-rendering is idempotent
and does not churn the config.
- DHCP_POOL_START/END are REQUIRED and validated: a pool outside the LAN subnet, a pool
that swallows the router's own LAN address, or reversed bounds all FAIL LOUD. Each of
those is XML-valid and would serve nobody (or fight the router) -- exactly the class of
silent misconfiguration this session has been finding all day.
Harness 15 -> 24 PASS (T10-T13, incl. three negative cases). Gauntlet ALL GREEN (52);
repo-lint 0 fail.
NOT YET APPLIED to the live edge -- that is a separate gated mutation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y117t1F525ba9r1vXSBETk
|

DOCFIX-191: the rendered edge config would have LOCKED MANAGEMENT OUT (no sshd, no key)
...
Found while building the D-112(c) bootstrap. The config template had:
<ssh><group>admins</group></ssh>
and no authorized key on root. That is OPNsense's DISABLED-sshd shape. So the config
we were about to push would have left the edge with sshd off and no key installed --
reachable only from the serial console. Under D-112(c), SSH *is* the management path,
so this is a defect in the config itself, independent of how it gets delivered. Had the
Importer worked back in July, we would have shipped an unmanageable edge and blamed the
network.
Element names are VERIFIED against upstream, not guessed:
- src/www/system_advanced_admin.php: `enabled` is the literal STRING "enabled";
`permitrootlogin` / `passwordauth` are PRESENCE-checked (isset), so omitting
passwordauth leaves password auth OFF.
- src/etc/inc/auth.inc: `$keys = base64_decode($user['authorizedkeys']);` -- the
<user><authorizedkeys> value is BASE64 and is written to ~/.ssh/authorized_keys.
Changes:
- template: <ssh> gains <enabled>enabled</enabled> + <permitrootlogin>; root <user>
gains <authorizedkeys>{{ROOT_AUTHORIZED_KEYS_B64}}</authorizedkeys>.
- render script: ROOT_AUTHORIZED_KEYS is now REQUIRED (no default -- an edge rendered
without a key is a lockout waiting to happen). PUBLIC key material only; the private
key is never read by this repo. Base64 is computed AFTER the req() loop so a missing
key fails with the friendly message, not a `set -u` crash.
- Key-only auth is deliberate; the serial console stays the break-glass fallback.
Harness 9 -> 12 PASS. T7b asserts sshd is actually enabled; T7c asserts <authorizedkeys>
base64-DECODES back to the supplied key (a round-trip, not a presence check -- a
wrongly-encoded key yields a well-formed config that silently grants no access); T8
asserts a missing key FAILS LOUD.
Note: the harness's existing T1c well-formed-XML check caught a bug in this very change
(an XML comment containing "--", which is illegal). Fixed. That is the harness earning
its keep.
Gauntlet ALL GREEN (52); repo-lint 0 fail.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y117t1F525ba9r1vXSBETk
|

Office1 OPNsense edge build: real-ISP-router config + first-boot fixes
...
Bringing the Office1 OPNsense edge online (router + DHCP for office1-local)
before the Office1 NetBox VM. First-ever OPNsense boot in this repo; several
first-execution findings fixed.
DOCFIX-184: opnsense-prep-image.sh decompresses the .img.bz2 via python3's
stdlib bz2 when bunzip2 is absent (no sudo install needed). Harness 3/3.
DOCFIX-185: the OPNsense edge is a normal simulated-ISP router, not an
egress-airgap. Operator clarified the transport model -- each site (DCs +
Office) has its own real ISP connection; dark fiber is East-West/replication
only; D-107 node-airgap is a separate DC concern. Stripped the WAN
egress-control rules (seq 20/21/99) + MIRROR_* tokens from the config template,
render script, and harness (8/8). Flagged a D-100/D-107 amendment note as
follow-up.
DOCFIX-186: instantiate module "office1_opnsense" in main.tf (first real
validation of modules/opnsense-edge). Two infra findings: (1) config_iso_path
must be OUTSIDE the pool dir (create.content.url collides with its own target
volume) -> stage the ISO; (2) apparmor blocks the custom /var/lib/libvirt/vr1/
pool path (foundational -- all VR1 VMs) -> needs a one-time local apparmor rule
(operator sudo). office1-wan NAT network created via virsh (D-103 debt).
repo-lint 0 fail; opnsense-prep-image 3/3; opnsense-render-config 8/8.
Boot blocked only on the apparmor fix (pending operator sudo).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
| 2026-07-10 |

DOCFIX-149: OPNsense config.xml template + tested renderer
...
Real design work, researched before drafting: fetched OPNsense's own actual
shipped config.xml.sample and its Route.xml static-routes model before
writing anything.
opentofu/templates/opnsense-config.xml.tmpl: {{TOKEN}}-parameterized (reuses
this repo's existing clientdocs convention), covering interfaces/gateway/
staticroutes/DNS/NTP and D-107-shaped firewall rules (default-deny WAN
egress except NTP + the per-DC mirror's upstream sync). The real sample
fetch directly confirmed last turn's audit finding with an actual example:
it ships literal placeholder device names inside each interface's own <if>
block, proving LAN/WAN role assignment really is that explicit per-block
mapping, not declaration order.
scripts/opnsense-render-config.sh: the renderer. Needs no external tool
(unlike every other opnsense-* script this session), so it's tested
END-TO-END, not just guard clauses -- 8/8, including well-formed-XML
validation. The harness caught a real bug before it shipped: the token
HOSTNAME collides with bash's own built-in $HOSTNAME variable (unset
doesn't actually clear it), silently passing a test that should have
failed. Renamed to OPNSENSE_HOSTNAME throughout.
opentofu/templates/README.md's token legend marks exactly which values are
real (NTP pool default, D-106 naming) vs. pending Stage 0 ratification
(D-100/D-101/D-107) vs. only measurable on a real boot (vtnetN assignment)
vs. a security requirement (root password hash must be freshly generated,
never the stock sample's own shipped default).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QrcJx8TUar7pYAvpGJw57A
|