# 2026-07-13 -- DOCFIX-193 APPLIED: the Office1 edge now serves DHCP

Executed the live apply of the Kea DHCP config built (but deliberately NOT applied) under
DOCFIX-193. The Office1 OPNsense edge is now a router **and** a DHCP server for office1-local.

## What was executed

Target: `root@10.10.0.1` (`office1-opnsense`), the live, routing Office1 edge.
Path: the D-112(c) steady-state path -- render -> scp over SSH -> SHA-256 verify on the
guest -> install to `/conf/config.xml` -> reboot.

    set -a; . ~/vr1-office1.env; set +a
    bash scripts/opnsense-render-config.sh "$OPNSENSE_CONFIG_OUT"
    scp -i "$OPNSENSE_SSH_KEY" "$OPNSENSE_CONFIG_OUT" root@10.10.0.1:/tmp/config.new
    # guest: sha256 -q /tmp/config.new == local sha256, else abort
    # guest: cp /tmp/config.new /conf/config.xml ; reboot

Rendered config sha256 `bc100bd49c9d2bfec7dfd799e03da7cc5fa48a2608a6521ed2dfea645366a9be`;
guest hash matched before install. Edge returned in ~30s.

## Measured end state (post-reboot, over SSH)

- `kea-dhcp4` running (pid 15090), **bound udp4 10.10.0.1:67** -- the deliverable.
- Kea: `enabled=1`, `interfaces=lan`, subnet `10.10.0.0/24`, pool `10.10.0.100-10.10.0.199`,
  routers/DNS `10.10.0.1`.
- Router intact: WAN `172.30.1.2`, LAN `10.10.0.1`, default route `172.30.1.1`,
  NAT `automatic`, egress to `1.1.1.1` **0.0% packet loss**.
- Console intact: `kern.console=ttyu0`, getty running (DOCFIX-192 holds).

NOT yet verified end-to-end: an actual client DHCPDISCOVER->lease. No client exists on
office1-local yet. The udp/67 binding + loaded subnet is the strongest available evidence
until Stage 2 puts a host on that LAN.

## The finding that nearly bit us (and why the push was still safe)

A full-config push REPLACES `/conf/config.xml` wholesale. The rendered template is 128
elements; the live config was 796. The overwrite therefore **drops 667 elements** that
OPNsense's own migrations had populated -- including, critically:

    /OPNsense/Firewall/Filter/rules/rule  x2   "Default allow LAN to any" (+ IPv6)

Those two MVC rules are the ONLY pass rules on the box (`/filter/rule` count is **0** --
the legacy rules were migrated away in 26.1), and pf's base policy is
`block drop in log inet all`. Dropping them with no replacement = an edge that boots but
does not route.

This was NOT resolved by inference. It was settled from the box's own `/conf/backup/`
trail, which shows the overwrite is **self-healing** and had already been exercised three
times on this exact host:

| time (07-12) | elements | MVC rules | legacy /filter | revision |
|---|---|---|---|---|
| 07:50 | 713-733 | 0 | **2** | factory nano; rules live in legacy `/filter` |
| 20:25 | 774 | **2** | 0 | after a template push (0 rules) + boot -> `run_migrations` |
| 23:28 | 776 | **2** | 0 | after a template push (0 rules) + boot -> `run_migrations` |
| 23:30 | 796 | **2** | 0 | after a template push (0 rules) + boot -> `run_migrations` |

Every push of a rule-less config came back with exactly 2 regenerated default-allow-LAN
rules and working routing. The same trail shows `Created web GUI TLS certificate` after
each boot, so `/ca`, `/cert` and `/system/webgui/ssl-certref` regenerate too.

Today's apply was cycle #4 of that proven mechanism plus one additive block, and the
post-reboot measurement confirms it: 791 elements, both LAN rules back, TLS cert back,
and our `subnet4` **survived** the migration pass.

## Defect in the executed command (logged, not fixed here)

Step 1 of the apply -- the pre-install snapshot -- **silently failed**:

    ssh root@10.10.0.1 'cp /conf/config.xml /conf/config.xml.pre-dhcp && echo "... $(sha256 -q ...)"'
    -> Illegal variable name.

root's shell on OPNsense is **tcsh**, not sh. The `$(...)` inside the quoted remote command
is parsed by tcsh, which rejects it -- and because the failure was non-fatal, the install
proceeded **without the named rollback point**. The config was recoverable anyway (OPNsense
keeps `/conf/config.xml.prev` + `/conf/backup/`), so no harm resulted, but the guard did
not exist when it was relied upon.

Lesson for the apply path: every remote command to this box must be fed to `sh -s` (as the
install and verify steps correctly were), never passed as a quoted string to root's tcsh.

## Open gap

The apply path is still **ad-hoc shell**, not a repo script with a harness -- which is why
the tcsh defect above could ship at all. It should become
`scripts/opnsense-apply-config.sh` (snapshot -> scp -> verify -> install -> reboot -> assert),
with the assertions from this session (kea on udp/67, 2 LAN rules, egress, console) as its
post-conditions. LOGGED, not executed -- it is out of scope for this step.

## Revert

    ssh -i "$OPNSENSE_SSH_KEY" root@10.10.0.1 'sh -s' <<'SH'
    cp /conf/config.xml.prev /conf/config.xml && reboot
    SH

Restores the pre-DHCP routing config (the edge routes; nothing serves DHCP). Repo-side,
DOCFIX-193 (`7a2b396`) is the commit that introduced the Kea template block.
