Skip to main content
RunBook Academy

OPNsenseIV · OPNsense ArchitectureOPNsense architecture

The configuration generation model — XML to FreeBSD to running services

Intermediate⏱ ~14 minxmllintconfigctlfindgrep

What you'll learn

  • Locate the OPNsense configuration XML and explain its role as source of truth
  • Trace the path from a GUI change to running FreeBSD service configuration
  • Read the template system that turns XML into service-specific files
  • Recognise the anti-patterns of editing /conf or /var files by hand

Prerequisites

Verified against OPNsense 25.x · FreeBSD 14.x · PF (FreeBSD packet filter) FreeBSD 14.x · Unbound 1.20+ · Kea DHCP OPNsense 25.x plugin · WireGuard in-kernel + OPNsense plugin · strongSwan (IPsec plugin) OPNsense 25.x plugin · OpenVPN 2.6.x · Suricata 7.x · 2026-08-14

Not yet marked complete on this device.

OPNsense’s central design decision is that the GUI and the API are two views of one source of truth — an XML file at /conf/config.xml. Every change the operator makes through either interface lands in that file. Every FreeBSD service that needs configuration reads a generated file that the OPNsense PHP layer produced from the XML.

The operator who understands this model can troubleshoot any “the GUI shows X, the service is doing Y” discrepancy in seconds. The operator who does not eventually hand-edits a generated file and is baffled when the next apply erases the edit.

This lesson covers where the XML lives, what happens on every apply, how the template system turns XML into service config, and why hand-editing generated files is always wrong.

The config.xml: one file, one source of truth

/conf/config.xml is the authoritative configuration. The GUI reads it on every page render; the API reads it on every request; the opnsense-bootstrap script reads it at every boot; the apply process reads it on every change. It is the only file the operator should back up to capture configuration state.

The XML schema is documented in the OPNsense source (under config/) and is stable across releases. The operator does not need to know the entire schema, but knowing how to navigate it is useful:

Read-only / Safexmllint --xpath
$ xmllint --xpath '//system/hostname' /conf/config.xml
<hostname>fw-edge-01</hostname>

Illustrative output

The XML has top-level sections for each subsystem (system, interfaces, filter for firewall rules, nat, dhcpd, unbound, ipsec, openvpn, wireguard, plugins, and so on). Each section uses a consistent shape — <section><item>... <field>value</field></item></section> — which is why XPath works.

The apply path: GUI → XML → templates → services

When the operator clicks Apply in the GUI (or sends a POST to the API), OPNsense runs a sequence:

  1. Validate the change. The PHP code that received the request validates the new configuration. Invalid values are rejected with an error.
  2. Write the XML. The new configuration is written to /conf/config.xml. A timestamped backup is created at /conf/backup/ config-\<YYYYMMDD-HHMMSS\>.xml.
  3. Notify configd. A signal is sent to the configd daemon, which is OPNsense’s central “this configuration changed” handler.
  4. Run template resolvers. For each subsystem that has a template, configd invokes the template resolver. The resolver reads the XML, substitutes values into a template (a PHP file that produces service-specific configuration), and writes the result to the FreeBSD service’s expected location.
  5. Reload the service. The resolver tells the service to reload its configuration. For PF, the resolver runs pfctl -f <generated file>; for Unbound, it sends unbound-control reload; for the ISC DHCP server, it sends service isc-dhcpd reload.
  6. Update the running state. A second XML file, /conf/config.cache, tracks the in-memory state for the running web GUI to read quickly without reparsing config.xml.

The entire path takes seconds. The operator sees the apply succeed or fail in the GUI; the underlying service configuration is reloaded without a daemon restart.

Read-only / Safeconfigctl filter reload
$ configctl filter reload
OK

Illustrative output

The template resolver in detail

Every FreeBSD service that OPNsense manages has a template. The template is a PHP file in /usr/local/opnsense/service/templates/ that produces a string — typically a FreeBSD-native configuration file — by interpolating values from the XML.

For example, the Unbound DNS template lives at /usr/local/opnsense/service/templates/OPNsense/Unbound/unbound.conf. When the resolver runs, it includes the template with the XML values substituted in. The output is written to /var/unbound/unbound.conf. Unbound is then told to reload.

