<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Firewall on Adur</title><link>https://adurrr.github.io/en/tags/firewall/</link><description>Recent content in Firewall on Adur</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Sat, 11 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://adurrr.github.io/en/tags/firewall/index.xml" rel="self" type="application/rss+xml"/><item><title>OPNsense: Auditing, Automation, and Advanced Practices</title><link>https://adurrr.github.io/en/p/opnsense-auditing-automation-and-advanced-practices/</link><pubDate>Sat, 11 Apr 2026 00:00:00 +0000</pubDate><guid>https://adurrr.github.io/en/p/opnsense-auditing-automation-and-advanced-practices/</guid><description>&lt;h2 id="where-we-stand"&gt;Where We Stand
&lt;/h2&gt;&lt;p&gt;In the first two posts of the series, we set up OPNsense from scratch and took it to a configuration that is no longer trivial. Let&amp;rsquo;s do a quick recap before continuing.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Layer&lt;/th&gt;
 &lt;th&gt;What Was Configured&lt;/th&gt;
 &lt;th&gt;Post&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Hardware&lt;/td&gt;
 &lt;td&gt;Mini PC with Intel N100/N200, Intel i226-V NICs&lt;/td&gt;
 &lt;td&gt;First&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Connectivity&lt;/td&gt;
 &lt;td&gt;PPPoE on WAN, bridge on LAN, wireless interface&lt;/td&gt;
 &lt;td&gt;First&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Detection&lt;/td&gt;
 &lt;td&gt;Suricata IDS/IPS with ET Open, Abuse.ch, Feodo rulesets&lt;/td&gt;
 &lt;td&gt;First&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Shared Intelligence&lt;/td&gt;
 &lt;td&gt;CrowdSec with firewall bouncer and community collections&lt;/td&gt;
 &lt;td&gt;First&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;VPN&lt;/td&gt;
 &lt;td&gt;WireGuard with configured peers&lt;/td&gt;
 &lt;td&gt;First&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Firewall Rules&lt;/td&gt;
 &lt;td&gt;Basic rules for LAN, WireGuard, and WAN&lt;/td&gt;
 &lt;td&gt;First&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Offloading&lt;/td&gt;
 &lt;td&gt;Checksum, TSO, TCP buffer tuning&lt;/td&gt;
 &lt;td&gt;First&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Users&lt;/td&gt;
 &lt;td&gt;Dedicated admin with OTP, restricted root, protected WebUI&lt;/td&gt;
 &lt;td&gt;First&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Backups&lt;/td&gt;
 &lt;td&gt;AES-256-CBC encrypted, automatic, external storage&lt;/td&gt;
 &lt;td&gt;Second&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;DPI&lt;/td&gt;
 &lt;td&gt;Zenarmor with per-interface and category policies&lt;/td&gt;
 &lt;td&gt;Second&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Segmentation&lt;/td&gt;
 &lt;td&gt;5 VLANs (Main, Guests, IoT, Servers, Management) with inter-VLAN rules&lt;/td&gt;
 &lt;td&gt;Second&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;SSH&lt;/td&gt;
 &lt;td&gt;ed25519 keys only, non-standard port, IP-restricted access&lt;/td&gt;
 &lt;td&gt;Second&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;DNS&lt;/td&gt;
 &lt;td&gt;Unbound with DNS over TLS to Quad9 and Cloudflare&lt;/td&gt;
 &lt;td&gt;Second&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Logging&lt;/td&gt;
 &lt;td&gt;Remote syslog with TCP/TLS to Grafana+Loki or ELK&lt;/td&gt;
 &lt;td&gt;Second&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;It&amp;rsquo;s a solid configuration. But so far everything has been done manually, through the web interface, without a formal review process or a way to reproduce the configuration if something breaks beyond the XML backup. This post covers what&amp;rsquo;s missing: advanced practices, automation, a serious auditing framework, and a comparison with alternatives to make informed decisions.&lt;/p&gt;
&lt;h2 id="security-posture-review"&gt;Security Posture Review
&lt;/h2&gt;&lt;h3 id="defense-in-depth-what-we-have"&gt;Defense in Depth: What We Have
&lt;/h3&gt;&lt;p&gt;If we look at what we&amp;rsquo;ve configured as defense layers, the architecture has some depth:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Defense Layer&lt;/th&gt;
 &lt;th&gt;OPNsense Component&lt;/th&gt;
 &lt;th&gt;Current State&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Perimeter&lt;/td&gt;
 &lt;td&gt;WAN deny-all rules + incoming WireGuard&lt;/td&gt;
 &lt;td&gt;Functional&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Intrusion Detection&lt;/td&gt;
 &lt;td&gt;Suricata IPS with ET Open and Abuse.ch&lt;/td&gt;
 &lt;td&gt;Functional, default tuning&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Log Analysis&lt;/td&gt;
 &lt;td&gt;CrowdSec bouncer + community intelligence&lt;/td&gt;
 &lt;td&gt;Functional&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Application Inspection&lt;/td&gt;
 &lt;td&gt;Zenarmor DPI with per-VLAN policies&lt;/td&gt;
 &lt;td&gt;Functional&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Segmentation&lt;/td&gt;
 &lt;td&gt;5 VLANs with deny-all inter-VLAN rules by default&lt;/td&gt;
 &lt;td&gt;Functional&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Access Control&lt;/td&gt;
 &lt;td&gt;Dedicated admin, OTP, SSH with ed25519 keys&lt;/td&gt;
 &lt;td&gt;Functional&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;DNS Encryption&lt;/td&gt;
 &lt;td&gt;Unbound with DoT to Quad9/Cloudflare&lt;/td&gt;
 &lt;td&gt;Functional&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Backups&lt;/td&gt;
 &lt;td&gt;Encrypted, automatic, external storage&lt;/td&gt;
 &lt;td&gt;Functional&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Five layers of detection and prevention, real segmentation, and reasonable access control. For a homelab or small office, it&amp;rsquo;s more than most have. But there are gaps.&lt;/p&gt;
&lt;h3 id="remaining-gaps"&gt;Remaining Gaps
&lt;/h3&gt;&lt;p&gt;Being honest, there are several things that haven&amp;rsquo;t been addressed that matter:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No geolocation filtering&lt;/strong&gt;. The entire planet can try to connect to exposed ports on WAN. Most automated attacks come from IP ranges with which you have no legitimate relationship.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No reverse proxy&lt;/strong&gt;. If self-hosted services are exposed (Nextcloud, Jellyfin), they go directly via NAT without TLS termination or application-level protection.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Suricata is at default configuration&lt;/strong&gt;. Rules are enabled, but performance parameters haven&amp;rsquo;t been tuned and irrelevant categories haven&amp;rsquo;t been removed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Everything has been configured manually&lt;/strong&gt;. If tomorrow OPNsense needs to be rebuilt from scratch, the XML backup is the only option. There are no playbooks, no version control of the configuration, no way to review what changed and when without opening the web interface history.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No formal audit cadence&lt;/strong&gt;. The second post mentioned a weekly/monthly/quarterly routine, but without a framework behind it, it&amp;rsquo;s easy for it to remain good intentions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No automated threat feeds&lt;/strong&gt; beyond Suricata rule updates. IP blocklists don&amp;rsquo;t update themselves.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The following sections cover these gaps.&lt;/p&gt;
&lt;h2 id="advanced-practices"&gt;Advanced Practices
&lt;/h2&gt;&lt;h3 id="geoip-blocking-with-maxmind"&gt;GeoIP Blocking with MaxMind
&lt;/h3&gt;&lt;p&gt;The idea is simple: if there&amp;rsquo;s no legitimate reason for a connection to come from certain countries, blocking those IP ranges reduces noise. It&amp;rsquo;s not a real security measure, because any attacker with a VPN bypasses it, but it does eliminate a significant amount of automated scans and brute force attacks.&lt;/p&gt;
&lt;p&gt;OPNsense uses MaxMind&amp;rsquo;s free GeoLite2 databases, which require an account.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Register a free account at MaxMind and generate a license key.&lt;/li&gt;
&lt;li&gt;In &lt;strong&gt;Firewall &amp;gt; Aliases &amp;gt; GeoIP settings&lt;/strong&gt;, enter the license key.&lt;/li&gt;
&lt;li&gt;Create a GeoIP type alias in &lt;strong&gt;Firewall &amp;gt; Aliases&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;Name: &lt;code&gt;GeoIP_Block&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Type: GeoIP&lt;/li&gt;
&lt;li&gt;Content: select the countries to block.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;In &lt;strong&gt;Firewall &amp;gt; Rules &amp;gt; WAN&lt;/strong&gt;, add a rule at the top:
&lt;ul&gt;
&lt;li&gt;Action: Block&lt;/li&gt;
&lt;li&gt;Source: &lt;code&gt;GeoIP_Block&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Destination: *&lt;/li&gt;
&lt;li&gt;Description: Geographic blocking&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;One warning: keep this list updated. MaxMind updates the databases weekly. Configure automatic update in the GeoIP settings so they don&amp;rsquo;t become obsolete.&lt;/p&gt;
&lt;h3 id="advanced-suricata-tuning"&gt;Advanced Suricata Tuning
&lt;/h3&gt;&lt;p&gt;OPNSense 26.1 includes Suricata 8, which improves multi-thread performance and adds support for new protocols. But the default configuration is conservative. If the hardware has headroom, tuning these parameters makes a difference.&lt;/p&gt;
&lt;p&gt;In &lt;strong&gt;Services &amp;gt; Intrusion Detection &amp;gt; Administration&lt;/strong&gt;, the advanced section allows configuring parameters not in the standard interface. The most relevant:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# /usr/local/opnsense/service/conf/actions.d/conf.d/&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Adjust based on available RAM (these values are for 8 GB)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Maximum memory for flow tracking&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;memcap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;256mb&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;hash-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;65536&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Maximum memory for TCP stream reconstruction&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;memcap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;512mb&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;reassembly.memcap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;256mb&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Ring buffer size for af-packet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;af-packet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;igb0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ring-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;30000&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;cluster-type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;cluster_flow&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&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;Beyond memory tuning, review the active rule categories. If there are no SCADA servers, exposed databases, or SMTP services on the network, disable those categories. Each active rule consumes CPU on every inspected packet.&lt;/p&gt;
&lt;p&gt;To identify the rules generating the most noise, analyze the EVE JSON log:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Top 10 alerts by SID in the last 24 hours&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat /var/log/suricata/eve.json &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; jq -r &lt;span class="s1"&gt;&amp;#39;select(.event_type==&amp;#34;alert&amp;#34;) | .alert.signature_id&amp;#39;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; sort &lt;span class="p"&gt;|&lt;/span&gt; uniq -c &lt;span class="p"&gt;|&lt;/span&gt; sort -rn &lt;span class="p"&gt;|&lt;/span&gt; head -10
&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;If a rule generates hundreds of daily alerts with none being true positives, disable it or adjust its threshold.&lt;/p&gt;
&lt;h3 id="reverse-proxy-with-haproxy-and-acme"&gt;Reverse Proxy with HAProxy and ACME
&lt;/h3&gt;&lt;p&gt;If self-hosted services are exposed to the internet, doing it directly with port forwarding is functional but insecure. A reverse proxy allows terminating TLS with valid certificates, applying rate limiting, and having a centralized control point.&lt;/p&gt;
&lt;p&gt;Install the &lt;code&gt;os-haproxy&lt;/code&gt; and &lt;code&gt;os-acme-client&lt;/code&gt; plugins from &lt;strong&gt;System &amp;gt; Firmware &amp;gt; Plugins&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Certificates with Let&amp;rsquo;s Encrypt (ACME)&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;In &lt;strong&gt;Services &amp;gt; ACME Client &amp;gt; Accounts&lt;/strong&gt;, create an ACME account.&lt;/li&gt;
&lt;li&gt;In &lt;strong&gt;Services &amp;gt; ACME Client &amp;gt; Challenge Types&lt;/strong&gt;, configure a DNS-01 challenge. It&amp;rsquo;s preferred over HTTP-01 because it doesn&amp;rsquo;t require opening port 80 and supports wildcards.&lt;/li&gt;
&lt;li&gt;In &lt;strong&gt;Services &amp;gt; ACME Client &amp;gt; Certificates&lt;/strong&gt;, create certificates for each service.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;HAProxy Configuration&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;In &lt;strong&gt;Services &amp;gt; HAProxy &amp;gt; Real Servers&lt;/strong&gt;, create a backend for each internal service (Nextcloud at &lt;code&gt;192.168.40.10:443&lt;/code&gt;, Jellyfin at &lt;code&gt;192.168.40.11:8096&lt;/code&gt;, etc.).&lt;/li&gt;
&lt;li&gt;In &lt;strong&gt;Services &amp;gt; HAProxy &amp;gt; Rules &amp;amp; Checks &amp;gt; Conditions&lt;/strong&gt;, create conditions based on SNI or hostname.&lt;/li&gt;
&lt;li&gt;In &lt;strong&gt;Services &amp;gt; HAProxy &amp;gt; Virtual Services &amp;gt; Public Services&lt;/strong&gt;, create a frontend that listens on port 443, binds the ACME certificates, and routes to backends based on conditions.&lt;/li&gt;
&lt;li&gt;Enable HSTS in HTTP headers to force HTTPS.&lt;/li&gt;
&lt;li&gt;Configure rate limiting per IP to mitigate brute force attacks against login forms.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="threat-feeds-and-scheduled-aliases"&gt;Threat Feeds and Scheduled Aliases
&lt;/h3&gt;&lt;p&gt;IP blocklists lose value if they aren&amp;rsquo;t updated. OPNsense allows creating URL Table type aliases that are downloaded automatically.&lt;/p&gt;
&lt;p&gt;In &lt;strong&gt;Firewall &amp;gt; Aliases&lt;/strong&gt;, create aliases with these sources:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Alias&lt;/th&gt;
 &lt;th&gt;URL&lt;/th&gt;
 &lt;th&gt;Frequency&lt;/th&gt;
 &lt;th&gt;Description&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;Spamhaus_DROP&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;https://www.spamhaus.org/drop/drop.txt&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Daily&lt;/td&gt;
 &lt;td&gt;Hijacked ranges or used for spam&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;Spamhaus_EDROP&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;https://www.spamhaus.org/drop/edrop.txt&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Daily&lt;/td&gt;
 &lt;td&gt;Extension of DROP&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;Abusech_Feodo&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;https://feodotracker.abuse.ch/downloads/ipblocklist.txt&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Every 6h&lt;/td&gt;
 &lt;td&gt;Banking botnet IPs&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;Abusech_SSLBL&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;https://sslbl.abuse.ch/blacklist/sslipblacklist.txt&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Every 6h&lt;/td&gt;
 &lt;td&gt;IPs with malicious certificates&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Apply these aliases as source in blocking rules on WAN. URL Table type aliases update automatically according to the configured frequency.&lt;/p&gt;
