# opentofu/templates/ -- OPNsense config.xml template

`opnsense-config.xml.tmpl` is the `{{TOKEN}}`-parameterized OPNsense
`config.xml` for a DC-DC edge (Office1, DC1, or DC2 -- one instantiation per
site). Built directly from OPNsense's own real, currently-shipped
`config.xml.sample` (`opnsense/core`, fetched 2026-07-09 -- see
`.claude/skills/openstack-cloud-ops/references/opentofu-provider-docs.md`
for the fetch method) plus the confirmed `staticroutes` schema
(`opnsense/core`'s `Route.xml` model). This is real design work, not a
placeholder -- but several tokens genuinely cannot be filled with a real
value yet because the decisions they depend on (D-100/D-101/D-107) are
still PROPOSED, or because the value can only be measured on a real boot.
Never invent a value for those; leave the token required and unfilled until
the real one exists.

## Pipeline

```
opnsense-config.xml.tmpl
  --(scripts/opnsense-render-config.sh, env-var tokens)-->
real config.xml
  --(scripts/opnsense-build-config-iso.sh)-->
config.iso (plain ISO9660, /conf/config.xml)
  --(opentofu/modules/opnsense-edge, config_iso_path variable)-->
attached as a secondary cdrom disk to the domain
```

## Token legend

| Token | Status | Notes |
|---|---|---|
| `{{OPNSENSE_HOSTNAME}}` | Design choice, not yet made | e.g. `opnsense`. NOT named `HOSTNAME` -- that collides with bash's own built-in `$HOSTNAME` variable (caught by this delivery's own test harness: `unset HOSTNAME` doesn't actually clear it, since bash repopulates it). |
| `{{DOMAIN}}` | Real, applies D-106's naming convention | e.g. `omega.dc1.vr1.cloud.neumatrix.local`, following the (still-PROPOSED, but already-settled-as-design-intent) `<service>.omega.<dc>.vr1.cloud.neumatrix.local` pattern -- same status as DC1's plane CIDRs elsewhere in this repo: carried-forward design intent, not invented. |
| `{{ROOT_PASSWORD_HASH}}` | MUST be freshly generated, never reused | The real `config.xml.sample` ships a bcrypt hash (`$2y$10$...`) for a well-known default OPNsense password -- reusing it verbatim would mean every edge shares a public default root password, a real exposure. Generate a fresh one per deployment, e.g. `htpasswd -bnBC 10 "" '<password>' \| tr -d ':\n'` (produces a `$2y$10$...`-compatible bcrypt hash) -- NOT independently verified against a real OPNsense login this session; confirm login works before relying on it operationally. |
| `{{NTP_UPSTREAM_SERVERS}}` / `{{NTP_PREFER_SERVER}}` | Real default supplied (optional to override) | Defaults to the exact same public pool OPNsense's own sample ships (`N.opnsense.pool.ntp.org`) -- a real, confirmed default, not invented. D-107 says the edge "syncs upstream to internet NTP pools," consistent with keeping this default. Override only if a different upstream is decided. |
| `{{WAN_IF}}` / `{{LAN_IF}}` | MUST be measured on a real boot | The `<if>` value is the actual `vtnetN` (or similar) device name FreeBRSD assigns -- confirmed from the real sample that this, not interface declaration order, is what assigns the LAN/WAN role (see `opentofu/README.md`'s "Audit pass" section for the full account of this correction). Boot the domain, run `ifconfig`, and set these to match `opentofu/modules/opnsense-edge`'s actual `lan_network_name`/`wan_network_name` wiring. |
| `{{WAN_IPADDR}}` / `{{WAN_SUBNET_BITS}}` / `{{WAN_GATEWAY}}` | Pending D-100/D-101 | The "simulated ISP uplink" network's real addressing isn't assigned yet (NetBox-pending). |
| `{{LAN_IPADDR}}` / `{{LAN_SUBNET_BITS}}` | Pending a design decision | WHICH plane(s) OPNsense's LAN interface actually serves (metal-admin? provider-public? something else?) is not fully specified in the buildout design as read this session -- confirm before filling this in, don't assume. |
| `{{MIRROR_SYNC_PROTOCOL}}` / `{{MIRROR_UPSTREAM_NET}}` / `{{MIRROR_SYNC_PORT}}` | Pending -- mirror software not chosen | D-107 requires the per-DC artifact mirror's own upstream sync as one of exactly two allowed WAN egress paths (the other is NTP), but doesn't specify which mirror software (apt-mirror? aptly? a generic reverse proxy?) -- the port/protocol depend on that choice. Tooling gap register item 3 territory. |
| `{{ROUTE1_NETWORK}}`/`{{ROUTE1_GATEWAY}}`/`{{ROUTE1_DESCR}}`, `{{ROUTE2_*}}` | Optional, pending DC2 CIDR assignment | At most 2 static routes needed per the known topology (a peer-DC replication-plane route at DC1/DC2; an Office1-management route). Leave a slot's `*_NETWORK` var unset to omit it entirely -- `scripts/opnsense-render-config.sh` drops an unset slot rather than emitting an empty/invalid `<route>`. DC2's own CIDRs aren't assigned yet (D-101 open item), so DC1's route to DC2 can't be filled in until then. |

## Design choices baked into the template (not tokens -- read before assuming these are wrong)

- **Firewall: default-deny WAN egress, explicit NTP + mirror-sync allow rules**
  (D-107's "controlled egress ... narrowly scoped: NTP-only, plus the
  mirror's upstream sync"). The two default LAN-allow rules are kept exactly
  as OPNsense's own sample ships them (basic operation depends on them).
- **`gateways.gateway_item`** is new versus the stock sample (which only has
  an implicit DHCP-derived gateway) -- required because the WAN side here is
  a static "simulated ISP uplink," not DHCP, and `staticroutes.route.gateway`
  must reference a named gateway object (confirmed from the real
  `Route.xml` model: `gateway` is a `JsonKeyValueStoreField` needing "a valid
  gateway from the list"). Named `WAN_GW` -- change consistently in both
  places if you rename it.
- **`unbound`/`nat`/`rrd`/legacy `<filter>`** sections are left exactly as
  the real stock sample ships them -- minimal, working defaults, not
  independently redesigned.

## What this delivery does NOT do

Write the actual, final, per-site `config.xml` files (Office1/DC1/DC2) --
that needs the pending values above filled in with real ones, which in turn
needs Stage 0 ratification (D-100/D-101/D-107) and a real boot to measure
`vtnetN` assignment. This delivery is the mechanism (template + a tested
renderer), consistent with everything else built this session: parameterized
and reusable, not three files with invented IP addresses.
