| 2026-07-13 |

SEC-006/SEC-007: put the two known credential exposures on the ledger
...
Both were flagged only in changelog/session-ledger prose, never as ledger rows -- exactly
the failure mode the security-ledger header warns about ("never only a comment in a script
header, where the libvirt item lived for a week"). An exposure with no row has no owner, no
status, and no revoke trigger.
SEC-006: the NetBox API token pasted into agent chat context during the D-111 IPv6 work.
This one is BURNED, not merely due for rotation -- it exists in a transcript outside our
control, so it needs revoke + reissue. Operator ruling 2026-07-13: DEFERRED, revoke at
completion of this deployment. Recorded as OPEN and explicitly live-and-exposed until then,
so the deferral is visible rather than forgotten.
SEC-007: ~/vr1-office1-creds/ holds the Office1 edge root password, its bcrypt hash, and the
office1_svc SSH private key in plaintext (0700/0600). This is a ROTATION obligation, not a
delete-me: D-112(c) makes SSH the only management path to the edge, so the key must exist.
No change to any live system; documentation only.
Revert: git revert this commit (the rows are the only change).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6Fzre1CxCY8tzwFCudu57
|

SEC-005: store a GitBucket credential on the jumphost so pushes stop failing
...
The jumphost clone could not push (could not read Username for https://git.baldurkeep.com),
which left this branch 12 commits ahead of origin for two days -- all the D-112 / OPNsense
edge work existed only on this box. Now fixed: credential.helper store + a dedicated PAT at
~/.git-credentials (mode 600), written by the operator via a hidden prompt. The token was
never read into agent context; the credential is verified by USING it, never by printing it.
Two GitBucket behaviours measured and recorded, because both cost a cycle:
1. The git username is NOT the web-UI login. The operator signs in with an email; that is
rejected for git transport. Measured against info/refs?service=git-receive-pack:
jesse.austin@neumatrix.com -> 401, JANeumatrix (the git user.name) -> 401,
jesse.austin -> 200 CAN PUSH. Note GitBucket 401s PAT basic-auth on /api/v3/ across the
board while accepting it for git -- so validating a credential against the API gives a
FALSE NEGATIVE. Validate against the receive-pack endpoint.
2. credential.helper store DELETES the stored credential when the server rejects it (git
calls credential reject on a 401). The first bad-username push therefore emptied
~/.git-credentials to 0 bytes, and the follow-up diagnostics were authenticating with an
empty string -- which read as a corrupt file and misdirected the investigation. If the
file mysteriously empties: the credential was rejected, it did not fail to write.
Tooling consequence: validate BEFORE writing.
SEC-005 is OPEN by operator ruling: a long-lived, account-wide PAT now sits in plaintext on
the jumphost (GitBucket has no per-repo token scoping, so the control is rotation, not
scope). Revoke/rotate at v1 close, or immediately if the jumphost is shared or rebuilt.
Revert: git config --global --unset credential.helper && rm -f ~/.git-credentials,
then revoke the token in GitBucket -> Account Settings -> Applications.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6Fzre1CxCY8tzwFCudu57
|

DOCFIX-193 APPLIED: the Office1 edge now serves DHCP (kea-dhcp4 on udp/67)
...
Executed the live apply that DOCFIX-193 deliberately left pending. Measured end state
on root@10.10.0.1 after reboot: kea-dhcp4 running, bound udp4 10.10.0.1:67, subnet
10.10.0.0/24, pool .100-.199, routers/DNS 10.10.0.1. Router unaffected -- WAN 172.30.1.2,
LAN 10.10.0.1, default route 172.30.1.1, NAT automatic, egress 1.1.1.1 at 0.0% loss,
serial console + getty alive (DOCFIX-192 holds).
Path: render -> scp -> SHA-256 verify on guest (matched) -> install /conf/config.xml ->
reboot. Edge back in ~30s.
The finding that nearly bit us: a full-config push drops ~667 migration-populated
elements, including the ONLY 2 pass rules on the box ("Default allow LAN to any" + IPv6 --
legacy /filter/rule count is 0 in 26.1) against a pf base policy of block-drop-all.
Dropping them = an edge that boots but does not route. Settled NOT by inference but from
the box's own /conf/backup/ trail: three prior pushes of a rule-less config each came back
with both rules regenerated by run_migrations. Post-apply re-confirms it -- 791 elements,
both LAN rules back, webgui TLS cert back, and our Kea subnet4 survived the migration.
The overwrite is self-healing; do not hand-merge to preserve them.
Defect logged, not fixed here: root's shell on OPNsense is tcsh, so the pre-install
snapshot step (a $(...) inside a quoted remote command) died with "Illegal variable name"
and silently no-op'd -- the install ran without its rollback point. Config was recoverable
via /conf/config.xml.prev + /conf/backup/, so no harm, but the guard was absent when
relied upon. Remote commands must be fed to sh -s. The apply path wants to become a
harnessed scripts/opnsense-apply-config.sh; LOGGED as an open gap, out of scope here.
Not verified end-to-end: no client has taken a lease (office1-local has no host yet).
Revert: ssh root@10.10.0.1 'sh -s' <<< 'cp /conf/config.xml.prev /conf/config.xml && reboot'
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
|

D-112(c) EXECUTED: the Office1 OPNsense edge is UP and ROUTING
...
End state, measured over SSH: office1-opnsense, OPNsense 26.1, WAN 172.30.1.2/24 static,
LAN 10.10.0.1/24, default route via 172.30.1.1, NAT active, and egress to the internet
WORKS through the simulated ISP (ping 1.1.1.1, 0% loss). Serial console restored
(kern.console=ttyu0, getty running). Managed over SSH with the service key.
Bootstrap: console (pexpect + virsh console) -> factory login -> config streamed as
base64, SHA-256 verified byte-exact, factory config backed up -> reboot. The DOCFIX-192
fix was then pushed OVER SSH -- the D-112(c) steady-state path proving itself.
Gotchas recorded in the ledger because each cost real time:
- Console settings need TWO reboots. OPNsense regenerates /boot.config and
/boot/loader.conf FROM config.xml *during* boot, so they only take effect on the NEXT
boot. After reboot #1 the loader files were already correct while the running kernel
was still on video. Do not conclude "the fix failed" after one reboot.
- /etc/ttys uses `onifconsole`: getty starts on ttyu0 only if ttyu0 IS the kernel
console. `sysctl kern.console` is ground truth.
- The guest's root shell is CSH (both via console and ssh). It rejects $(...) with
"Illegal variable name." Pipe POSIX scripts to `sh -s`.
- `virsh console` REFUSES a unix-socket chardev. The serial must be a pty + log file.
LEDGER CORRECTION: the 2026-07-12 entry claimed the rendered config provided
"DHCP .100-.199". That is FALSE -- there is NO DHCP configuration in the template at all
(no <dhcpd>, no <Kea>), and nothing serves DHCP on office1-local (measured: no
kea/dhcpd/dnsmasq process, nothing on udp/67). Another optimistic claim from the
max-context session. The operator's goal was "router + DHCP": the ROUTER half is DONE,
the DHCP half was never built. OPNsense 26.1 uses Kea (ISC dhcpd is gone), so this needs
the Kea schema verified upstream -- do not invent it.
repo-lint 0 fail.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y117t1F525ba9r1vXSBETk
|

DOCFIX-192: the edge config silenced its own ONLY console (no serial, no video device)
...
Third lockout-class defect in the same config, found by executing D-112(c) for real.
After the config was applied and the box rebooted, the serial console went PERMANENTLY
SILENT -- no login prompt, no output, nothing (measured: raw console read returns empty).
The edge VM has NO VIDEO DEVICE, so the serial port is its only console and its only
break-glass path.
Cause: the config set neither <primaryconsole> nor <serialspeed>, so the applied config
did not select the serial console. Boot output still appears (the loader honours the nano
image's own /boot.config -S115200), which is why this looks like a hang rather than a
config choice -- the console dies exactly when OPNsense applies our config.
The box was NOT lost: DOCFIX-191 had just enabled sshd + installed the service key, so it
stayed reachable. That is the only reason this was a finding and not an outage. Two
lockout bugs in one config, and the first one saved us from the second.
Key names VERIFIED against upstream src/www/system_advanced_admin.php:
- `primaryconsole` and `serialspeed` are STRING values.
- There is NO `enableserial` key in OPNsense -- that is a pfSense-ism. Do not add it.
115200 matches the nano image's own /boot.config.
Harness 12 -> 15 PASS. T9b asserts the serial console is retained; T9c guards against
someone "fixing" this by re-adding the bogus pfSense <enableserial> key.
(T9c initially failed on my own template COMMENT, which names enableserial while warning
against it. Tightened to match an actual element. The harness caught it.)
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
|

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
|

D-112: record option (a) as MEASURED INFEASIBLE; rule the (c) bootstrap; make the edge console interactive
...
Operator asked to re-examine option (a) (bake /conf/config.xml into the nano) before
committing to (c). Done, and it is BLOCKED on this host -- measured, not assumed:
CONFIG_UFS_FS=m but "# CONFIG_UFS_FS_WRITE is not set" -> UFS driver is READ-ONLY
partition table: MBR, a single FreeBSD a5 partition -> UFS only, no FAT to write
fuse-ufs / ufs2-tools -> not present, not packaged
libguestfs (guestfish / virt-copy-in) -> not installed
Two ways (a) could be forced, both rejected and recorded so they are not re-litigated:
a FreeBSD helper VM (works, xorriso is present to remaster a live ISO -- but it means
maintaining a builder VM forever to deliver one 4.5 KB file), and a raw byte-patch of
the image (probably works; silently corrupts if the file is not block-contiguous, and
is NON-TRANSFERABLE -- you cannot byte-patch a real appliance on Roosevelt hardware,
which is exactly what this repo's governing constraint forbids).
Examining (a) properly STRENGTHENED (c): (a) is not merely less mainstream here, it is
unavailable without a detour costing more than (c)'s entire bootstrap.
Bootstrap RULED (operator): B1 = temporary host leg on virbr2 (reversible; no change to
the D-100 network definition). B2 = one interactive console login to enable SSH and
install the ed25519 pubkey already generated in ~/vr1-office1-creds/, after which
everything is plain SSH/SCP.
Module change enabling B2: the edge's serial becomes a unix socket + a log file, rather
than a write-only file source. Both roles are needed and they are different:
source.unix -> INTERACTIVE bidirectional console. A `file` source is write-only capture
and cannot be typed into, so it cannot bootstrap anything. A unix socket
is scriptable with `socat - UNIX-CONNECT:`, unlike a pty, which needs a
controlling TTY.
log -> preserves full boot capture from power-on (a socket loses anything
emitted before a client connects). That capture is what made both
2026-07-12 boot bugs legible; it is not being dropped.
S1+S2 guards PASS, harness 6/6, repo-lint 0 fail, tofu fmt/validate clean.
Live recreate + host leg remain separately gated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y117t1F525ba9r1vXSBETk
|

D-112 ADOPTED: option (c) post-boot network provisioning (operator ruling)
...
Operator ruled (c): "C since it is the way people automate opnsense" -- prefer the
mainstream automation path (the OPNsense REST API, as used by opn-cli and the
Ansible OPNsense collection) over image-baking or a media-import trick. The
rendered config (opnsense-config.xml.tmpl) is REUSED unchanged; only the DELIVERY
mechanism changes.
IMPLEMENTATION NOT STARTED -- deliberately. "Push the config over the API" is not
actually reachable from a factory-default OPNsense, and the bootstrap sub-choices
are unruled. Recorded in D-112 so the next session does not discover this the hard
way:
1. NO ROUTE. Factory-default LAN is 192.168.1.1/24 on office1-local, an ISOLATED
libvirt network with no host leg on virbr2 (measured). The WAN side is reachable
at 172.30.1.126 but OPNsense blocks all inbound on WAN by default, so it is not
a way in.
2. NO CREDENTIALED API. The REST API authenticates with an API key+secret (not the
root password), minted via the GUI. SSH is disabled in the factory default.
3. OUR SERIAL IS WRITE-ONLY. The module attaches <serial type='file'> -- a one-way
capture that cannot be typed into. An interactive console means type='pty' +
virsh console (module change + recreate).
Sub-choices B1 (how to reach it) and B2 (how to authenticate first) are left
UNRULED in D-112 rather than picked.
SELF-CORRECTION recorded: I had earlier described option (b) as making the ISO
mechanism "work as designed, unchanged". That was over-claimed -- even with a
read-only root the importer prompts with a 7s timeout and asks for a device name,
so (b) may have been (d) in disguise. Logged so nobody revives (b) on my bad
summary. Note a console LOGIN SHELL is not the rejected (d): (d) was rejected for
racing the importer's 7s timeout; a login shell has no timing race.
The config-ISO path (opnsense-build-config-iso.sh + harness, the module's
config_seed volume + cdrom disk) is RETIRED but left in place -- it is inert, not
harmful. Remove it in the same change that lands (c), not before.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y117t1F525ba9r1vXSBETk
|

D-112 (PROPOSED): OPNsense Config Importer cannot fire on a nano image -- by design
...
Bug 3 of the Office1 edge boot. With memory (DOCFIX-188) and ACPI (DOCFIX-190)
fixed, OPNsense boots -- but on FACTORY DEFAULTS. The config ISO was never read.
Root cause is architectural, not a bug. rc.syshook.d/import/20-importer runs
`opnsense-importer -b`, whose first act is:
INSTALL="/.probe.for.readonly"
touch ${INSTALL} 2> /dev/null
if [ -f ${INSTALL} -a -f /conf/config.xml ]; then
bootstrap_and_exit 0
fi
That touch is a PROBE FOR A READ-ONLY ROOT. On installer media root is read-only,
the touch fails, the marker is absent, and the importer scans attached media
(cd9660 included). On a pre-installed nano the root is WRITABLE and a factory
/conf/config.xml already exists -- so both conditions hold and it exits
immediately, never enumerating a single device.
Our ISO was correct all along: verified ISO9660 'OPNSENSE_CFG' containing
CONF/CONFIG.XML with our 10.10.0.1, and the guest sees it as cd0 at exactly the
right byte size. Nothing was ever going to read it.
The README's research correctly established that the Importer's scan path supports
ISO9660 -- but never that the Importer RUNS on a nano image. It does not. The
module header has always flagged this mechanism UNVERIFIED; that flag is now
discharged NEGATIVE.
Recorded as D-112 PROPOSED with four options (bake config into the image / switch
to the installer image / post-boot API push / serial expect-scripting). NOT
implemented -- this replaces the config-delivery mechanism of a built surface and
also gates Stage 3, so it is the operator's ruling to make.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y117t1F525ba9r1vXSBETk
|

DOCFIX-190: second boot bug -- libvirt domains had ACPI disabled (FreeBSD kernel panic)
...
With DOCFIX-188's memory fix in place the guest finally got its 2 GiB, the kernel
reached interrupt initialisation, and hit a SECOND defect the first bug had masked:
panic: running without device atpic requires a local APIC
apic_init() ... mi_startup() ... db>
None of the three VM modules emitted a `features` block, so libvirt rendered
`-machine pc-i440fx-noble,...,acpi=off`. FreeBSD gets its local APIC from ACPI's
MADT table and the OPNsense kernel has no atpic fallback, so with ACPI off it has
no usable interrupt controller. The guest parks in the ddb debugger -- which is
the 100%-of-one-core spin, while virsh still reports the domain as "running".
Fix: features = { acpi = true, apic = {} } in opnsense-edge, cloudinit-vm and
node-vm. node-vm matters most long term: MAAS drives power via ACPI signalling,
so without it graceful shutdown / MAAS power-off would never have worked.
Guard S2 added. Like memory_unit, `tofu validate` cannot catch this -- the whole
features block is optional, so its absence is schema-valid. Flag renamed
--static-only (--check-memory-unit kept as a back-compat alias). Harness 4 -> 6
PASS; fixtures/s2-bad PASSES S1 and still FAILS S2, proving the guards catch
independent classes rather than one masking the other.
LIVE RESULT (measured): acpi=on; kernel boots to userland; CPU idle, no spin;
DHCP lease 172.30.1.126 hostname "OPNsense" on office1-wan. Both boot bugs closed.
STILL OPEN: the Configuration Importer did not apply -- OPNsense came up on
factory defaults. The ISO is verified well-formed (ISO9660 OPNSENSE_CFG containing
CONF/CONFIG.XML with our 10.10.0.1). This is the mechanism the module header has
always flagged UNVERIFIED. Do not guess the fix; read the Importer's console
output. The unanswered WAN ping is NOT a fault -- OPNsense blocks inbound on WAN.
Gauntlet-relevant suites green; repo-lint 0 fail; tofu fmt/validate clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y117t1F525ba9r1vXSBETk
|

DOCFIX-189: re-audit the max-context session's troubleshooting + docs
...
Operator-instructed re-evaluation. The 2026-07-12 session authored its docs and
module comments WHILE WORKING FROM A WRONG DIAGNOSIS, at a context limit, so its
reasoning was re-checked rather than just its one-line bug (DOCFIX-188).
Headline: the settings it changed are mostly fine and are RETAINED. The reasoning
it recorded was not -- it attributed the fault to whatever knob was being turned,
on no evidence, four separate times. It also left one silently-dead knob.
FALSE CAUSAL CLAIMS (corrected at the source, opnsense-edge/main.tf). The disk
shape, q35->i440fx, the serial console and svm-disable were EACH written up as a
contributing cause. None changed the symptom -- the fault sat at a deterministic
262 bytes through all of them. All four settings RETAINED (each defensible on its
own merits); only the false justifications are gone. The serial console earned its
keep, but it REVEALED the fault, it did not change it.
DEAD KNOB (real functional defect). `disk_size_bytes = 16 GiB` did nothing once
DOCFIX-187 made the disk a direct copy; unused variables are legal HCL so neither
validate nor plan complained. MEASURED: the live disk is 11.00 GiB, not 16.
Removed from main.tf + module variables; real sizing is opnsense-prep-image.sh's
GROW (+8G). Proof it was dead: tofu plan is byte-identical before/after removal.
STALE/UNSOURCED FACTS. "Opteron_G3" is a red herring -- the host is really an AMD
EPYC 9965 (Zen 5, family 26); libvirt's CPU-model DB doesn't know family 26 and
falls back to the oldest name, and the prior session partly built its nested-virt
theory on that artifact. "OPNsense's 3 GB min" is unsourced -- annotated, do not
propagate. DOCFIX-186's backing_store parenthetical is stale (same conclusion,
different mechanism). DOCFIX-187's changelog gains a SUPERSEDED-IN-PART header.
KEPT: DOCFIX-186's apparmor + config-iso-staging findings are real and measured.
DOCFIX-185 (operator transport ruling) untouched.
STANDING LESSON: when a change does not fix the symptom, do not write it up as
though it did. A fault that is deterministic and immune to every knob you turn
means you have not yet touched the cause.
Gauntlet ALL GREEN (52); repo-lint 0 fail; tofu fmt/validate/plan clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y117t1F525ba9r1vXSBETk
|
DOCFIX-188 followup: correct the pass criteria (anchor on virsh dominfo, not the QEMU -m string)
...
libvirt normalizes MiB to KiB in the QEMU cmdline, so '-m size=2097152k' would be
SUCCESS, not failure -- the original criterion would have read a correct boot as a
failed one. Anchor on 'virsh dominfo' Max memory, which is unambiguous. Also: re-run
plan AFTER the undefine (the captured in-place plan is not what executes), and
pre-stage the two fallbacks (provider ignores memory_unit -> *1024; boots but no
router -> sizing, not a regression).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y117t1F525ba9r1vXSBETk
|

DOCFIX-188: root-cause the OPNsense boot triple-fault -- libvirt memory_unit (guest had 2 MiB)
...
The 2026-07-12 triple-fault was never a CPU/nesting/machine/console problem.
dmacvicar/libvirt >=0.9 reinterprets `memory` as raw libvirt units (KiB), not
MiB (the 0.8-era meaning the modules were authored against). With no
`memory_unit`, `memory = 2048` rendered `<memory unit='KiB'>2048</memory>` ->
QEMU `-m size=2048k` = 2 MiB. boot2 fits in 2 MiB and echoes /boot.config;
/boot/loader does not -- hence the deterministic 262-byte fault, immune to every
knob the prior window turned.
Measured every hop: module input -> tofu state -> domain XML -> virsh dominfo
(Max memory: 2048 KiB) -> live QEMU cmdline.
FOUNDATIONAL: the identical defect was latent in all three VM modules --
opnsense-edge (live blocker), cloudinit-vm (MAAS/NetBox/GitBucket, the next
step), node-vm (Stage 3). Every VR1 VM would have gotten 1024x too little RAM.
- memory_unit = "MiB" on all three libvirt_domain resources (memory_mib = 2048
value untouched -- it now means the 2 GiB always intended).
- Corrected the disproven "svm is ROOT CAUSE" comment in opnsense-edge; the
setting is retained as legitimate hardening, the false claim is not.
- Guard: opentofu-validate.sh S1 + --check-memory-unit flag. `tofu validate`
cannot catch this (memory_unit is optional => omitting it is schema-valid,
which is exactly how it shipped). Harness T3 is a true negative test: the
fixture IS the shipped defect. Harness 1 -> 4 PASS.
- Incident report gains a READ-THIS-FIRST box so nobody re-chases the CPU theories.
Gauntlet ALL GREEN (52 harnesses); repo-lint 0 fail; tofu fmt/validate clean.
Live boot verification remains a separately-gated operator step.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y117t1F525ba9r1vXSBETk
|

DOCFIX-187: opnsense-edge module boot fixes + boot-triple-fault incident report
...
Office1 OPNsense edge first-boot debugging. Several correct module fixes made;
the boot is NOT yet resolved (BTX-loader triple-fault under double-nested virt),
documented as an OPEN incident for the next session.
Module (opentofu/modules/opnsense-edge/main.tf), all kept (a working config
needs them, none resolved the fault):
- serial console (nano is serial-only; the module had none) + boot-log capture
- machine q35 -> i440fx (pc)
- disk COW-overlay -> direct per-VM copy (matches virt-install --import)
- CPU host-passthrough with AMD svm disabled (documented AMD nested-virt fix;
note the provider key is `features` plural, not `feature`)
Incident report docs/incident-20260712-opnsense-edge-boot-triplefault.md: full
symptom (boot2 echoes /boot.config then triple-faults loading /boot/loader, a
consistent 262-byte fault confirmed fresh each boot), environment (double-nested
AMD Opteron_G3), everything tried, and ranked next steps (full CPU flags
+kvm_pv_eoi/+kvm_pv_unhalt, add a video device for the -D dual console, mem 4G,
UEFI/OVMF, outer-hypervisor CPU). Ledger checkpoint added; session at context
limit.
repo-lint 0 fail; module tofu validate Success.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|

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-11 |

D-111: align VR1 v6 subcarve to deployed NN mnemonic; fix ULA-gen doc bug
...
Basing VR1 NetBox on the real netbox.baldurkeep.com modeling (operator
directive). A read-only live export confirmed VR0-DC0/Willamette use an NN
net-byte mnemonic with /60-per-plane + /64-active; dc-dc-prefixes-import.py
did not match it (arbitrary contiguous indices, /64-only).
DOCFIX-182 / D-111: carve_v6 replaces carve_ula/carve_gua -- provider :10
(+API-VIP :11) GUA, metal /60 shared by admin :20 / internal :21, data :30,
storage :40, repl :50, all /60+/64 ULA; the ULA /56 is indexed to the GUA site
nibble so the 4th hextet reads DC.NN in both families. Direct-math _sub_at (no
subnet enumeration -- preserves the DOCFIX-181 no-hang property). Harness
40 -> 53 PASS. D-111 ADOPTED in design-decisions.md; scope-doc sub-decision #1
ratified.
DOCFIX-183: fix the invalid-IPv6 ULA-generation sed in the netem/ULA proposal
doc (4+6 -> 2+4+4 hextets, caught by actually running it); ratified VR1 ULA is
fd50:840e:74e2::/48.
repo-lint 0 fail; tests/dc-dc-prefixes-import 53/53.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Merge origin/main (netbox uploads 057c31b) into stage1 branch -- brings the NetBox buildout scope doc + roles-aggregates tool alongside the carve_gua fix
|
|
|
| 2026-07-10 |
DOCFIX-181: fix carve_gua subnet-enumeration hang (gauntlet was stalling)
...
netbox/dc-dc-prefixes-import.py::carve_gua() list()-ed every /64 subnet of the
GUA prefix (2**24 = ~16.7M for D-101's /40 example) only to use the first ->
hung/OOM. Real import runs would hang identically, not just the test. Fixed to
take the first /64 lazily via next() (O(1)).
Added a faulthandler watchdog to tests/dc-dc-prefixes-import/test_logic.py so a
future blocking regression self-aborts with a traceback after 30s instead of
silently stalling the whole run-tests-all.sh gauntlet -- the false-green that
let the ledger claim "40/40" for a test that never actually completed.
tests/dc-dc-prefixes-import: 40/40 in 0.05s (was: hung indefinitely).
Full gauntlet: ALL GREEN (52 harnesses). repo-lint 0 fail.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|

DC-DC Stage 1 (dc-dc-phase0): first real execution + scaffold/tooling fixes
...
First end-to-end run of a DC-DC runbook against real infrastructure (vcloud host,
OpenTofu v1.12.3). Applied 13 libvirt objects (6 DC1 planes, dc1/office1/dc2
pools, office1-local, 3 mesh legs, all MTU 9000); prior VR0 nets torn down (wan
kept as a gap-#17 model); DC2 planes deferred (NetBox supernet), DC2 storage wired.
DOCFIX-178: scripts/prereqs/ idempotent workstation prereq installers + a
Prerequisites section in the runbook + tests/prereqs/ (25/25). Runbooks must
not assume prereq runtimes.
DOCFIX-179: OpenTofu scaffold first validation + apply. Per-module
required_providers (child modules do NOT inherit provider SOURCE -> OpenTofu
inferred nonexistent hashicorp/libvirt); tofu fmt -recursive; provider "maas"
deferred to Stage 3 (kept a sensitive key out of Stage-1 plan/state);
repo-lint now skips the .terraform/ provider cache; .terraform.lock.hcl added.
DOCFIX-180: dc-dc-phase0 runbook as-executed corrections folded in at phase
close (two MTU domains, non-pristine-host handling, Step-9 13-resource plan,
-input=false, Known-Gap resolved).
repo-lint 0 fail; prereqs harness 25/25; repo-lint harness 34/34.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
DOCFIX-177: make the skill and runbooks/README.md aware the VR1 DC-DC track exists
...
Found while the operator was heading to the vcloud host to begin real
execution: SKILL.md and runbooks/README.md (what its routing table points
to) had zero mentions of the entire VR1 DC-DC track, so a fresh session
would be routed to VR0's phase-01..08 runbooks with no indication today's
actual mission (dc-dc-phase0..6 + the new teardown runbook) exists.
Added a "VR1 DC-DC track" index to runbooks/README.md, VR1-specific rows
to SKILL.md's routing table and standard loops, and pointers in
references/environment.md and references/troubleshooting.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QrcJx8TUar7pYAvpGJw57A
|

DOCFIX-175/176: OpenTofu state file security + VR1 teardown/rollback runbook
...
Found while reviewing what's missing from the VR1 buildout ahead of
tomorrow's execution.
DOCFIX-175: terraform.tfstate stores sensitive=true variables (like
maas_api_key) in plaintext regardless of the flag, which only redacts
CLI/plan output (verified against HashiCorp's own docs). Added .gitignore
coverage, a "State file handling" section in opentofu/README.md, an inline
caveat on maas_api_key's description, and a "SECURE THE STATE FILE" callout
at Stage 1 Step 10 -- the actual first real tofu apply (an early draft
wrongly assumed Stage 3; corrected after reading the runbooks directly).
DOCFIX-176: new runbooks/dc-dc-teardown-rollback.md for the OpenTofu/
libvirt/MAAS-vm_host layer, which D-061's existing teardown scripts don't
cover (different layer, juju/MAAS-machine). Two paths (scoped -target
teardown or full VR1 destroy), a rollback decision tree favoring
fix-forward over destroy, and explicit mesh-link shared-infrastructure
handling.
Also folded in: a fix to docs/changelog-20260710-ledger-scan-self-inflicted-falsepositive-fix.md
after re-running ledger-scan.sh caught that its own narration (and the
session-ledger mirror) had reintroduced the exact false-positive class
DOCFIX-174 had just fixed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QrcJx8TUar7pYAvpGJw57A
|
DOCFIX-174: fix self-inflicted false positive in ledger-scan.sh next-free scan
...
DOCFIX-173's own changelog narrated its {3}->{3,} regex fix using a literal
illustrative example ("DOCFIX-1004"), which the widened regex then matched
as a real assignment -- inflating DOCFIX next-free from 174 to 1005.
Reworded the illustrative examples in prose, added a reproduc-line
exclusion to nextfree_mentions() as defense-in-depth, and added a
regression fixture to the test harness (37/37 PASS).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QrcJx8TUar7pYAvpGJw57A
|

DOCFIX-173: full-project sweep -- script logic bugs + security hardening
...
Four confirmed logic bugs (unguarded pipelines/function calls under
set -e+pipefail), all independently reproduced in isolation before being
trusted -- including catching that a first proposed fix for one of them
was itself wrong: `|| true` after a failing pipeline under pipefail
silently discards PIPESTATUS, which would have made phase-06-capi-stack's
own gate check never fire again. The correct `if pipeline; then :; fi`
idiom was verified via reproduction before committing.
- carve-host-interfaces.sh: emit()'s unguarded `return 1` killed apply-mode
runs on the first MAAS error, before the FATAL-accumulator summary
could print.
- phase-06-capi-stack.sh: run_step's PIPESTATUS read was dead code.
- juju-spaces-check.sh, osd-blank-check.sh: one unguarded display-only
pipe each, safely `|| true`-able.
- ledger-scan.sh: DOCFIX/BUNDLEFIX next-free regex used a fixed {3} not
{3,} -- the same bug class already fixed once for the D-series, never
mirrored here. Dormant, not yet reachable, but a real landmine.
- dc-dc-rbd-mirror.sh: hardened the bootstrap token's default path
(random suffix + immediate chmod 600, was a predictable /tmp filename
with no permission hardening).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QrcJx8TUar7pYAvpGJw57A
|
DOCFIX-172: full-project sweep -- bundle.yaml fixes
...
magnum-dashboard had NO bindings: block at all -- a real, pre-existing bug
(confirmed via git log, present since the D-052 explicit-bindings commit,
not a regression from tonight's DOCFIX-167 work) violating this bundle's
own stated "explicit per-application blocks" discipline. Fixed with a
minimal block confirmed against the real charm-magnum-dashboard metadata
(subordinate, only a real `dashboard` endpoint -- not a copy of
octavia-dashboard's fuller shape). Also relocated two section-header
comments that visually (not functionally) misattributed to the wrong app,
and corrected a stale "11 API charms" HA-chain count to 12. 56 apps/108
relations unchanged; provider-bundle-check.py PASS throughout.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QrcJx8TUar7pYAvpGJw57A
|
DOCFIX-171: full-project sweep -- DC-DC runbook consistency fixes
...
Stage 1's calculator scripts (DOCFIX-162) were never wired into its own
Step 3 (still showed manual arithmetic) -- fixed to actually invoke them.
Stage 5's Sequence-list still tagged the IPv6 overlay [NOT YET WRITTEN]
after the Step 6 body was already updated -- fixed to match. Stage 6's
delivery checklist claimed its steps call the new Ceph replication
scripts when the step bodies still only showed raw commands -- added real
"PREFER scripts/dc-dc-..." pointers to each affected step. Also closed out
two DOCFIX numbers (164, 167) whose ledger entries never got their real
number written into the closing line during the earlier integration pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QrcJx8TUar7pYAvpGJw57A
|
DOCFIX-170: full-project sweep -- VR0 runbook + OpenTofu doc fixes
...
Part of an 8-dimension parallel sweep for errors/architectural problems
across the whole repo, requested after DOCFIX-169. phase-08-workload-
cluster-acceptance.md had the same cascading-staleness class as DOCFIX-169
(Designate framing never updated after DOCFIX-167's bundle.yaml edit) --
fixed with a nuanced correction distinguishing "charms now active/idle"
from "D-011.8's real acceptance criterion still needs Stage 7's overlay."
Also fixed a stale opentofu/modules/base-image/outputs.tf comment
referencing nonexistent cloudinit-vm variables. Session ledger updated
with the full 8-dimension sweep summary and two orphaned DOCFIX-number
closures (164, 167) caught as integration misses.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QrcJx8TUar7pYAvpGJw57A
|
DOCFIX-169: fix stale bundle.yaml expectations in phase-01-bundle-deploy.md
...
DOCFIX-167 (Designate + D-108 DR mechanism reactivation) edited the same
shared bundle.yaml this existing, live-cloud-relevant VR0 runbook deploys,
but didn't update this runbook's own documented expectations. Found during
a full-project sweep: "expected plan: 50 apps, 97 relations" was stale
(real count 56/108); "NO designate (D-019)" GATE text was flatly wrong.
Fixed both live-gate references; left the historical 2026-06-03 as-built
record unchanged (append-only audit trail). Flagged, rather than decided,
a real architectural tension: this bundle now serves both a plain VR0
redeploy and DC1's buildout, so any future phase-01 run gets the new
charms regardless of intent.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QrcJx8TUar7pYAvpGJw57A
|

DOCFIX-168: proposed netem parameters + ULA/GUA generation guidance (gaps #4(d)+11); tracker sync
...
docs/dc-dc-netem-and-ula-gua-proposal.md: a concrete, reasoned netem
parameter proposal (1ms/0.2ms jitter/0.01% loss/uncapped rate) for operator
ratification, plus the exact safe command (openssl rand -hex 5, RFC
4193-compliant) for the operator to generate the real org ULA /48
themselves. Deliberately does not generate the GUA carve or DC2's supernet
-- both need real ARIN/NetBox coordination this session has no authority
or visibility to perform.
Also finalizes docs/dc-dc-deployment-workflow.md and docs/session-ledger.md
across all seven gap-closure deliveries tonight (DOCFIX-162 through 168):
tooling gap register items #4(d), #5, #6, #7, #11, #12, #13, #14, #15, #16
updated to their real closed/partial state, new gap #17 added, harness-debt
item #10 corrected. Full test gauntlet re-run clean: same pre-existing
environment-gap failures as every earlier check tonight (jq missing on
this workstation), zero regressions across all seven new script families.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QrcJx8TUar7pYAvpGJw57A
|