The operator does not need to read templates to operate OPNsense, but knowing they exist is useful for two reasons:

  1. Custom snippets. When a feature is not in the GUI but can be expressed in the underlying service, the operator adds a custom snippet (a raw fragment of PF / Unbound / strongSwan syntax) through the GUI. The template resolver splices the snippet into the output at a defined point.
  2. Debugging. When the GUI shows one thing and the service does another, the operator reads the generated file (the output of the template resolver) and finds the discrepancy there. The lesson on PF internals walks through this for the firewall; the same pattern applies to every service.

What survives reboots and what does not

Two categories:

FileSurvives reboot?Survives apply?Survives firmware update?
/conf/config.xmlyesyes (canonical)yes (preserved)
/conf/backup/config-*.xmlyesyes (every apply creates one)yes
/conf/config.cacheno (regenerated at boot)no (regenerated on apply)no
/var/unbound/unbound.confno (regenerated at boot)no (regenerated on apply)no
/conf/backup/filter/filter.confno (regenerated at boot)no (regenerated on apply)no
Custom snippets in XMLyesyesyes (across the same major)
Certificates, keysyes (under /conf/)yesyes
RRD graphs, syslog bufferyes (under /var/)yesyes
Files in /root/, /home/, /tmp/yesyesyes (operator-managed)

The operator’s mental model: anything under /conf/ is managed; anything under /var/ is generated; anything elsewhere is operator-owned. The boot process reads /conf/config.xml, regenerates everything in /var/, and starts the services. The apply process reads /conf/config.xml, regenerates everything in /var/, and reloads the services.

The “I edited a file and it disappeared” incident pattern

Three failure modes the operator will encounter:

  1. Hand-edited /var/... file disappears on next apply. The fix is the snippet feature. For Unbound: Services → Unbound DNS → Advanced → Custom options. For PF: System → Advanced → Firewall → Generated Rules → Custom Rules. For strongSwan: the per-connection advanced configuration fields.
  2. Hand-edited /etc/rc.conf is overwritten on next apply. The fix is the GUI feature that turns the daemon on (so the generated value matches). If the daemon has no GUI toggle, use sysrc to set the variable and accept that an apply will regenerate it — or use the OPNsense System → Advanced → System → System tunables table to set the variable in a managed way.
  3. Hand-edited /conf/config.xml directly bypasses the validation and the audit trail. The XML is the source of truth, but the GUI and API are the supported writers. A direct XML edit works until the schema changes, until a sibling change overwrites the edit, or until a colleague reads the XML and cannot tell what the operator intended. Use the API for automation; it is the same XML write path the GUI uses, with validation.

Production patterns

Three patterns make the configuration model tractable in a production estate.

Versioned backups as change control

Every apply creates /conf/backup/config-<timestamp>.xml. The operator exports a copy off-box (via System → Configuration → Backups → Download or the API) on a schedule. The versioned backups are the change-control artefact: every change between two backups can be diffed, audited, and reverted.

Configuration as code via the API

The OPNsense API exposes every GUI action. An operator with a configuration-as-code pipeline drives the firewall through the API rather than the XML directly. The XML is the destination, the API is the supported writer, and the pipeline’s git history becomes the audit log. The course has a dedicated lesson on this.

Custom snippets for what the GUI cannot express

When the GUI does not model a configuration the operator needs, the snippet features are the supported escape hatch. Snippets live in the XML, survive reboots and applies, and are visible in the GUI’s “custom” indicator. They are the right way to extend OPNsense without forking the codebase.

Summary

  • /conf/config.xml is the single source of truth. The GUI, the API, the templates, and the boot process all read it.
  • Every apply writes the XML, runs the template resolvers, and reloads the FreeBSD services that need to know.
  • Hand-editing /var/... or /etc/rc.conf is overwritten by the next apply. Use the GUI/API for changes, snippets for extensions, and never edit generated files.
  • The template resolvers are open source, deterministic given the input, and inspectable. Read the generated output to verify what the service is doing.

Knowledge check · 3 questions

  1. Q1. You need to add a local-data Unbound entry to override a public DNS record for an internal view. The GUI does not have a field for this specific override syntax. What is the supported way to add it?

  2. Q2. The /conf/config.xml file is the canonical OPNsense configuration; the GUI, API, boot process and template resolvers all read it as the source of truth.

  3. Q3. Which of the following statements about OPNsense configuration are correct? Select all that apply.

Passing score: 75%. Answers are checked in this browser.