<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Opnsense on Dexome</title>
        <link>https://blog.dexome.com/tags/opnsense/</link>
        <description>Recent content in Opnsense on Dexome</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>en</language>
        <lastBuildDate>Tue, 07 Jul 2026 00:00:00 +0530</lastBuildDate><atom:link href="https://blog.dexome.com/tags/opnsense/index.xml" rel="self" type="application/rss+xml" /><item>
        <title>Managing My Existing OPNsense Setup with OpenTofu</title>
        <link>https://blog.dexome.com/post/firewall-as-code/</link>
        <pubDate>Tue, 07 Jul 2026 00:00:00 +0530</pubDate>
        
        <guid>https://blog.dexome.com/post/firewall-as-code/</guid>
        <description>&lt;p&gt;I wanted to manage my existing OPNsense configuration using OpenTofu. The
firewall was already running DNS, DHCP, several VLANs, VPN connections and all
the rules for my home network. Recreating everything from code was not an option.&lt;/p&gt;
&lt;p&gt;I used the &lt;code&gt;browningluke/opnsense&lt;/code&gt; provider and started with one Unbound DNS
setting. After importing it, I did not continue until the plan showed:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;No changes. Your infrastructure matches the configuration.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;This worked for DNS, but the next step caused a DHCP outage because one provider
default removed the gateway and DNS options from client leases. This post covers
the order I used after that incident and the provider limitations I found.&lt;/p&gt;
&lt;h2 id=&#34;import-first-and-make-no-changes&#34;&gt;Import first and make no changes
&lt;/h2&gt;&lt;p&gt;There are two separate things we may want to do during this migration:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;represent the current system in code;&lt;/li&gt;
&lt;li&gt;clean up the system while doing it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I would not combine them.&lt;/p&gt;
&lt;p&gt;The first goal is adoption. Its success criterion is boring: after import, the
configuration describes the live object exactly enough that a plan proposes no
change. Only after that baseline is stable should a separate change improve the
object.&lt;/p&gt;
&lt;p&gt;This matters most for routers and firewalls because the management path is one
of the resources being changed. An incorrect web-server deployment can return a
500. An incorrect gateway, DHCP option, or anti-lockout rule can remove the path
you need to repair it.&lt;/p&gt;
&lt;p&gt;I used four gates for every subsystem:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;inventory ──&amp;gt; import ──&amp;gt; zero-diff plan ──&amp;gt; one-object apply
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;		│             │             │                  │
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;		└── stop ─────┴── stop ─────┴── stop on drift ─┘
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The first real apply was always deliberately small.&lt;/p&gt;
&lt;figure class=&#34;article-diagram&#34;&gt;
    &lt;pre class=&#34;mermaid&#34;&gt;
flowchart TB
	U[Unbound DNS: 66 objects] --&gt;|zero-diff plan| K[Kea DHCP: 64 objects]
	K --&gt;|client lease test| O[Omada VLANs, profiles, ports and SSIDs]
	O --&gt;|controller no-op| C[Guest firewall canary]
	C --&gt;|compiled pf order| F[Per-interface filter migration]
	F --&gt; N[NAT and VPN resources]
&lt;/pre&gt;
    &lt;figcaption&gt;The adoption moved outward from lower-risk DNS objects to connectivity-critical filters and NAT, with a stop gate after every phase.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&#34;start-with-unbound-dns&#34;&gt;Start with Unbound DNS