&lt;h3 id="zfs-snapshots-for-rollback"&gt;ZFS Snapshots for Rollback
&lt;/h3&gt;&lt;p&gt;If ZFS was chosen as the filesystem during installation (instead of UFS), one of its best features can be used: instant snapshots with rollback.&lt;/p&gt;
&lt;p&gt;Before any important change (firmware update, massive rule change, plugin installation), create a snapshot:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Create a snapshot before updating&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zfs snapshot zroot/ROOT/default@pre-update-&lt;span class="k"&gt;$(&lt;/span&gt;date +%Y%m%d&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# List existing snapshots&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zfs list -t snapshot
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# If something goes wrong, rollback to the previous snapshot&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zfs rollback zroot/ROOT/default@pre-update-20260413
&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 returns the entire filesystem to the snapshot state in seconds. It&amp;rsquo;s insurance against failed updates that complements XML configuration backups. The snapshot recovers everything; the XML backup only recovers the configuration.&lt;/p&gt;
&lt;h2 id="infrastructure-as-code-for-opnsense"&gt;Infrastructure as Code for OPNsense
&lt;/h2&gt;&lt;p&gt;Doing everything from the web interface works, but it has known problems: there&amp;rsquo;s no readable change history, there&amp;rsquo;s no way to review what was modified before applying it, and rebuilding the configuration requires following a step-by-step guide or restoring an opaque backup.&lt;/p&gt;
&lt;h3 id="opnsense-rest-api"&gt;OPNsense REST API
&lt;/h3&gt;&lt;p&gt;OPNsense exposes a fairly complete REST API. The documentation is in &lt;code&gt;/api/&lt;/code&gt; and covers most web interface functionalities: aliases, firewall rules, interface configuration, IDS, CrowdSec, Unbound, HAProxy, and more.&lt;/p&gt;
&lt;p&gt;To use the API, create a key/secret pair in &lt;strong&gt;System &amp;gt; Access &amp;gt; Users&lt;/strong&gt;, select the administrator user, and generate an API key. OPNsense generates a file with the key and secret.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Query firewall aliases&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -k -u &lt;span class="s2"&gt;&amp;#34;API_KEY:API_SECRET&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; https://192.168.1.1/api/firewall/alias/searchItem
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Create a new alias&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -k -u &lt;span class="s2"&gt;&amp;#34;API_KEY:API_SECRET&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -X POST &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -H &lt;span class="s2"&gt;&amp;#34;Content-Type: application/json&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -d &lt;span class="s1"&gt;&amp;#39;{&amp;#34;alias&amp;#34;:{&amp;#34;name&amp;#34;:&amp;#34;test_alias&amp;#34;,&amp;#34;type&amp;#34;:&amp;#34;host&amp;#34;,&amp;#34;content&amp;#34;:&amp;#34;10.0.0.1&amp;#34;}}&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; https://192.168.1.1/api/firewall/alias/addItem
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Apply pending changes to the firewall&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -k -u &lt;span class="s2"&gt;&amp;#34;API_KEY:API_SECRET&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -X POST &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; https://192.168.1.1/api/firewall/alias/reconfigure
&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 API doesn&amp;rsquo;t cover 100% of the web interface. Some plugin functions (like parts of Zenarmor) aren&amp;rsquo;t exposed. But for firewall management, aliases, rules, interfaces, and most core services, it&amp;rsquo;s sufficient.&lt;/p&gt;
&lt;h3 id="ansible-ansibleguycollection_opnsense"&gt;Ansible: ansibleguy/collection_opnsense
&lt;/h3&gt;&lt;p&gt;The &lt;code&gt;ansibleguy.opnsense&lt;/code&gt; collection is the most mature option for managing OPNsense as code in 2026. It wraps the REST API in idempotent Ansible modules.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Install the collection&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ansible-galaxy collection install ansibleguy.opnsense
&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;An example playbook managing aliases and firewall rules:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nn"&gt;---&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Configure OPNsense firewall&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;hosts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;opnsense&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;httpapi&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;vars&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ansible_httpapi_port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;443&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ansible_httpapi_use_ssl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ansible_httpapi_validate_certs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Create RFC1918 alias&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ansibleguy.opnsense.alias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;RFC1918&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;network&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;10.0.0.0/8&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;172.16.0.0/12&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;192.168.0.0/16&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;RFC1918 private networks&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Create admin alias&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ansibleguy.opnsense.alias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Admin_IPs&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;host&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;192.168.50.10&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;192.168.50.11&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Admin IPs&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Block IoT to internal networks&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ansibleguy.opnsense.rule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;opt3&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# VLAN 30 IoT&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;block&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;source_net&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;IoT net&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;destination_net&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;RFC1918&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;IoT without access to internal networks&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&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;Ansible supports check mode (&lt;code&gt;--check&lt;/code&gt;) to see what would change without applying anything. This is especially useful for reviewing firewall changes before executing them, something the web interface doesn&amp;rsquo;t allow.&lt;/p&gt;
&lt;p&gt;The main limitation is that not all OPNsense modules are covered by the collection. Services like Zenarmor, CrowdSec, or advanced Suricata configurations may require direct API calls using Ansible&amp;rsquo;s &lt;code&gt;uri&lt;/code&gt; module.&lt;/p&gt;
&lt;h3 id="version-control-of-configxml"&gt;Version Control of config.xml
&lt;/h3&gt;&lt;p&gt;The most straightforward approach to have configuration under version control: export the &lt;code&gt;config.xml&lt;/code&gt;, encrypt it, and store it in a private Git repository. It&amp;rsquo;s what&amp;rsquo;s already done with the backups from the second post, but integrated into a Git workflow.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# export_config.sh - Run from cron or manually before changes&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;BACKUP_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;/root/config-backups&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;REPO_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;/root/opnsense-config&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;DATE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;date +%Y%m%d_%H%M&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Export current configuration&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cp /conf/config.xml &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BACKUP_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/config_&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DATE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.xml&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Encrypt with age (simpler than OpenSSL for this use)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;age -r age1publickey... &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -o &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REPO_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/config_&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DATE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.xml.age&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BACKUP_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/config_&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DATE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.xml&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Clean up unencrypted file&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BACKUP_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/config_&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DATE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.xml&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Commit to repository&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REPO_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git add .
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git commit -m &lt;span class="s2"&gt;&amp;#34;config: backup &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DATE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git push origin main
&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;With this, you have a change history of the configuration with timestamps and the ability to do &lt;code&gt;git diff&lt;/code&gt; between encrypted versions (or decrypt two versions and compare them with &lt;code&gt;diff&lt;/code&gt;). It&amp;rsquo;s not as clean as the VyOS model where configuration is plain text, but it works.&lt;/p&gt;
&lt;h3 id="cicd-pipeline-for-validation"&gt;CI/CD Pipeline for Validation
&lt;/h3&gt;&lt;p&gt;Taking automation one step further: validate configuration changes before applying them. A lightweight pipeline in GitLab CI or GitHub Actions that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Decrypts the &lt;code&gt;config.xml&lt;/code&gt; in an ephemeral environment.&lt;/li&gt;
&lt;li&gt;Validates the XML structure with &lt;code&gt;xmllint&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Checks anti-patterns with custom rules (rules with &lt;code&gt;source=any destination=any action=pass&lt;/code&gt;, users without OTP, unnecessary services enabled).&lt;/li&gt;
&lt;li&gt;Runs the Ansible playbook in check mode against a staging environment (if it exists) or just validates the syntax.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This doesn&amp;rsquo;t replace manual testing, but it catches obvious errors before they reach production.&lt;/p&gt;
&lt;p&gt;In terms of IaC maturity, OPNsense is at an intermediate point:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Aspect&lt;/th&gt;
 &lt;th&gt;OPNsense&lt;/th&gt;
 &lt;th&gt;VyOS&lt;/th&gt;
 &lt;th&gt;OpenWrt&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;REST API&lt;/td&gt;
 &lt;td&gt;Complete&lt;/td&gt;
 &lt;td&gt;Complete&lt;/td&gt;
 &lt;td&gt;Limited (ubus/JSON-RPC)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Terraform&lt;/td&gt;
 &lt;td&gt;No mature provider&lt;/td&gt;
 &lt;td&gt;Official provider (Foltik/vyos)&lt;/td&gt;
 &lt;td&gt;No provider&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Ansible&lt;/td&gt;
 &lt;td&gt;ansibleguy.opnsense&lt;/td&gt;
 &lt;td&gt;vyos.vyos (official)&lt;/td&gt;
 &lt;td&gt;Community roles&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Config format&lt;/td&gt;
 &lt;td&gt;XML (config.xml)&lt;/td&gt;
 &lt;td&gt;Plain text CLI&lt;/td&gt;
 &lt;td&gt;UCI (plain text)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Git workflow&lt;/td&gt;
 &lt;td&gt;Manual or scripted export&lt;/td&gt;
 &lt;td&gt;Native (text config)&lt;/td&gt;
 &lt;td&gt;Manual export&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;VyOS wins at IaC by design: its configuration is plain text from day one, with atomic commits and native rollback. OPNsense compensates with a solid REST API and the Ansible collection, but requires more effort to reach the same level of automation.&lt;/p&gt;
&lt;h2 id="auditing-framework-iso-27001-and-ens"&gt;Auditing Framework: ISO 27001 and ENS
&lt;/h2&gt;&lt;h3 id="why-it-matters-even-for-a-homelab"&gt;Why It Matters Even for a Homelab
&lt;/h3&gt;&lt;p&gt;It&amp;rsquo;s tempting to think that a formal auditing framework is only for companies. But the reality is more pragmatic: an auditing framework is a checklist that people with more experience than you have already validated. Following it avoids the &amp;ldquo;I forgot to review X&amp;rdquo; problem that appears when the security routine depends only on memory.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s an additional reason if you&amp;rsquo;re in Spain: the National Security Scheme (ENS, Real Decreto 311/2022) is mandatory for the public sector and its technology providers&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;. This increasingly includes freelancers and small companies providing services to public administrations. Having the infrastructure aligned with the ENS, even at a basic level, isn&amp;rsquo;t just good practice—it&amp;rsquo;s a potential requirement.&lt;/p&gt;
&lt;h3 id="relevant-iso-270012022-controls"&gt;Relevant ISO 27001:2022 Controls
&lt;/h3&gt;&lt;p&gt;ISO 27001:2022 organizes security controls in Annex A&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;. The ones that apply directly to what was configured in this series:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Control&lt;/th&gt;
 &lt;th&gt;Description&lt;/th&gt;
 &lt;th&gt;OPNsense Implementation&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;A.8.20&lt;/td&gt;
 &lt;td&gt;Networks security&lt;/td&gt;
 &lt;td&gt;WAN deny-all firewall, IPS, CrowdSec, GeoIP&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;A.8.22&lt;/td&gt;
 &lt;td&gt;Networks segregation&lt;/td&gt;
 &lt;td&gt;5 VLANs with restrictive inter-VLAN rules&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;A.8.23&lt;/td&gt;
 &lt;td&gt;Web filtering&lt;/td&gt;
 &lt;td&gt;Zenarmor DPI with per-category policies&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;A.8.15&lt;/td&gt;
 &lt;td&gt;Logging&lt;/td&gt;
 &lt;td&gt;Remote syslog with TLS, Suricata EVE JSON&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;A.8.16&lt;/td&gt;
 &lt;td&gt;Monitoring activities&lt;/td&gt;
 &lt;td&gt;Suricata alerts, CrowdSec dashboards, Zenarmor&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;A.8.17&lt;/td&gt;
 &lt;td&gt;Clock synchronization&lt;/td&gt;
 &lt;td&gt;NTP configured in System &amp;gt; General (essential for log correlation)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;A.5.15&lt;/td&gt;
 &lt;td&gt;Access control&lt;/td&gt;
 &lt;td&gt;Least-privilege policy, dedicated admin&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;A.5.17&lt;/td&gt;
 &lt;td&gt;Authentication information&lt;/td&gt;
 &lt;td&gt;16+ character passwords, OTP enabled&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;A.5.18&lt;/td&gt;
 &lt;td&gt;Access rights&lt;/td&gt;
 &lt;td&gt;Periodic review of users and permissions&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;A.8.2&lt;/td&gt;
 &lt;td&gt;Privileged access&lt;/td&gt;
 &lt;td&gt;Restricted root, separate admin, SSH keys only&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The standard doesn&amp;rsquo;t prescribe exact review frequencies, but auditors expect: firewall rule review at least quarterly, privileged access review quarterly, and continuous log monitoring with weekly manual review.&lt;/p&gt;
&lt;h3 id="applicable-ens-measures"&gt;Applicable ENS Measures
&lt;/h3&gt;&lt;p&gt;The ENS (RD 311/2022) classifies systems into three levels: basic, medium, and high. The relevant measures for a firewall:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;ENS Measure&lt;/th&gt;
 &lt;th&gt;Description&lt;/th&gt;
 &lt;th&gt;Minimum Level&lt;/th&gt;
 &lt;th&gt;Implementation&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;mp.com.1&lt;/td&gt;
 &lt;td&gt;Secure perimeter&lt;/td&gt;
 &lt;td&gt;Medium&lt;/td&gt;
 &lt;td&gt;WAN firewall, GeoIP, deny-all by default&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;mp.com.2&lt;/td&gt;
 &lt;td&gt;Confidentiality protection&lt;/td&gt;
 &lt;td&gt;Medium&lt;/td&gt;
 &lt;td&gt;WireGuard VPN, DoT for DNS&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;mp.com.4&lt;/td&gt;
 &lt;td&gt;Networks segregation&lt;/td&gt;
 &lt;td&gt;Medium&lt;/td&gt;
 &lt;td&gt;VLANs with inter-VLAN rules&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;op.exp.2&lt;/td&gt;
 &lt;td&gt;Security configuration&lt;/td&gt;
 &lt;td&gt;Basic&lt;/td&gt;
 &lt;td&gt;SSH hardening, disabled services&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;op.exp.8&lt;/td&gt;
 &lt;td&gt;Activity logging&lt;/td&gt;
 &lt;td&gt;Basic&lt;/td&gt;
 &lt;td&gt;Remote syslog (2-year retention for medium/high)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;op.acc.1&lt;/td&gt;
 &lt;td&gt;Identification&lt;/td&gt;
 &lt;td&gt;Basic&lt;/td&gt;
 &lt;td&gt;Unique users, no shared accounts&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;op.acc.5&lt;/td&gt;
 &lt;td&gt;Authentication mechanism&lt;/td&gt;
 &lt;td&gt;Basic&lt;/td&gt;
 &lt;td&gt;Strong passwords; OTP for medium/high&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;op.acc.7&lt;/td&gt;
 &lt;td&gt;Remote access&lt;/td&gt;
 &lt;td&gt;Basic&lt;/td&gt;
 &lt;td&gt;WireGuard VPN mandatory for remote admin&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;op.mon.1&lt;/td&gt;
 &lt;td&gt;Intrusion detection&lt;/td&gt;
 &lt;td&gt;Medium&lt;/td&gt;
 &lt;td&gt;Suricata IPS + CrowdSec&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The CCN-STIC guides from the National Cryptology Center provide detailed implementation instructions. Specifically, CCN-STIC-811 for system interconnection and CCN-STIC-408 for perimeter security are the most relevant for this context&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;An important ENS detail: log retention for medium and high levels is a minimum of two years. If the remote syslog doesn&amp;rsquo;t have capacity for that, it needs to be planned. With Loki and compression, firewall logs from a homelab don&amp;rsquo;t take up much space, but it needs to be considered from the design phase.&lt;/p&gt;
&lt;h3 id="audit-calendar"&gt;Audit Calendar
&lt;/h3&gt;&lt;p&gt;Combining ISO 27001 and ENS recommendations with what&amp;rsquo;s realistic for a small environment:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Frequency&lt;/th&gt;
 &lt;th&gt;Task&lt;/th&gt;
 &lt;th&gt;Type&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Daily&lt;/td&gt;
 &lt;td&gt;Automated review of IDS/IPS alerts and CrowdSec decisions&lt;/td&gt;
 &lt;td&gt;Automated&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Weekly&lt;/td&gt;
 &lt;td&gt;Manual log review: blocked events, failed login attempts, anomalous traffic&lt;/td&gt;
 &lt;td&gt;Manual&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Monthly&lt;/td&gt;
 &lt;td&gt;Firewall rule review and alias update. Verify threat feeds are updating&lt;/td&gt;
 &lt;td&gt;Manual&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Quarterly&lt;/td&gt;
 &lt;td&gt;Complete rule review: identify rules with no hits, overly permissive rules, forgotten temporary rules&lt;/td&gt;
 &lt;td&gt;Manual&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Quarterly&lt;/td&gt;
 &lt;td&gt;Access review: active users, permissions, valid SSH keys, API tokens&lt;/td&gt;
 &lt;td&gt;Manual&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Semi-annual&lt;/td&gt;
 &lt;td&gt;Basic exposure test: &lt;code&gt;nmap&lt;/code&gt; from outside the network against the public IP&lt;/td&gt;
 &lt;td&gt;Manual&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Annual&lt;/td&gt;
 &lt;td&gt;Complete security posture review. Compare with previous year&amp;rsquo;s state&lt;/td&gt;
 &lt;td&gt;Manual&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;For automated daily review, a basic script that runs with cron:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# /root/scripts/daily_audit.sh&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Run with: crontab -e -&amp;gt; 0 7 * * * /root/scripts/daily_audit.sh&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;LOG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;/var/log/daily_audit_&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;date +%Y%m%d&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;.log&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;=== Daily audit &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;date&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; ===&amp;#34;&lt;/span&gt; &amp;gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Critical Suricata alerts in the last 24h&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; -e &lt;span class="s2"&gt;&amp;#34;\n--- Suricata alerts (severity 1) ---&amp;#34;&lt;/span&gt; &amp;gt;&amp;gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat /var/log/suricata/eve.json &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; jq -r &lt;span class="s1"&gt;&amp;#39;select(.event_type==&amp;#34;alert&amp;#34; and .alert.severity==1) | 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &amp;#34;\(.timestamp) \(.alert.signature) src=\(.src_ip) dst=\(.dest_ip)&amp;#34;&amp;#39;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; tail -50 &amp;gt;&amp;gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Active CrowdSec decisions&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; -e &lt;span class="s2"&gt;&amp;#34;\n--- CrowdSec active decisions ---&amp;#34;&lt;/span&gt; &amp;gt;&amp;gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cscli decisions list -o raw 2&amp;gt;/dev/null &lt;span class="p"&gt;|&lt;/span&gt; wc -l &amp;gt;&amp;gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Age of last backup&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; -e &lt;span class="s2"&gt;&amp;#34;\n--- Last backup ---&amp;#34;&lt;/span&gt; &amp;gt;&amp;gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;LAST_BACKUP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;ls -t /conf/backup/*.xml 2&amp;gt;/dev/null &lt;span class="p"&gt;|&lt;/span&gt; head -1&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; -n &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$LAST_BACKUP&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; stat -f &lt;span class="s2"&gt;&amp;#34;%Sm&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$LAST_BACKUP&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &amp;gt;&amp;gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;No backups found&amp;#34;&lt;/span&gt; &amp;gt;&amp;gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# System status&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; -e &lt;span class="s2"&gt;&amp;#34;\n--- System resources ---&amp;#34;&lt;/span&gt; &amp;gt;&amp;gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;CPU: &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;sysctl -n dev.cpu.0.temperature 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;N/A&amp;#39;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &amp;gt;&amp;gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;RAM: &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;sysctl -n vm.stats.vm.v_free_count&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &amp;gt;&amp;gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;States: &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;pfctl -si 2&amp;gt;/dev/null &lt;span class="p"&gt;|&lt;/span&gt; grep &lt;span class="s1"&gt;&amp;#39;current entries&amp;#39;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &amp;gt;&amp;gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Send via email or copy to syslog&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;logger -t daily_audit &lt;span class="s2"&gt;&amp;#34;Audit completed. See &lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&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;This script isn&amp;rsquo;t meant to be a SIEM. It&amp;rsquo;s a first line of automated review that flags if there&amp;rsquo;s something requiring attention. For serious log analysis, the Grafana + Loki combination or a dedicated Wazuh is appropriate.&lt;/p&gt;
&lt;h2 id="opnsense-vs-alternatives"&gt;OPNsense vs. Alternatives
&lt;/h2&gt;&lt;p&gt;Before continuing to invest time in OPNsense, it&amp;rsquo;s worth comparing with the other serious open source firewall/router options. Not to migrate now, but to know what&amp;rsquo;s out there and make informed decisions if needs change.&lt;/p&gt;
&lt;h3 id="vyos"&gt;VyOS
&lt;/h3&gt;&lt;p&gt;VyOS is a network operating system based on Debian with a CLI configuration model inspired by JunOS. It has no web interface.&lt;/p&gt;
&lt;p&gt;What it does better than OPNsense:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Plain text configuration&lt;/strong&gt;. VyOS config is a readable text file, with atomic commits and native rollback. It&amp;rsquo;s the IaC dream: &lt;code&gt;git diff&lt;/code&gt; works directly on the configuration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Official Terraform provider&lt;/strong&gt; (Foltik/vyos). Real declarative network infrastructure.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Advanced routing&lt;/strong&gt;. BGP, OSPF, IS-IS at scale. Supports routing tables with more than one million BGP prefixes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance&lt;/strong&gt;. The VPP dataplane enables 10 Gbps+ throughput with appropriate hardware.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cloud-native&lt;/strong&gt;. Direct deployment on AWS, Azure, and GCP with official support.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What it does worse:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No GUI&lt;/strong&gt;. Everything is CLI or API. The learning curve is steep if you don&amp;rsquo;t come from enterprise networking.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Limited integrated security&lt;/strong&gt;. There&amp;rsquo;s no equivalent to Suricata with GUI, no DPI like Zenarmor, no integrated CrowdSec. Suricata can be installed manually, but without the integration OPNsense offers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Paid LTS&lt;/strong&gt;. Since 2024, stable LTS images require a subscription. Rolling releases are free but without stability guarantee.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="openwrt"&gt;OpenWrt
&lt;/h3&gt;&lt;p&gt;OpenWrt is a Linux-based router operating system. Its strength is embedded hardware support.&lt;/p&gt;
&lt;p&gt;What it does better than OPNsense:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Massive hardware support&lt;/strong&gt;. Runs on more than 500 commercial router models, plus x86.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Native WiFi&lt;/strong&gt;. Directly manages wireless interfaces, with excellent driver support and advanced AP configuration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lightweight&lt;/strong&gt;. Can run on 128 MB RAM and 16 MB flash on embedded hardware.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Package ecosystem&lt;/strong&gt;. More than 27,000 packages available.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What it does worse:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Basic security&lt;/strong&gt;. nftables firewall without integrated IDS/IPS. Suricata and Snort packages are community-maintained, poorly maintained, and have performance problems on limited hardware.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No DPI&lt;/strong&gt;. There&amp;rsquo;s no equivalent to Zenarmor.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Limited IaC&lt;/strong&gt;. UCI is scriptable but there&amp;rsquo;s no Terraform provider or mature REST API.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Complicated updates&lt;/strong&gt;. On x86, updating OpenWrt means reinstalling and restoring configuration. OPNsense updates with one click.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="when-to-choose-each"&gt;When to Choose Each
&lt;/h3&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Criterion&lt;/th&gt;
 &lt;th&gt;OPNsense&lt;/th&gt;
 &lt;th&gt;VyOS&lt;/th&gt;
 &lt;th&gt;OpenWrt&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Integrated security&lt;/td&gt;
 &lt;td&gt;Excellent (Suricata, CrowdSec, Zenarmor)&lt;/td&gt;
 &lt;td&gt;Basic&lt;/td&gt;
 &lt;td&gt;Minimal&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;IaC and automation&lt;/td&gt;
 &lt;td&gt;Good (API + Ansible)&lt;/td&gt;
 &lt;td&gt;Excellent (Terraform + text config)&lt;/td&gt;
 &lt;td&gt;Limited (UCI)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;10G+ performance&lt;/td&gt;
 &lt;td&gt;Moderate&lt;/td&gt;
 &lt;td&gt;Excellent (VPP)&lt;/td&gt;
 &lt;td&gt;Not applicable&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Learning curve&lt;/td&gt;
 &lt;td&gt;Moderate (GUI)&lt;/td&gt;
 &lt;td&gt;Steep (CLI)&lt;/td&gt;
 &lt;td&gt;Moderate&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Cost&lt;/td&gt;
 &lt;td&gt;Free&lt;/td&gt;
 &lt;td&gt;Paid LTS, free rolling&lt;/td&gt;
 &lt;td&gt;Free&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Ideal use case&lt;/td&gt;
 &lt;td&gt;Perimeter firewall/UTM&lt;/td&gt;
 &lt;td&gt;Enterprise or cloud edge router&lt;/td&gt;
 &lt;td&gt;WiFi AP, lightweight gateway&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;For a homelab or small office where security is the priority, OPNsense remains the best option. The combination of Suricata, CrowdSec, and Zenarmor with a usable web interface has no equivalent among the alternatives.&lt;/p&gt;
&lt;p&gt;VyOS makes sense if the environment grows toward complex routing (multiple uplinks with BGP, SD-WAN) or if infrastructure is managed exclusively with Terraform.&lt;/p&gt;
&lt;p&gt;OpenWrt makes sense as a complement: a WiFi AP running OpenWrt behind an OPNsense is a solid combination. But as a perimeter firewall for security, it falls short.&lt;/p&gt;
&lt;h2 id="conclusions-and-next-steps"&gt;Conclusions and Next Steps
&lt;/h2&gt;&lt;p&gt;In three posts we&amp;rsquo;ve gone from a mini PC without an operating system to a segmented network with five VLANs, three detection layers (Suricata, CrowdSec, Zenarmor), VPN with WireGuard, encrypted DNS, automatic backups, automation with Ansible, and an auditing framework based on real standards.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not perfect. OPNsense&amp;rsquo;s IaC doesn&amp;rsquo;t reach VyOS&amp;rsquo;s level. Zenarmor has a licensing model that limits advanced features in the free version. And maintaining an audit routine requires discipline that&amp;rsquo;s easy to set aside when everything seems to work well.&lt;/p&gt;
&lt;p&gt;But it&amp;rsquo;s a solid foundation to keep building on. There are topics that were deliberately left out of this series because they deserve their own depth:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Wazuh SIEM integration&lt;/strong&gt;: correlation of Suricata, CrowdSec, and system logs in one place, with centralized alerts and dashboards. It&amp;rsquo;s the natural step for anyone wanting real security monitoring.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-WAN and failover&lt;/strong&gt;: configure two internet connections with load balancing and automatic failover. Relevant when availability matters.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Advanced HAProxy&lt;/strong&gt;: mutual TLS (mTLS) with client certificates, certificate authentication for internal services, OAuth2 as an authentication layer against self-hosted applications.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Network monitoring with NetFlow/Insight&lt;/strong&gt;: detailed traffic analysis by protocol, host, and port to detect anomalies that IDS doesn&amp;rsquo;t catch.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Complete automation with Terraform&lt;/strong&gt;: if OPNsense gets a mature Terraform provider (or if there&amp;rsquo;s a partial migration to VyOS for routing), declarative management of the entire network.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If there&amp;rsquo;s interest, a fourth post can cover Wazuh integration and advanced monitoring. That&amp;rsquo;s where the jump from &amp;ldquo;secure homelab&amp;rdquo; to &amp;ldquo;infrastructure with real visibility&amp;rdquo; becomes most evident.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;Real Decreto 311/2022, de 3 de mayo, por el que se regula el Esquema Nacional de Seguridad. BOE-A-2022-7191.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;ISO/IEC 27001:2022 — Information security, cybersecurity and privacy protection — Information security management systems — Requirements.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;The CCN-STIC guides from the National Cryptology Center provide detailed instructions for ENS implementation. Specifically, CCN-STIC-811 covers system interconnection and CCN-STIC-408 covers perimeter security.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>OPNsense: network segmentation, VLANs and hardening</title><link>https://adurrr.github.io/en/p/opnsense-network-segmentation-vlans-and-hardening/</link><pubDate>Fri, 03 Apr 2026 00:00:00 +0000</pubDate><guid>https://adurrr.github.io/en/p/opnsense-network-segmentation-vlans-and-hardening/</guid><description>&lt;h2 id="native-encrypted-backups"&gt;Native encrypted backups
&lt;/h2&gt;&lt;p&gt;Losing the OPNsense configuration after hours of tweaking is the kind of disaster that only happens once. After that incident, automatic backups are configured.&lt;/p&gt;
&lt;p&gt;OPNsense has a native backup system that exports all configuration to an XML file. Since version 24.1, these backups can be encrypted directly from the web interface.&lt;/p&gt;
&lt;h3 id="encrypted-backup-configuration"&gt;Encrypted backup configuration
&lt;/h3&gt;&lt;p&gt;In &lt;strong&gt;System &amp;gt; Configuration &amp;gt; Backups&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to the &lt;strong&gt;Google Drive / Nextcloud&lt;/strong&gt; section if you want remote backup, or stick with local backup.&lt;/li&gt;
&lt;li&gt;Check the &lt;strong&gt;Encrypt backup&lt;/strong&gt; box and enter an encryption password. This password is independent of system credentials. Store it in a password manager, because without it the backup is unrecoverable.&lt;/li&gt;
&lt;li&gt;In the automatic backup section (&lt;strong&gt;Scheduled&lt;/strong&gt;), configure the frequency. A daily backup is reasonable for most environments.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="manual-backup-from-the-interface"&gt;Manual backup from the interface
&lt;/h3&gt;&lt;p&gt;In &lt;strong&gt;System &amp;gt; Configuration &amp;gt; Backups&lt;/strong&gt;, the &lt;strong&gt;Download configuration&lt;/strong&gt; button generates an XML with the current configuration. If encryption was selected, the downloaded file will be encrypted with AES-256-CBC.&lt;/p&gt;
&lt;h3 id="backup-from-the-console"&gt;Backup from the console
&lt;/h3&gt;&lt;p&gt;To automate backups from the command line:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Export the configuration&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cp /conf/config.xml /root/backup_&lt;span class="k"&gt;$(&lt;/span&gt;date +%Y%m%d&lt;span class="k"&gt;)&lt;/span&gt;.xml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Encrypt with OpenSSL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;openssl enc -aes-256-cbc -salt -pbkdf2 &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -in /root/backup_&lt;span class="k"&gt;$(&lt;/span&gt;date +%Y%m%d&lt;span class="k"&gt;)&lt;/span&gt;.xml &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -out /root/backup_&lt;span class="k"&gt;$(&lt;/span&gt;date +%Y%m%d&lt;span class="k"&gt;)&lt;/span&gt;.xml.enc
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Remove the unencrypted file&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm /root/backup_&lt;span class="k"&gt;$(&lt;/span&gt;date +%Y%m%d&lt;span class="k"&gt;)&lt;/span&gt;.xml
&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;It is recommended to copy encrypted backups to external storage: a NAS, an S3 bucket, or even a private Git repository (the encrypted XML is small, a few hundred KB).&lt;/p&gt;
&lt;h3 id="restoration"&gt;Restoration
&lt;/h3&gt;&lt;p&gt;To restore, go to &lt;strong&gt;System &amp;gt; Configuration &amp;gt; Backups&lt;/strong&gt;, upload the file and, if encrypted, enter the password. OPNsense applies the configuration and restarts the affected services.&lt;/p&gt;
&lt;h2 id="static-ip-assignment"&gt;Static IP assignment
&lt;/h2&gt;&lt;p&gt;There are devices that need to always have the same IP: servers, NAS, printers, surveillance cameras. This can be done in two ways: configuring a static IP on the device itself or, which is cleaner, assigning DHCP reservations in OPNsense.&lt;/p&gt;
&lt;h3 id="dhcp-reservations-the-recommended-way"&gt;DHCP reservations (the recommended way)
&lt;/h3&gt;&lt;p&gt;In &lt;strong&gt;Services &amp;gt; DHCPv4 &amp;gt; [interface]&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to the &lt;strong&gt;DHCP Static Mappings&lt;/strong&gt; section.&lt;/li&gt;
&lt;li&gt;Add a new entry with:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MAC Address&lt;/strong&gt;: the MAC address of the device.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IP Address&lt;/strong&gt;: the IP you want to always assign.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hostname&lt;/strong&gt;: a descriptive name.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The advantage of doing it this way is that management is centralized in OPNsense. If you change routers tomorrow, devices don&amp;rsquo;t need to be reconfigured.&lt;/p&gt;
&lt;h3 id="range-convention"&gt;Range convention
&lt;/h3&gt;&lt;p&gt;A convention that works well for organizing the network:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Range&lt;/th&gt;
 &lt;th&gt;Usage&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;.1&lt;/td&gt;
 &lt;td&gt;Gateway (OPNsense)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;.2 - .19&lt;/td&gt;
 &lt;td&gt;Infrastructure (switches, APs, NAS)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;.20 - .49&lt;/td&gt;
 &lt;td&gt;Servers and services&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;.50 - .99&lt;/td&gt;
 &lt;td&gt;Fixed IP devices (printers, cameras)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;.100 - .254&lt;/td&gt;
 &lt;td&gt;Dynamic DHCP pool&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This means that just by seeing a device&amp;rsquo;s IP, you already know which category it falls into.&lt;/p&gt;
&lt;h2 id="zenarmor-sensei"&gt;Zenarmor (Sensei)
&lt;/h2&gt;&lt;p&gt;Zenarmor is a deep packet inspection (DPI) plugin for OPNsense. It goes beyond what Suricata or CrowdSec do because it inspects traffic at the application level: it can distinguish between Netflix and YouTube, between Telegram and WhatsApp, between legitimate traffic and potentially dangerous applications.&lt;/p&gt;
&lt;h3 id="what-it-does-exactly"&gt;What it does exactly
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Application-based traffic classification&lt;/strong&gt;: identifies more than 300 applications and protocols.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Content filtering by categories&lt;/strong&gt;: allows blocking entire categories (gambling, malware, adult content) without needing to maintain lists manually.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Encrypted traffic analysis (TLS)&lt;/strong&gt;: Zenarmor can classify HTTPS traffic without decrypting it, using metadata like SNI, JA3 fingerprints, and connection patterns.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Detailed reporting&lt;/strong&gt;: dashboards with consumption by device, application, and category.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="installation"&gt;Installation
&lt;/h3&gt;&lt;p&gt;In &lt;strong&gt;System &amp;gt; Firmware &amp;gt; Plugins&lt;/strong&gt;, search for &lt;code&gt;os-sunnyvalley&lt;/code&gt; and install. After installation, Zenarmor appears in the main menu.&lt;/p&gt;
&lt;p&gt;When starting Zenarmor for the first time, a configuration wizard runs:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Deployment mode&lt;/strong&gt;: choose &lt;strong&gt;Routed Mode&lt;/strong&gt; to inspect all traffic passing through OPNsense. &lt;strong&gt;Bridge&lt;/strong&gt; mode is for specific cases.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Database engine&lt;/strong&gt;: Zenarmor uses a local database for logs. For modest hardware (N100), select &lt;strong&gt;SQLite&lt;/strong&gt;. For more powerful hardware, &lt;strong&gt;Elasticsearch&lt;/strong&gt; gives better query performance.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interfaces to protect&lt;/strong&gt;: select the LAN interfaces you want to inspect.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Default policy&lt;/strong&gt;: start with a permissive policy (monitor only) and adjust after seeing actual traffic.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="policy-configuration"&gt;Policy configuration
&lt;/h3&gt;&lt;p&gt;In &lt;strong&gt;Zenarmor &amp;gt; Policies&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;Policies are applied by interface or by device group. A reasonable configuration:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;General policy (main LAN)&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Block categories: Malware, Phishing, Cryptomining, C2 (Command &amp;amp; Control).&lt;/li&gt;
&lt;li&gt;Monitor but allow: Streaming, Social Media, Gaming.&lt;/li&gt;
&lt;li&gt;Allow everything else.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Policy for IoT&lt;/strong&gt; (we&amp;rsquo;ll create this with VLANs later):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Block everything except the necessary domains for each device.&lt;/li&gt;
&lt;li&gt;IoT devices should not be able to access the internet freely.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Policy for guests&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Block: P2P, Tor, VPN (to avoid filter bypass).&lt;/li&gt;
&lt;li&gt;Limit bandwidth per device.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="difference-with-suricata-and-crowdsec"&gt;Difference with Suricata and CrowdSec
&lt;/h3&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Feature&lt;/th&gt;
 &lt;th&gt;Suricata (IDS/IPS)&lt;/th&gt;
 &lt;th&gt;CrowdSec&lt;/th&gt;
 &lt;th&gt;Zenarmor&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Inspection&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;Packets and signatures&lt;/td&gt;
 &lt;td&gt;Logs and patterns&lt;/td&gt;
 &lt;td&gt;Application (DPI)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;What it detects&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;Exploits, malware, C2&lt;/td&gt;
 &lt;td&gt;Brute force, scans&lt;/td&gt;
 &lt;td&gt;Applications, categories&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Blocking&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;By signature/rule&lt;/td&gt;
 &lt;td&gt;By IP (temp ban)&lt;/td&gt;
 &lt;td&gt;By application/category&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Main resource&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;CPU (high)&lt;/td&gt;
 &lt;td&gt;CPU (low)&lt;/td&gt;
 &lt;td&gt;CPU (medium) + RAM&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Complementary&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;Yes&lt;/td&gt;
 &lt;td&gt;Yes&lt;/td&gt;
 &lt;td&gt;Yes&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;All three complement each other. Suricata looks for known threats in traffic. CrowdSec detects malicious behavior in logs and shares intelligence. Zenarmor classifies and filters at the application level. Using them together gives security coverage that is hard to beat on home equipment.&lt;/p&gt;
&lt;h2 id="disable-insecure-ssh"&gt;Disable insecure SSH
&lt;/h2&gt;&lt;p&gt;SSH is the usual way to access the OPNsense console remotely. But the default configuration has aspects that should be changed.&lt;/p&gt;
&lt;h3 id="secure-ssh-configuration"&gt;Secure SSH configuration
&lt;/h3&gt;&lt;p&gt;In &lt;strong&gt;System &amp;gt; Settings &amp;gt; Administration&lt;/strong&gt;, SSH section:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Disable root login via SSH&lt;/strong&gt;. Create a specific user with SSH access and sudo permissions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Change the default port&lt;/strong&gt;. Port 22 is the first one bots scan. Change to a high port (for example, 2222 or something less predictable).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Disable password authentication&lt;/strong&gt;. Use exclusively SSH keys:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# On your local machine, generate a key pair if you don&amp;#39;t have one&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh-keygen -t ed25519 -C &lt;span class="s2"&gt;&amp;#34;opnsense-admin&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Copy the public key&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat ~/.ssh/id_ed25519.pub
&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;ol start="4"&gt;
&lt;li&gt;Paste the public key in the user&amp;rsquo;s profile at &lt;strong&gt;System &amp;gt; Access &amp;gt; Users &amp;gt; [user] &amp;gt; Authorized Keys&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;In the SSH configuration, uncheck &lt;strong&gt;Permit Password Login&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="ssh-access-restriction-by-firewall"&gt;SSH access restriction by firewall
&lt;/h3&gt;&lt;p&gt;Create a firewall rule that only allows SSH from specific IPs:&lt;/p&gt;
&lt;p&gt;In &lt;strong&gt;Firewall &amp;gt; Rules &amp;gt; LAN&lt;/strong&gt;, create a rule:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Action: Pass&lt;/li&gt;
&lt;li&gt;Protocol: TCP&lt;/li&gt;
&lt;li&gt;Source: alias with admin IPs&lt;/li&gt;
&lt;li&gt;Destination: This Firewall&lt;/li&gt;
&lt;li&gt;Destination port: the configured SSH port&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And another rule that blocks SSH from any other source.&lt;/p&gt;
&lt;h2 id="vlans-network-segmentation"&gt;VLANs: network segmentation
&lt;/h2&gt;&lt;p&gt;Segmentation with VLANs is probably the most important change you can make to home network security. Without segmentation, a compromised WiFi bulb has direct access to the NAS with family photos. With VLANs, each type of device lives in its own isolated segment.&lt;/p&gt;
&lt;h3 id="vlan-design"&gt;VLAN design
&lt;/h3&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;VLAN ID&lt;/th&gt;
 &lt;th&gt;Name&lt;/th&gt;
 &lt;th&gt;Subnet&lt;/th&gt;
 &lt;th&gt;Purpose&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;10&lt;/td&gt;
 &lt;td&gt;Main&lt;/td&gt;
 &lt;td&gt;192.168.10.0/24&lt;/td&gt;
 &lt;td&gt;Trusted devices: laptops, desktops, personal mobiles&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;20&lt;/td&gt;
 &lt;td&gt;Guests&lt;/td&gt;
 &lt;td&gt;192.168.20.0/24&lt;/td&gt;
 &lt;td&gt;Guest devices, no access to internal network&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;30&lt;/td&gt;
 &lt;td&gt;IoT&lt;/td&gt;
 &lt;td&gt;192.168.30.0/24&lt;/td&gt;
 &lt;td&gt;IoT devices: cameras, sensors, bulbs, vacuums&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;40&lt;/td&gt;
 &lt;td&gt;Servers&lt;/td&gt;
 &lt;td&gt;192.168.40.0/24&lt;/td&gt;
 &lt;td&gt;Servers, NAS, self-hosted services&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;50&lt;/td&gt;
 &lt;td&gt;Management&lt;/td&gt;
 &lt;td&gt;192.168.50.0/24&lt;/td&gt;
 &lt;td&gt;Infrastructure management: switches, APs, OPNsense itself&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="creating-vlans-in-opnsense"&gt;Creating VLANs in OPNsense
&lt;/h3&gt;&lt;p&gt;For each VLAN:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Interfaces &amp;gt; Other Types &amp;gt; VLAN&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Create a new VLAN:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Parent interface&lt;/strong&gt;: the physical interface connected to the managed switch (for example, &lt;code&gt;igb1&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VLAN tag&lt;/strong&gt;: the ID from the table above (10, 20, 30, 40, 50).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Description&lt;/strong&gt;: the name of the VLAN.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Interfaces &amp;gt; Assignments&lt;/strong&gt; and assign each VLAN as a new interface.&lt;/li&gt;
&lt;li&gt;Configure each interface:
&lt;ul&gt;
&lt;li&gt;Enable the interface.&lt;/li&gt;
&lt;li&gt;Assign a static IP: the gateway IP for that subnet (for example, &lt;code&gt;192.168.10.1/24&lt;/code&gt; for VLAN 10).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Configure DHCP in &lt;strong&gt;Services &amp;gt; DHCPv4&lt;/strong&gt; for each VLAN with its corresponding range.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="switch-configuration"&gt;Switch configuration
&lt;/h3&gt;&lt;p&gt;The managed switch needs to be configured to understand VLANs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;trunk&lt;/strong&gt; port going to OPNsense must be &lt;strong&gt;tagged&lt;/strong&gt; for all VLANs (10, 20, 30, 40, 50).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Access ports&lt;/strong&gt; are configured as &lt;strong&gt;untagged&lt;/strong&gt; in the corresponding VLAN. For example, the port where a guest AP is connected is set to untagged on VLAN 20.&lt;/li&gt;
&lt;li&gt;If the AP supports multiple SSIDs with VLANs (like Ubiquiti or TP-Link Omada), you can create one SSID per VLAN and the AP handles tagging the traffic.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="firewall-rules-between-vlans"&gt;Firewall rules between VLANs
&lt;/h3&gt;&lt;p&gt;This is the point where segmentation is really defined. Without firewall rules, VLANs share the same router and can communicate with each other. Explicit rules need to be created.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;General principle&lt;/strong&gt;: deny everything between VLANs by default and allow only what&amp;rsquo;s necessary.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;VLAN 10 (Main)&lt;/strong&gt;:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Action&lt;/th&gt;
 &lt;th&gt;Source&lt;/th&gt;
 &lt;th&gt;Destination&lt;/th&gt;
 &lt;th&gt;Port&lt;/th&gt;
 &lt;th&gt;Description&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Pass&lt;/td&gt;
 &lt;td&gt;Main net&lt;/td&gt;
 &lt;td&gt;*&lt;/td&gt;
 &lt;td&gt;*&lt;/td&gt;
 &lt;td&gt;Full internet access&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Pass&lt;/td&gt;
 &lt;td&gt;Main net&lt;/td&gt;
 &lt;td&gt;Servers net&lt;/td&gt;
 &lt;td&gt;*&lt;/td&gt;
 &lt;td&gt;Access to internal services&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Block&lt;/td&gt;
 &lt;td&gt;Main net&lt;/td&gt;
 &lt;td&gt;Management net&lt;/td&gt;
 &lt;td&gt;*&lt;/td&gt;
 &lt;td&gt;No direct access to management&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Block&lt;/td&gt;
 &lt;td&gt;Main net&lt;/td&gt;
 &lt;td&gt;IoT net&lt;/td&gt;
 &lt;td&gt;*&lt;/td&gt;
 &lt;td&gt;IoT isolation&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;VLAN 20 (Guests)&lt;/strong&gt;:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Action&lt;/th&gt;
 &lt;th&gt;Source&lt;/th&gt;
 &lt;th&gt;Destination&lt;/th&gt;
 &lt;th&gt;Port&lt;/th&gt;
 &lt;th&gt;Description&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Pass&lt;/td&gt;
 &lt;td&gt;Guests net&lt;/td&gt;
 &lt;td&gt;*&lt;/td&gt;
 &lt;td&gt;80, 443, 53&lt;/td&gt;
 &lt;td&gt;Only web browsing and DNS&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Block&lt;/td&gt;
 &lt;td&gt;Guests net&lt;/td&gt;
 &lt;td&gt;RFC1918&lt;/td&gt;
 &lt;td&gt;*&lt;/td&gt;
 &lt;td&gt;No access to internal networks&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;VLAN 30 (IoT)&lt;/strong&gt;:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Action&lt;/th&gt;
 &lt;th&gt;Source&lt;/th&gt;
 &lt;th&gt;Destination&lt;/th&gt;
 &lt;th&gt;Port&lt;/th&gt;
 &lt;th&gt;Description&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Pass&lt;/td&gt;
 &lt;td&gt;IoT net&lt;/td&gt;
 &lt;td&gt;*&lt;/td&gt;
 &lt;td&gt;443, 8883&lt;/td&gt;
 &lt;td&gt;Only HTTPS and MQTT for cloud&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Pass&lt;/td&gt;
 &lt;td&gt;IoT net&lt;/td&gt;
 &lt;td&gt;IoT gateway&lt;/td&gt;
 &lt;td&gt;53&lt;/td&gt;
 &lt;td&gt;DNS&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Block&lt;/td&gt;
 &lt;td&gt;IoT net&lt;/td&gt;
 &lt;td&gt;RFC1918&lt;/td&gt;
 &lt;td&gt;*&lt;/td&gt;
 &lt;td&gt;No access to internal networks&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;VLAN 40 (Servers)&lt;/strong&gt;:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Action&lt;/th&gt;
 &lt;th&gt;Source&lt;/th&gt;
 &lt;th&gt;Destination&lt;/th&gt;
 &lt;th&gt;Port&lt;/th&gt;
 &lt;th&gt;Description&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Pass&lt;/td&gt;
 &lt;td&gt;Servers net&lt;/td&gt;
 &lt;td&gt;*&lt;/td&gt;
 &lt;td&gt;80, 443, 53&lt;/td&gt;
 &lt;td&gt;Internet access for updates&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Pass&lt;/td&gt;
 &lt;td&gt;Servers net&lt;/td&gt;
 &lt;td&gt;Servers net&lt;/td&gt;
 &lt;td&gt;*&lt;/td&gt;
 &lt;td&gt;Inter-service communication&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Block&lt;/td&gt;
 &lt;td&gt;Servers net&lt;/td&gt;
 &lt;td&gt;Main net&lt;/td&gt;
 &lt;td&gt;*&lt;/td&gt;
 &lt;td&gt;Servers don&amp;rsquo;t initiate connections to Main&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;VLAN 50 (Management)&lt;/strong&gt;:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Action&lt;/th&gt;
 &lt;th&gt;Source&lt;/th&gt;
 &lt;th&gt;Destination&lt;/th&gt;
 &lt;th&gt;Port&lt;/th&gt;
 &lt;th&gt;Description&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Pass&lt;/td&gt;
 &lt;td&gt;Management net&lt;/td&gt;
 &lt;td&gt;*&lt;/td&gt;
 &lt;td&gt;*&lt;/td&gt;
 &lt;td&gt;Full access (admins only)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;To implement the RFC1918 network blocking rule (which covers all private subnets), create an alias in &lt;strong&gt;Firewall &amp;gt; Aliases&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Name: &lt;code&gt;RFC1918&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Type: Network&lt;/li&gt;
&lt;li&gt;Content: &lt;code&gt;10.0.0.0/8&lt;/code&gt;, &lt;code&gt;172.16.0.0/12&lt;/code&gt;, &lt;code&gt;192.168.0.0/16&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This alias is used as the destination in blocking rules to prevent VLANs like Guests or IoT from accessing any internal network.&lt;/p&gt;
&lt;h2 id="hardening-and-best-practices"&gt;Hardening and best practices
&lt;/h2&gt;&lt;h3 id="updates"&gt;Updates
&lt;/h3&gt;&lt;p&gt;The first and most basic thing: keep OPNsense updated. Security updates are published regularly and patches are applied quickly.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Configure email update notifications.&lt;/li&gt;
&lt;li&gt;Apply updates during low-usage times.&lt;/li&gt;
&lt;li&gt;Before updating, make a backup (it&amp;rsquo;s already automated if you followed the first section).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="dns-over-tls-dot"&gt;DNS over TLS (DoT)
&lt;/h3&gt;&lt;p&gt;Configure Unbound (OPNsense&amp;rsquo;s DNS resolver) to use DNS over TLS:&lt;/p&gt;
&lt;p&gt;In &lt;strong&gt;Services &amp;gt; Unbound DNS &amp;gt; General&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Enable &lt;strong&gt;DNS over TLS&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;In &lt;strong&gt;Custom forwarding&lt;/strong&gt;, add DNS servers that support DoT:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# Quad9 (includes malware filtering)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;9.9.9.9@853#dns.quad9.net
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;149.112.112.112@853#dns.quad9.net
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# Cloudflare
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;1.1.1.1@853#cloudflare-dns.com
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;1.0.0.1@853#cloudflare-dns.com
&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 encrypts DNS queries between OPNsense and the resolver, preventing the ISP from seeing which domains each device queries.&lt;/p&gt;
&lt;h3 id="disable-unnecessary-services"&gt;Disable unnecessary services
&lt;/h3&gt;&lt;p&gt;In &lt;strong&gt;System &amp;gt; Settings &amp;gt; Administration&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Disable &lt;strong&gt;UPnP&lt;/strong&gt; unless strictly necessary (and even then, limit it to specific interfaces).&lt;/li&gt;
&lt;li&gt;Disable &lt;strong&gt;SNMP&lt;/strong&gt; if it&amp;rsquo;s not used for monitoring.&lt;/li&gt;
&lt;li&gt;Review installed plugins and uninstall those not in use.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="centralized-logging"&gt;Centralized logging
&lt;/h3&gt;&lt;p&gt;OPNsense can send logs to an external syslog server. If you have a monitoring stack (Grafana + Loki, or ELK), configure sending in &lt;strong&gt;System &amp;gt; Settings &amp;gt; Logging &amp;gt; Remote&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Server: the syslog server IP.&lt;/li&gt;
&lt;li&gt;Protocol: TCP with TLS if possible.&lt;/li&gt;
&lt;li&gt;Facility: select which logs to send (firewall, system, IDS).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="regular-audit"&gt;Regular audit
&lt;/h3&gt;&lt;p&gt;Establish a review routine:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Weekly&lt;/strong&gt;: review IDS/IPS and CrowdSec logs. Look for recurring patterns.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Monthly&lt;/strong&gt;: review firewall rules. Are there rules that no longer make sense? Has any new device been added that needs specific rules?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Quarterly&lt;/strong&gt;: review users and permissions. Is each user still necessary? Are SSH keys still valid?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the third and final post of the series, we&amp;rsquo;ll review everything configured, check the overall security posture, and look at advanced practices.&lt;/p&gt;</description></item><item><title>OPNsense: from hardware to a working firewall</title><link>https://adurrr.github.io/en/p/opnsense-from-hardware-to-a-working-firewall/</link><pubDate>Wed, 01 Apr 2026 00:00:00 +0000</pubDate><guid>https://adurrr.github.io/en/p/opnsense-from-hardware-to-a-working-firewall/</guid><description>&lt;h2 id="what-hardware-opnsense-needs"&gt;What hardware OPNsense needs
&lt;/h2&gt;&lt;p&gt;Before opening the installer, it&amp;rsquo;s worth knowing what OPNsense requires and what makes sense to buy. The official documentation distinguishes between minimum and recommended, but in practice there are nuances that matter quite a bit.&lt;/p&gt;
&lt;h3 id="official-minimum-requirements"&gt;Official minimum requirements
&lt;/h3&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Resource&lt;/th&gt;
 &lt;th&gt;Minimum&lt;/th&gt;
 &lt;th&gt;Recommended&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;CPU&lt;/td&gt;
 &lt;td&gt;64-bit x86-64, 1 GHz&lt;/td&gt;
 &lt;td&gt;Recent multi-core with AES-NI&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;RAM&lt;/td&gt;
 &lt;td&gt;2 GB&lt;/td&gt;
 &lt;td&gt;8 GB&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Storage&lt;/td&gt;
 &lt;td&gt;40 GB SSD&lt;/td&gt;
 &lt;td&gt;120 GB SSD&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;NICs&lt;/td&gt;
 &lt;td&gt;2 network interfaces&lt;/td&gt;
 &lt;td&gt;2+ Intel interfaces&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;AES-NI has been mandatory since OPNsense 24.1. Without this instruction the installer won&amp;rsquo;t even boot. Any Intel processor from sixth generation onward includes it, and it&amp;rsquo;s been present in AMD since Ryzen.&lt;/p&gt;
&lt;h3 id="budget-options-that-work"&gt;Budget options that work
&lt;/h3&gt;&lt;p&gt;The cheapest option I&amp;rsquo;ve had good experience with is a mini PC with an Intel N100 or N200 processor. They&amp;rsquo;re designed for low power consumption and have AES-NI, which covers the main requirement. They can be found with four Intel i226-V Ethernet ports for under 150 euros.&lt;/p&gt;
&lt;p&gt;Some specific models that work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Topton N100/N200 with 4x i226-V&lt;/strong&gt;: between 120 and 170 euros depending on configuration. They come without RAM or SSD, which are purchased separately. An 8 GB DDR5 module and a 128 GB SSD add about 30 euros more.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Protectli VP2420 or VP2410&lt;/strong&gt;: more expensive (around 300 euros), but with official support and an aluminum case that dissipates heat well. A good option if you prefer something with serious warranty.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Recycled hardware with two NICs&lt;/strong&gt;: a Dell OptiPlex or Lenovo ThinkCentre with a dual-port Intel PCIe card works perfectly. They can be found for 50-80 euros in second-hand markets.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What really matters: make sure the network interfaces are Intel. Realtek works, but they cause problems with offloading and performance under load. It&amp;rsquo;s worth paying the difference.&lt;/p&gt;
&lt;h2 id="installation"&gt;Installation
&lt;/h2&gt;&lt;p&gt;Installing OPNsense is straightforward. Download the ISO image from the official website, write it to a USB with &lt;code&gt;dd&lt;/code&gt; or Rufus on Windows, and boot from the USB.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Write the image to a USB (be careful to select the correct device)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;dd &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;OPNsense-24.7-dvd-amd64.iso &lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/sdX &lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4M &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;progress
&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;On boot, a live environment appears with the option to try before installing. The default user is &lt;code&gt;installer&lt;/code&gt; with password &lt;code&gt;opnsense&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;During installation:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Select the destination disk for installation (the internal SSD, not the USB).&lt;/li&gt;
&lt;li&gt;Choose the filesystem. &lt;strong&gt;UFS&lt;/strong&gt; is the simple and stable option. &lt;strong&gt;ZFS&lt;/strong&gt; has advantages (snapshots, compression), but for a firewall with a single disk, UFS is sufficient.&lt;/li&gt;
&lt;li&gt;Define the root password.&lt;/li&gt;
&lt;li&gt;Remove the USB and reboot.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;After reboot, OPNsense boots directly and presents a text console with a basic menu. From there you can assign interfaces and configure the LAN IP address to access the web interface.&lt;/p&gt;
&lt;h2 id="pppoe-configuration-on-wan"&gt;PPPoE configuration on WAN
&lt;/h2&gt;&lt;p&gt;If your ISP uses PPPoE (as is the case with many fiber connections in Spain and Latin America), it needs to be configured on the WAN interface.&lt;/p&gt;
&lt;p&gt;In &lt;strong&gt;Interfaces &amp;gt; WAN&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Change the IPv4 configuration type to &lt;strong&gt;PPPoE&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Enter the username and password provided by the ISP.&lt;/li&gt;
&lt;li&gt;With most providers you don&amp;rsquo;t need to touch the MTU, but if you notice fragmentation issues, adjust it to &lt;strong&gt;1492&lt;/strong&gt; (the standard for PPPoE over Ethernet with MTU 1500).&lt;/li&gt;
&lt;li&gt;Save and apply.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If the connection doesn&amp;rsquo;t come up, verify that the ONT cable goes directly to the port assigned as WAN. Some ISP ONTs need to be in bridge mode for OPNsense to negotiate the PPPoE session directly.&lt;/p&gt;
&lt;h2 id="lan-in-bridge-mode"&gt;LAN in bridge mode
&lt;/h2&gt;&lt;p&gt;There are situations where it&amp;rsquo;s useful to group multiple physical ports into the same network segment, for example when the mini PC has four ports and we want three of them to work as a switch without additional hardware.&lt;/p&gt;
&lt;p&gt;To configure a bridge in OPNsense:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Interfaces &amp;gt; Other Types &amp;gt; Bridge&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Create a new bridge and add the interfaces you want to group (for example, &lt;code&gt;igb1&lt;/code&gt;, &lt;code&gt;igb2&lt;/code&gt;, &lt;code&gt;igb3&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Interfaces &amp;gt; Assignments&lt;/strong&gt; and assign the newly created bridge as the LAN interface.&lt;/li&gt;
&lt;li&gt;Configure the static LAN IP on the bridge interface (for example, &lt;code&gt;192.168.1.1/24&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Enable the DHCP server in &lt;strong&gt;Services &amp;gt; DHCPv4&lt;/strong&gt; pointing to the bridge interface.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;With this, all three ports share the same network segment and DHCP serves addresses for all of them.&lt;/p&gt;
&lt;h2 id="wireless-interface-and-firmware"&gt;Wireless interface and firmware
&lt;/h2&gt;&lt;p&gt;OPNsense supports some WiFi cards, but the support is limited compared to Linux. Atheros cards work best, but many need additional firmware that isn&amp;rsquo;t included by default.&lt;/p&gt;
&lt;p&gt;To install the required firmware:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# From the OPNsense console (option 8 from the menu for shell)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pkg install wifi-firmware-atheros
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Or for Intel cards:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pkg install wifi-firmware-intel
&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;After installing the firmware, restart the system. The wireless interface should appear in &lt;strong&gt;Interfaces &amp;gt; Assignments&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;To configure the access point:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Interfaces &amp;gt; Wireless&lt;/strong&gt; and create a new device in &lt;strong&gt;Access Point&lt;/strong&gt; mode.&lt;/li&gt;
&lt;li&gt;Select the standard (802.11ac/ax if the card supports it).&lt;/li&gt;
&lt;li&gt;Configure the SSID and WPA2/WPA3 security.&lt;/li&gt;
&lt;li&gt;Assign the wireless interface and give it an IP in a different range from the wired LAN, or add it to the existing bridge if you want it on the same segment.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;An honest warning: integrated WiFi in OPNsense works, but don&amp;rsquo;t expect the performance or stability of a dedicated access point. For serious use, it&amp;rsquo;s better to use an external AP (Ubiquiti, TP-Link Omada) and let OPNsense handle only routing and firewall.&lt;/p&gt;
&lt;h2 id="ids-and-ips-intrusion-detection-and-prevention"&gt;IDS and IPS: intrusion detection and prevention
&lt;/h2&gt;&lt;p&gt;OPNsense includes Suricata as the IDS/IPS engine. The difference between the two modes is simple: IDS detects and logs, IPS detects and blocks.&lt;/p&gt;
&lt;h3 id="initial-configuration"&gt;Initial configuration
&lt;/h3&gt;&lt;p&gt;In &lt;strong&gt;Services &amp;gt; Intrusion Detection&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Enable IDS&lt;/strong&gt;: check the activation box.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IPS mode&lt;/strong&gt;: if you want it to block traffic, change the mode to IPS. This requires Suricata to run in inline mode, which is the default behavior in OPNsense.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interfaces&lt;/strong&gt;: select WAN at minimum. If you want to inspect internal traffic as well, add LAN, but this consumes quite a bit more CPU.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pattern matcher&lt;/strong&gt;: select &lt;strong&gt;Hyperscan&lt;/strong&gt; if the hardware supports it (processors with SSSE3). It&amp;rsquo;s significantly faster than the default matcher.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="recommended-rule-sets-in-2026"&gt;Recommended rule sets in 2026
&lt;/h3&gt;&lt;p&gt;It&amp;rsquo;s not about activating all available rules. That consumes resources and generates false positives. A reasonable selection:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Rule set&lt;/th&gt;
 &lt;th&gt;What it&amp;rsquo;s for&lt;/th&gt;
 &lt;th&gt;Recommendation&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;ET Open (Emerging Threats)&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;Known threats, malware, C2&lt;/td&gt;
 &lt;td&gt;Activate. It&amp;rsquo;s the foundation&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Abuse.ch SSL Blacklist&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;Certificates associated with malware&lt;/td&gt;
 &lt;td&gt;Activate&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Abuse.ch URLhaus&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;Malware distribution URLs&lt;/td&gt;
 &lt;td&gt;Activate&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;ET Open Compromised&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;Known compromised IPs&lt;/td&gt;
 &lt;td&gt;Activate&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Feodo Tracker&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;Banking botnets&lt;/td&gt;
 &lt;td&gt;Activate&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;ET Open Tor&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;Tor traffic&lt;/td&gt;
 &lt;td&gt;Only if you want to block Tor&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Snort VRT&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;Snort commercial rules&lt;/td&gt;
 &lt;td&gt;Requires subscription, not essential&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="best-practices-for-idsips-in-2026"&gt;Best practices for IDS/IPS in 2026
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Don&amp;rsquo;t activate all rules&lt;/strong&gt;. Select the ones that make sense for your environment. A home network doesn&amp;rsquo;t need SCADA or SQL server rules.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automatic rule updates&lt;/strong&gt;: configure scheduled rule downloads. In &lt;strong&gt;Schedule&lt;/strong&gt; within IDS, set a daily update.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Review logs before switching to IPS mode&lt;/strong&gt;. Leave the system in IDS mode for at least a week to identify false positives. If something legitimate triggers alerts, create an exception before starting to block.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Monitor CPU usage&lt;/strong&gt;. Suricata can consume a lot on modest hardware. If the processor stays above 80% usage with IPS active, reduce the number of rules or limit inspection to the WAN interface.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use EVE JSON logging&lt;/strong&gt; to export events to a SIEM or analysis tool. The JSON format facilitates integration with Elasticsearch, Grafana, or Wazuh.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Don&amp;rsquo;t rely solely on IDS/IPS&lt;/strong&gt;. It&amp;rsquo;s one more layer of defense. It doesn&amp;rsquo;t replace good firewall rules, network segmentation, or regular updates.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="crowdsec"&gt;CrowdSec
&lt;/h2&gt;&lt;p&gt;CrowdSec complements Suricata with a different approach: log analysis and shared decisions with the community. While Suricata inspects packets in real time, CrowdSec analyzes service logs and applies bans based on behavior patterns.&lt;/p&gt;
&lt;h3 id="installation-1"&gt;Installation
&lt;/h3&gt;&lt;p&gt;CrowdSec has an official plugin for OPNsense:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;System &amp;gt; Firmware &amp;gt; Plugins&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Search for &lt;code&gt;os-crowdsec&lt;/code&gt; and install it.&lt;/li&gt;
&lt;li&gt;After installation, it appears in &lt;strong&gt;Services &amp;gt; CrowdSec&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="recommended-configuration-and-collections"&gt;Recommended configuration and collections
&lt;/h3&gt;&lt;p&gt;(Optional) After installation, register the instance in the CrowdSec central console:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# From the OPNsense shell&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cscli console enroll &amp;lt;your-enrollment-key&amp;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 collections define what attack patterns CrowdSec detects. Recommended for a home or small office firewall:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Base collection for firewalls (usually already installed)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cscli collections install crowdsecurity/freebsd
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Detection of port scanning and SSH brute force&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cscli collections install crowdsecurity/sshd
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cscli collections install crowdsecurity/iptables
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# HTTP protection if you expose web services&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cscli collections install crowdsecurity/nginx
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cscli collections install crowdsecurity/base-http-scenarios
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Aggressive scan detection&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cscli collections install crowdsecurity/http-cve
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Community blocklists (known malicious IPs)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cscli collections install crowdsecurity/whitelist-good-actors
&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;Enable the &lt;strong&gt;firewall bouncer&lt;/strong&gt; so CrowdSec can create blocking rules directly in the OPNsense firewall:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cscli bouncers add opnsense-firewall
&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 generated token is entered in the plugin configuration in the web interface, in &lt;strong&gt;Services &amp;gt; CrowdSec &amp;gt; Bouncer&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The real advantage of CrowdSec is shared intelligence. When a community member detects an attacking IP, that information is distributed to everyone else. It&amp;rsquo;s like having a collaborative IP reputation system.&lt;/p&gt;
&lt;h2 id="wireguard"&gt;WireGuard
&lt;/h2&gt;&lt;p&gt;WireGuard is the cleanest option for VPN in 2026. Faster, simpler, and with better cryptography than OpenVPN or IPsec.&lt;/p&gt;
&lt;h3 id="server-configuration"&gt;Server configuration
&lt;/h3&gt;&lt;p&gt;In &lt;strong&gt;VPN &amp;gt; WireGuard&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Create an instance&lt;/strong&gt;: go to the &lt;strong&gt;Instances&lt;/strong&gt; tab (or &lt;strong&gt;Local&lt;/strong&gt; in earlier versions) and add a new one.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Generate a key pair (done automatically when creating the instance).&lt;/li&gt;
&lt;li&gt;Listen port: &lt;code&gt;51820&lt;/code&gt; (or whatever you prefer).&lt;/li&gt;
&lt;li&gt;Tunnel address: &lt;code&gt;10.10.10.1/24&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Add a peer&lt;/strong&gt;: in the &lt;strong&gt;Peers&lt;/strong&gt; tab, create a new pair.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Client&amp;rsquo;s public key (generated on the client device).&lt;/li&gt;
&lt;li&gt;Allowed IPs: &lt;code&gt;10.10.10.2/32&lt;/code&gt; (the IP the client will have inside the tunnel).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Assign the interface&lt;/strong&gt;: go to &lt;strong&gt;Interfaces &amp;gt; Assignments&lt;/strong&gt;, assign the WireGuard interface (&lt;code&gt;wg0&lt;/code&gt; or &lt;code&gt;wg1&lt;/code&gt;), enable it, and don&amp;rsquo;t touch the IP configuration (it&amp;rsquo;s already defined in WireGuard).&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="client-configuration"&gt;Client configuration
&lt;/h3&gt;&lt;p&gt;On the client (mobile, laptop), the configuration is a simple file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Interface]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;PrivateKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;client-private-key&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10.10.10.2/24&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;DNS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10.10.10.1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Peer]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;PublicKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;server-public-key&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;public-ip-or-ddns&amp;gt;:51820&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;AllowedIPs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0/0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;PersistentKeepalive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;25&lt;/span&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;With &lt;code&gt;AllowedIPs = 0.0.0.0/0&lt;/code&gt;, all client traffic goes through the tunnel. If you only want to access the local network, change to &lt;code&gt;AllowedIPs = 192.168.1.0/24, 10.10.10.0/24&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="firewall-rules"&gt;Firewall rules
&lt;/h2&gt;&lt;p&gt;It&amp;rsquo;s no use configuring services if the firewall rules don&amp;rsquo;t allow the correct traffic. OPNsense blocks everything by default on WAN, which is correct. The work is in allowing what&amp;rsquo;s necessary on LAN and WireGuard.&lt;/p&gt;
&lt;h3 id="rules-for-lan"&gt;Rules for LAN
&lt;/h3&gt;&lt;p&gt;In &lt;strong&gt;Firewall &amp;gt; Rules &amp;gt; LAN&lt;/strong&gt;:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Action&lt;/th&gt;
 &lt;th&gt;Protocol&lt;/th&gt;
 &lt;th&gt;Source&lt;/th&gt;
 &lt;th&gt;Destination&lt;/th&gt;
 &lt;th&gt;Destination Port&lt;/th&gt;
 &lt;th&gt;Description&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Pass&lt;/td&gt;
 &lt;td&gt;IPv4+6&lt;/td&gt;
 &lt;td&gt;LAN net&lt;/td&gt;
 &lt;td&gt;*&lt;/td&gt;
 &lt;td&gt;*&lt;/td&gt;
 &lt;td&gt;Allow LAN outbound&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Pass&lt;/td&gt;
 &lt;td&gt;IPv4&lt;/td&gt;
 &lt;td&gt;LAN net&lt;/td&gt;
 &lt;td&gt;LAN address&lt;/td&gt;
 &lt;td&gt;53&lt;/td&gt;
 &lt;td&gt;DNS to firewall&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Pass&lt;/td&gt;
 &lt;td&gt;IPv4&lt;/td&gt;
 &lt;td&gt;LAN net&lt;/td&gt;
 &lt;td&gt;LAN address&lt;/td&gt;
 &lt;td&gt;443&lt;/td&gt;
 &lt;td&gt;WebUI access&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The first rule is the most permissive and allows LAN to go out to the internet. In the second post of the series we&amp;rsquo;ll see how to restrict this with VLANs.&lt;/p&gt;
&lt;h3 id="rules-for-wireguard"&gt;Rules for WireGuard
&lt;/h3&gt;&lt;p&gt;In &lt;strong&gt;Firewall &amp;gt; Rules &amp;gt; WireGuard&lt;/strong&gt; (or the assigned interface):&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Action&lt;/th&gt;
 &lt;th&gt;Protocol&lt;/th&gt;
 &lt;th&gt;Source&lt;/th&gt;
 &lt;th&gt;Destination&lt;/th&gt;
 &lt;th&gt;Destination Port&lt;/th&gt;
 &lt;th&gt;Description&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Pass&lt;/td&gt;
 &lt;td&gt;IPv4&lt;/td&gt;
 &lt;td&gt;WireGuard net&lt;/td&gt;
 &lt;td&gt;LAN net&lt;/td&gt;
 &lt;td&gt;*&lt;/td&gt;
 &lt;td&gt;LAN access from VPN&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Pass&lt;/td&gt;
 &lt;td&gt;IPv4&lt;/td&gt;
 &lt;td&gt;WireGuard net&lt;/td&gt;
 &lt;td&gt;*&lt;/td&gt;
 &lt;td&gt;*&lt;/td&gt;
 &lt;td&gt;Internet outbound from VPN&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="rule-on-wan-for-wireguard"&gt;Rule on WAN for WireGuard
&lt;/h3&gt;&lt;p&gt;In &lt;strong&gt;Firewall &amp;gt; Rules &amp;gt; WAN&lt;/strong&gt;, add a rule to allow incoming connection to the WireGuard port:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Action&lt;/th&gt;
 &lt;th&gt;Protocol&lt;/th&gt;
 &lt;th&gt;Source&lt;/th&gt;
 &lt;th&gt;Destination&lt;/th&gt;
 &lt;th&gt;Destination Port&lt;/th&gt;
 &lt;th&gt;Description&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Pass&lt;/td&gt;
 &lt;td&gt;UDP&lt;/td&gt;
 &lt;td&gt;*&lt;/td&gt;
 &lt;td&gt;WAN address&lt;/td&gt;
 &lt;td&gt;51820&lt;/td&gt;
 &lt;td&gt;Incoming WireGuard&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="offloading-and-hardware-tuning"&gt;Offloading and hardware tuning
&lt;/h2&gt;&lt;p&gt;When everything is working, it&amp;rsquo;s time to squeeze out the performance. OPNsense runs on FreeBSD, and it has several offloading options that can make a noticeable difference.&lt;/p&gt;
&lt;h3 id="what-offloading-is"&gt;What offloading is
&lt;/h3&gt;&lt;p&gt;Offloading means delegating certain network operations to the network card hardware instead of processing them in software on the CPU. This frees up CPU cycles for other tasks (like Suricata or CrowdSec) and reduces latency.&lt;/p&gt;
&lt;h3 id="available-options"&gt;Available options
&lt;/h3&gt;&lt;p&gt;In &lt;strong&gt;Interfaces &amp;gt; Settings&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hardware CRC (Checksum Offloading)&lt;/strong&gt;: delegates TCP/UDP/IP checksum calculation to the network card. Enable if the NIC supports it (Intel i210/i225/i226 do). Measurably reduces CPU load.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hardware TSO (TCP Segmentation Offloading)&lt;/strong&gt;: the network card handles splitting large TCP packets into smaller segments. Improves throughput in large transfers. Can cause problems with some IPS configurations, so test and verify.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hardware LRO (Large Receive Offloading)&lt;/strong&gt;: groups small incoming packets into larger blocks before passing them to the CPU. Reduces interrupts. &lt;strong&gt;Do not enable if using IPS in inline mode&lt;/strong&gt;, as it interferes with packet inspection.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VLAN Hardware Filtering&lt;/strong&gt;: if VLANs are used (we&amp;rsquo;ll cover this in the second post), let the NIC filter by VLAN ID in hardware.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="additional-system-tuning"&gt;Additional system tuning
&lt;/h3&gt;&lt;p&gt;In &lt;strong&gt;System &amp;gt; Settings &amp;gt; Tunables&lt;/strong&gt;, some useful adjustments:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# Increase network buffers
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;net.inet.tcp.recvspace=65536
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;net.inet.tcp.sendspace=65536
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# Enable RACK and BBR if hardware supports it (FreeBSD 14+)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;net.inet.tcp.functions_default=bbr
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# Adjust the number of NIC queues
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hw.igb.num_queues=4
&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;Don&amp;rsquo;t obsess over tuning. On most home connections (up to 1 Gbps symmetric), an N100 with the default settings already gives maximum performance. Tuning starts to matter when you want to squeeze 2.5 Gbps connections or more, or when Suricata consumes too much CPU.&lt;/p&gt;
&lt;h2 id="user-management-and-web-interface-security"&gt;User management and web interface security
&lt;/h2&gt;&lt;p&gt;Using &lt;code&gt;root&lt;/code&gt; for day-to-day web interface access is bad practice. If someone compromises those credentials, they have full control.&lt;/p&gt;
&lt;h3 id="create-a-new-administrator-user"&gt;Create a new administrator user
&lt;/h3&gt;&lt;p&gt;In &lt;strong&gt;System &amp;gt; Access &amp;gt; Users&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a new user with a descriptive name (not &lt;code&gt;admin&lt;/code&gt;, something less predictable).&lt;/li&gt;
&lt;li&gt;Assign a strong password. Minimum 16 characters, generated with a password manager.&lt;/li&gt;
&lt;li&gt;In &lt;strong&gt;Effective Privileges&lt;/strong&gt;, assign the &lt;code&gt;admins&lt;/code&gt; group.&lt;/li&gt;
&lt;li&gt;Enable OTP authentication if possible. OPNsense supports native TOTP, so it can be used with any authenticator app.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="change-the-root-password"&gt;Change the root password
&lt;/h3&gt;&lt;p&gt;In &lt;strong&gt;System &amp;gt; Access &amp;gt; Users&lt;/strong&gt;, select &lt;code&gt;root&lt;/code&gt; and change the password to something long and random. Store this password in a safe place (password manager) and don&amp;rsquo;t use it for daily access.&lt;/p&gt;
&lt;p&gt;A more radical option: disable root login on the web interface. This can be done by removing web access privileges from the root user, leaving only physical console access as a last resort.&lt;/p&gt;
&lt;h3 id="restrict-web-interface-access"&gt;Restrict web interface access
&lt;/h3&gt;&lt;p&gt;By default, the web interface is accessible from the entire LAN. This can be restricted:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Change the HTTPS port&lt;/strong&gt;: in &lt;strong&gt;System &amp;gt; Settings &amp;gt; Administration&lt;/strong&gt;, change the port from &lt;code&gt;443&lt;/code&gt; to another non-standard one (for example, &lt;code&gt;8443&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Restrict by source IP&lt;/strong&gt;: create an alias in &lt;strong&gt;Firewall &amp;gt; Aliases&lt;/strong&gt; with the IPs from which web interface access is allowed. Then, in the LAN firewall rules, create a rule that allows access to the WebUI port only from that alias, and a blocking rule for everything else.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enable HTTPS with your own certificate&lt;/strong&gt;: in &lt;strong&gt;System &amp;gt; Trust &amp;gt; Certificates&lt;/strong&gt;, generate a self-signed certificate or import one from Let&amp;rsquo;s Encrypt. This eliminates browser warnings and ensures the connection is properly encrypted.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Brute force protection&lt;/strong&gt;: in &lt;strong&gt;System &amp;gt; Settings &amp;gt; Administration&lt;/strong&gt;, configure the maximum number of failed attempts and the lockout time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Disable HTTP&lt;/strong&gt;: make sure only HTTPS is enabled. Unencrypted HTTP access to a firewall&amp;rsquo;s admin interface makes no sense.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In the next post in the series, we&amp;rsquo;ll take security further with encrypted backups, VLANs, Zenarmor, and system hardening.&lt;/p&gt;</description></item></channel></rss>