&lt;/h2&gt;&lt;p&gt;Resolver settings and host overrides were a good first target. Sixty-six Unbound
objects were numerous enough to test import automation but less dangerous than
rewriting the firewall ruleset.&lt;/p&gt;
&lt;p&gt;The import revealed an important category of provider behavior: fields that
exist on the appliance but not in the provider schema. One host override generated
a reverse record, yet the provider did not expose that switch. Importing and
planning the resource produced no change, so the appliance-only field survived.&lt;/p&gt;
&lt;p&gt;That was acceptable. IaC coverage does not need to be 100 percent to be useful.
It does need to be honest.&lt;/p&gt;
&lt;p&gt;Another DNS feature exposed the opposite problem: the provider could read a
blocklist setting but failed when writing it. Rather than force ownership, I left
that feature GUI-managed and documented the boundary. A provider that cannot
round-trip a field does not own that field.&lt;/p&gt;
&lt;p&gt;The first phase ended with dozens of objects imported and a no-op plan. The point
was not the count. It was proving the API credentials, import identifiers, schema,
and state storage before touching client connectivity.&lt;/p&gt;
&lt;h2 id=&#34;kea-dhcp-and-the-auto_collect-issue&#34;&gt;Kea DHCP and the &lt;code&gt;auto_collect&lt;/code&gt; issue
&lt;/h2&gt;&lt;p&gt;Kea DHCP import covered 64 objects and looked equally clean until the first apply.
Clients on THINGS and QUANTUM renewed and still received valid addresses, but
they lost their default gateway and DNS server.&lt;/p&gt;
&lt;p&gt;The provider exposed an &lt;code&gt;auto_collect&lt;/code&gt; option. Its default was enabled, suggesting
that the appliance would derive subnet options automatically. On this system it
did not. Applying the resource removed the stored router, DNS, and NTP values.&lt;/p&gt;
&lt;p&gt;A simplified version of the dangerous assumption looked like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-hcl&#34; data-lang=&#34;hcl&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;resource&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;firewall_dhcp_subnet&amp;#34; &amp;#34;clients&amp;#34;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;	subnet&lt;/span&gt;       &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;10.20.0.0/24&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;	auto_collect&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The repaired declaration made every client-visible option explicit. This is a
simplified version of the QUANTUM subnet, whose gateway and resolver are
&lt;code&gt;10.100.30.1&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;4
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;5
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;6
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;7
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-hcl&#34; data-lang=&#34;hcl&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;resource&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;firewall_dhcp_subnet&amp;#34; &amp;#34;clients&amp;#34;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;	subnet&lt;/span&gt;       &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;10.100.30.0/24&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;	auto_collect&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;	routers&lt;/span&gt;     &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;10.100.30.1&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;	dns_servers&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;10.100.30.1&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;	ntp_servers&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;10.100.30.1&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;figure class=&#34;article-diagram&#34;&gt;
    &lt;pre class=&#34;mermaid&#34;&gt;
flowchart LR
		CLIENT[QUANTUM client] --&gt;|DHCP Discover| KEA[OPNsense Kea]
		KEA --&gt;|Offer: address only| CLIENT
		CLIENT --&gt; IP[Client has a 10.100.30.x address]
		CLIENT -. missing .-&gt; GW[Default gateway 10.100.30.1]
		CLIENT -. missing .-&gt; DNS[DNS server 10.100.30.1]
		IP --&gt; SYMPTOM[Looks connected but cannot route or resolve]
&lt;/pre&gt;
    &lt;figcaption&gt;The DHCP daemon stayed healthy while `auto_collect` removed the information clients needed to use their leases.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The important thing here is that the provider default did not match the existing
OPNsense behavior. I now set every client-visible DHCP option explicitly.&lt;/p&gt;
&lt;p&gt;After restoring the option data, I verified DHCP as a client would:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;obtain a new lease;&lt;/li&gt;
&lt;li&gt;inspect the offered router and DNS options;&lt;/li&gt;
&lt;li&gt;reach the gateway;&lt;/li&gt;
&lt;li&gt;resolve a name;&lt;/li&gt;
&lt;li&gt;cross the firewall to an external address.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;ldquo;The service is running&amp;rdquo; would not have caught this failure. DHCP was running
perfectly while handing out incomplete leases.&lt;/p&gt;
&lt;h2 id=&#34;importing-omada-configuration&#34;&gt;Importing Omada configuration
&lt;/h2&gt;&lt;p&gt;The managed-switch controller added another translation layer. The API endpoint
behind the normal reverse-proxy address redirected login requests, while the
provider expected to talk directly to the controller. Connecting to the direct
management origin fixed authentication.&lt;/p&gt;
&lt;p&gt;Imports then showed several values whose controller defaults differed from the
provider defaults: multicast snooping, relay booleans, and profile flags. To
reach a zero-diff plan, I had to write values that the GUI had previously left
implicit.&lt;/p&gt;
&lt;p&gt;Wireless credentials were particularly important. The controller returned a
non-null pre-shared key. Omitting the field in code did not mean &amp;ldquo;leave it alone&amp;rdquo;;
it meant &amp;ldquo;clear it.&amp;rdquo; The secret therefore had to be supplied at runtime from an
encrypted source so the plan could preserve the live network without committing
the key.&lt;/p&gt;
&lt;p&gt;Hardware controls deserve the same caution. On this controller, Power over
Ethernet belonged to a port profile. Applying a profile with PoE disabled to a
live access point would cut power to the device carrying the management traffic.
I treated profile changes as physical operations, not harmless metadata edits.&lt;/p&gt;
&lt;h2 id=&#34;firewall-rules-and-their-real-order&#34;&gt;Firewall rules and their real order
&lt;/h2&gt;&lt;p&gt;Firewall filters were the highest-risk phase because the appliance had two rule
stores:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;legacy rules created in the traditional per-interface GUI;&lt;/li&gt;
&lt;li&gt;automation rules created through the API and managed by OpenTofu.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The provider could not import legacy rules because they were not the same kind of
object. They had to be recreated in the automation store.&lt;/p&gt;
&lt;p&gt;That raised a more important question than whether the declarations looked
equivalent: where would the new rules land in the effective packet-filter order?&lt;/p&gt;
&lt;p&gt;Firewall evaluation is ordered. Two identical sets of rules can behave
differently if a broad pass or block moves above a specific exception. The GUI&amp;rsquo;s
visual order was not enough because it separated the two stores.&lt;/p&gt;
&lt;p&gt;I found an API endpoint that returned the compiled packet-filter rules in actual
evaluation order, including labels that distinguished automation objects from
legacy objects. I wrapped it in a small read-only script and made its output a
mandatory gate for every interface migration.&lt;/p&gt;
&lt;figure class=&#34;article-diagram&#34;&gt;
    &lt;pre class=&#34;mermaid&#34;&gt;
flowchart TB
	TF[OpenTofu resources] --&gt; AUTO[os-firewall Automation store]
	GUI[Existing GUI rules] --&gt; LEGACY[Legacy interface store]
	AUTO --&gt; COMPILE[OPNsense rule compiler]
	LEGACY --&gt; COMPILE
	SYSTEM[Anti-lockout and generated rules] --&gt; COMPILE
	COMPILE --&gt; PF[Effective pf rules in @N order]
	PF --&gt; CHECK[pf-rule-order.sh verification]
	CHECK --&gt;|Automation safely shadows legacy| REMOVE[Remove legacy twin]
	CHECK --&gt;|Unexpected order| STOP[Stop and repair sequence]
&lt;/pre&gt;
    &lt;figcaption&gt;OPNsense displayed legacy and Automation rules separately, so I queried the compiled pf order before removing any legacy rule.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The sequence per interface became:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Recreate a small set of legacy rules as automation resources.&lt;/li&gt;
&lt;li&gt;Apply them while the legacy originals remain enabled.&lt;/li&gt;
&lt;li&gt;Query the compiled ruleset.&lt;/li&gt;
&lt;li&gt;Confirm the automation rules sit in the intended order and shadow the legacy
copies safely.&lt;/li&gt;
&lt;li&gt;Test traffic through that interface.&lt;/li&gt;
&lt;li&gt;Disable, then remove, the legacy copies.&lt;/li&gt;
&lt;li&gt;Plan again and confirm no unexpected drift.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I started with a low-risk guest network containing only three rules. It was a
canary for the ordering model. Only after its compiled order and behavior were
correct did I migrate management, server, VPN, and WAN interfaces one at a time.&lt;/p&gt;
&lt;p&gt;Explicit sequence values were essential. Relying on every resource&amp;rsquo;s default
sequence created ties and non-deterministic placement. I reserved sequence ranges
per interface so both humans and the provider had one stable ordering model.&lt;/p&gt;
&lt;h2 id=&#34;disabled-rules-can-still-block-deletion&#34;&gt;Disabled rules can still block deletion
&lt;/h2&gt;&lt;p&gt;One migration exposed another appliance quirk. A disabled legacy rule still
referenced an alias, and that reference prevented OpenTofu from deleting the
alias. From an operator&amp;rsquo;s perspective the rule was inactive. From the appliance&amp;rsquo;s
validation perspective it still existed.&lt;/p&gt;
&lt;p&gt;The fix was to remove the obsolete legacy rule, not merely disable it.&lt;/p&gt;
&lt;p&gt;This is why I avoided bulk cleanup during adoption. Relationships that do not
affect packet evaluation can still affect schema validation and deletion order.&lt;/p&gt;
&lt;h2 id=&#34;migrating-nat-separately&#34;&gt;Migrating NAT separately
&lt;/h2&gt;&lt;p&gt;Filter rules and NAT rules may appear together in the GUI, but they are not the
same ownership boundary. Some legacy firewall rules carried an association to a
generated NAT rule that the provider could not preserve. I migrated NAT in a
later phase after filter behavior was stable.&lt;/p&gt;
&lt;p&gt;The NAT provider also had schema gaps: some labels were unavailable, some port
fields rejected aliases, and protocol values normalized differently from the
appliance. These limitations did not invalidate the whole migration. They defined
which details stayed appliance-managed and which needed a different expression.&lt;/p&gt;
&lt;p&gt;I left settings in the GUI when the provider could not safely read and write
them. It is better to document that boundary than force an incomplete resource
to own it.&lt;/p&gt;
&lt;h2 id=&#34;secrets-and-state&#34;&gt;Secrets and state
&lt;/h2&gt;&lt;p&gt;An API-driven firewall migration touches credentials in several places:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;firewall API keys;&lt;/li&gt;
&lt;li&gt;VPN static keys and certificates;&lt;/li&gt;
&lt;li&gt;wireless pre-shared keys;&lt;/li&gt;
&lt;li&gt;remote-state access credentials.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I kept secrets encrypted outside the HCL and injected them into provider or
resource variables only for the command that needed them. That keeps plaintext
out of source files, but it does not automatically keep secrets out of state.
Provider schemas may still serialize sensitive values into the state backend.&lt;/p&gt;
&lt;p&gt;The state backend therefore needs the same protection as the firewall backup:
access control, encryption, and a tested recovery procedure. If the backend has
no locking, only one writer can safely apply at a time.&lt;/p&gt;
&lt;p&gt;Import declarations are worth retaining as disaster-recovery documentation. A
state loss otherwise also loses the mapping between stable resource names and
opaque appliance UUIDs.&lt;/p&gt;
&lt;h2 id=&#34;checks-used-for-each-resource-type&#34;&gt;Checks used for each resource type
&lt;/h2&gt;&lt;p&gt;For every new resource family, I now ask:&lt;/p&gt;
&lt;h3 id=&#34;before-import&#34;&gt;Before import
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Does the provider read and write the same API representation?&lt;/li&gt;
&lt;li&gt;Which live fields are absent from the schema?&lt;/li&gt;
&lt;li&gt;Which provider defaults differ from appliance defaults?&lt;/li&gt;
&lt;li&gt;Can this resource interrupt the management path, power, DHCP, DNS, or WAN?&lt;/li&gt;
&lt;li&gt;Is there a read-only way to inspect the compiled/effective result?&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;before-the-first-apply&#34;&gt;Before the first apply
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Is the plan a no-op after import?&lt;/li&gt;
&lt;li&gt;Are secret values present at runtime but absent from source?&lt;/li&gt;
&lt;li&gt;Is the first apply limited to one object or one low-risk segment?&lt;/li&gt;
&lt;li&gt;Is there an independent management path and a rollback artifact?&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;after-apply&#34;&gt;After apply
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Did a real client receive the expected service, not merely a green status?&lt;/li&gt;
&lt;li&gt;Does the compiled firewall order match the intended order?&lt;/li&gt;
&lt;li&gt;Did the appliance preserve fields the provider does not expose?&lt;/li&gt;
&lt;li&gt;Does a second plan return to no changes?&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;final-setup&#34;&gt;Final setup
&lt;/h2&gt;&lt;p&gt;Not every OPNsense setting is managed by OpenTofu. Some remain in the GUI because
the provider cannot represent or write them safely. What I have now is a clear
list of which tool owns each resource and a repeatable process:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;import live state;&lt;/li&gt;
&lt;li&gt;insist on zero drift;&lt;/li&gt;
&lt;li&gt;change one boundary at a time;&lt;/li&gt;
&lt;li&gt;inspect the effective system, not just the tool&amp;rsquo;s model;&lt;/li&gt;
&lt;li&gt;preserve a way back in.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The main rule is to get a no-change plan after import and then apply one small
change. Also verify from a real client. In the DHCP incident, the daemon was
healthy and the apply succeeded, but clients received leases without a gateway
or DNS server.&lt;/p&gt;
</description>
        </item>
        
    </channel>
</rss>
