<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Networking &#8211; Bizr.net</title>
	<atom:link href="https://bizr.net/category/homelab/networking/feed/" rel="self" type="application/rss+xml" />
	<link>https://bizr.net</link>
	<description>Lessons from the lab, the stack, and the team.</description>
	<lastBuildDate>Wed, 24 Jun 2026 23:46:32 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0.2</generator>

<image>
	<url>https://bizr.net/wp-content/uploads/2026/05/favicon-1.png</url>
	<title>Networking &#8211; Bizr.net</title>
	<link>https://bizr.net</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Beyond the Tunnel: Adding HAProxy, ACME, and VLAN Isolation Behind My OPNsense WireGuard Setup</title>
		<link>https://bizr.net/beyond-the-tunnel-adding-haproxy-acme-and-vlan-isolation-behind-my-opnsense-wireguard-setup/</link>
					<comments>https://bizr.net/beyond-the-tunnel-adding-haproxy-acme-and-vlan-isolation-behind-my-opnsense-wireguard-setup/#respond</comments>
		
		<dc:creator><![CDATA[Bizr]]></dc:creator>
		<pubDate>Mon, 25 May 2026 21:55:25 +0000</pubDate>
				<category><![CDATA[Networking]]></category>
		<category><![CDATA[ACME]]></category>
		<category><![CDATA[HAProxy]]></category>
		<category><![CDATA[Let's Encrypt]]></category>
		<category><![CDATA[Network Segmentation]]></category>
		<category><![CDATA[OPNsense]]></category>
		<category><![CDATA[Reverse Proxy]]></category>
		<category><![CDATA[Self-Hosting]]></category>
		<category><![CDATA[TLS]]></category>
		<category><![CDATA[VLAN]]></category>
		<category><![CDATA[WireGuard]]></category>
		<guid isPermaLink="false">https://bizr.net/?p=2143</guid>

					<description><![CDATA[The WireGuard tunnel got traffic into my homelab. HAProxy, ACME, and a strict VLAN policy decide what happens next. A follow-up on how I expose services safely — and what I deliberately keep hidden.]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"> n the <a href="https://bizr.net/bypassing-cgnat-with-an-opnsense-wireguard-tunnel/">previous post</a>, I walked through how I escaped my ISP&#8217;s CGNAT prison using a WireGuard tunnel between my OPNsense firewall and a small VPS with a public IP. That solved the <em>&#8220;how do I reach my homelab from the outside&#8221;</em> problem.</p>



<p class="wp-block-paragraph">But getting traffic <em>into</em> the network is only half the story. Once it arrives, you need a clean, secure, and maintainable way to actually route it to the right service — without accidentally exposing things you didn&#8217;t mean to. That&#8217;s where the next layer comes in: <strong>HAProxy as a reverse proxy, automatic certificates via ACME, and a separate VLAN for services that should never see the public internet.</strong></p>



<p class="wp-block-paragraph">Here&#8217;s how I think about it, and how I built it.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">The Problem: One Tunnel, Many Services</h3>



<p class="wp-block-paragraph">The WireGuard tunnel gives me a single entry point. But behind that entry point, I have:</p>



<ul class="wp-block-list">
<li>A handful of services I <em>want</em> to expose (public-facing dashboards, a Git server, a few self-hosted apps).</li>



<li>A much larger pile of services I <em>never</em> want exposed (internal monitoring, IPMI interfaces, admin panels, backup systems, anything with a weak default).</li>



<li>TLS certificates I don&#8217;t want to manage by hand for every service.</li>
</ul>



<p class="wp-block-paragraph">Throwing all of that on one flat network is asking for trouble. So the design splits things up — physically, logically, and at the proxy layer.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">The Architecture</h3>



<p class="wp-block-paragraph">At a high level, traffic flows like this:</p>



<pre class="wp-block-code has-ast-global-color-5-color has-text-color has-link-color wp-elements-e63aba22c38d5288fa43006372541d35"><code>Internet → VPS (public IP) → WireGuard tunnel → OPNsense
                                                    ↓
                                            HAProxy (DMZ VLAN)
                                            ↙              ↘
                                  Public services       Internal-only services
                                  (DMZ VLAN)            (separate VLAN, no proxy exposure)</code></pre>



<p class="wp-block-paragraph">Three things matter here:</p>



<ol class="wp-block-list">
<li><strong>HAProxy lives on a dedicated DMZ VLAN.</strong> It&#8217;s the only thing that talks to the tunnel-facing side. If it gets compromised, the blast radius is limited to that VLAN.</li>



<li><strong>Public services sit on the DMZ VLAN too</strong>, but only the ones I&#8217;ve explicitly chosen to expose. (This is the same DMZ VLAN that, a few posts later, ends up getting <a href="https://bizr.net/hardening-your-homelabs-public-edge-geoip-blocking-crowdsec-and-syncing-fail2ban-with-opnsense/">GeoIP blocking, CrowdSec, and a fail2ban sync</a> layered on top of exactly this HAProxy entry point.) </li>



<li><strong>Internal-only services live on a separate VLAN entirely.</strong> HAProxy can route to them only if I configure it to — and for most of them, I never do. They&#8217;re reachable only over the LAN or via WireGuard from my admin devices.</li>
</ol>



<p class="wp-block-paragraph">The VLAN separation is enforced at OPNsense with firewall rules, not just at the proxy layer. Defense in depth: even if HAProxy were misconfigured, the firewall wouldn&#8217;t let it reach where it shouldn&#8217;t.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">HAProxy on OPNsense: Why and How</h3>



<p class="wp-block-paragraph">OPNsense ships with an HAProxy plugin, and for a small homelab it&#8217;s more than enough. I chose HAProxy over alternatives like Nginx Proxy Manager, Caddy, or Traefik for a few reasons:</p>



<ul class="wp-block-list">
<li>It&#8217;s already integrated into OPNsense — no extra container or VM to maintain.</li>



<li>The configuration model maps cleanly to how I think about traffic: <em>frontends</em>, <em>backends</em>, <em>ACLs</em>, and <em>rules</em>.</li>



<li>It handles SNI-based routing well, which means I can serve multiple HTTPS services on a single IP and port.</li>
</ul>



<p class="wp-block-paragraph">The setup is roughly:</p>



<ul class="wp-block-list">
<li><strong>One frontend on port 443</strong>, listening on the DMZ-side interface that receives traffic from the WireGuard tunnel.</li>



<li><strong>Backends per service</strong>, each pointing to the internal IP and port of the actual application.</li>



<li><strong>ACLs based on SNI / Host header</strong>, routing <code>git.example.com</code> to the Git backend, <code>dash.example.com</code> to the dashboard backend, and so on.</li>



<li>A default backend that returns a generic 404 — anything that doesn&#8217;t match an explicit rule gets nothing useful.</li>
</ul>



<p class="wp-block-paragraph">That last point matters. Whitelist what you expose. Don&#8217;t blacklist what you don&#8217;t.</p>



<p class="wp-block-paragraph">(Caddy ends up playing a closely related role one layer further in, in front of the Docker-based DMZ stack rather than at the OPNsense/HAProxy edge — see <a href="https://bizr.net/ditch-google-self-hosting-searxng-behind-authelia-for-desktop-and-open-webui/">Ditch Google</a> for that next hop, where the same &#8220;explicit allow, default reject&#8221; philosophy shows up again as a Caddy <code>forward_auth</code> block instead of an HAProxy ACL.) </p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">Automatic Certificates with ACME</h3>



<p class="wp-block-paragraph">Managing TLS by hand is a recipe for outages and weekend fire drills. OPNsense has an ACME Client plugin that integrates well with HAProxy.</p>



<p class="wp-block-paragraph">My setup uses <strong>DNS-01 challenges</strong> rather than HTTP-01, for two reasons:</p>



<ol class="wp-block-list">
<li><strong>It works for internal-only hostnames too.</strong> I can issue valid Let&#8217;s Encrypt certificates for services that aren&#8217;t reachable from the public internet, because the validation happens via DNS, not via an inbound HTTP request.</li>



<li><strong>It avoids opening port 80</strong> to the outside world for the sole purpose of cert renewal. The WireGuard tunnel + HAProxy already handles 443; I&#8217;d rather not introduce another exposed port.</li>
</ol>



<p class="wp-block-paragraph">The ACME client talks to my DNS provider&#8217;s API, creates the TXT record, gets the cert issued, and hands it off to HAProxy. Renewals happen automatically. I check the logs once a month, and that&#8217;s about it.</p>



<p class="wp-block-paragraph">A small but important detail: I keep a <strong>separate account and API token</strong> scoped only to the DNS zone used for ACME challenges. If that token leaks, the damage is limited to one zone — not my entire DNS setup. (Worth noting since one TLS mismatch born from exactly this kind of &#8220;which name am I actually using&#8221; confusion ends up costing a whole debugging session in <a href="https://bizr.net/locking-down-authelia-totp-real-smtp-and-the-hostname-vs-ip-tls-trap/">Locking Down Authelia</a> — a raw IP standing in for a hostname, rather than a DNS-01 zone scoping issue, but the same family of TLS-identity gotcha.) </p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">How I Decide What to Expose</h3>



<p class="wp-block-paragraph">This is the part that&#8217;s less about config files and more about discipline. My rule of thumb:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>A service gets a public hostname only if it needs one. Everything else stays internal.</strong></p>
</blockquote>



<p class="wp-block-paragraph">Concretely, I ask three questions before exposing anything:</p>



<ol class="wp-block-list">
<li><strong>Does someone outside my LAN actually need to reach this?</strong> If the answer is &#8220;it would be convenient,&#8221; that&#8217;s not a yes. Convenient is what WireGuard is for.</li>



<li><strong>Does the service have proper authentication, ideally with MFA?</strong> If it&#8217;s a service with a single shared password or no auth, it doesn&#8217;t get a public route. Period. (This is exactly the bar <a href="https://bizr.net/one-login-to-rule-them-all-wiring-authelia-as-sso-for-open-webui-and-nextcloud/">Authelia, wired up as SSO with real TOTP</a>, ends up clearing for Open WebUI and Nextcloud a few posts later — neither one had MFA on its own.)</li>



<li><strong>Am I willing to keep it patched?</strong> Exposing something means owning its security posture. If I won&#8217;t keep up with updates, it doesn&#8217;t go on the proxy.</li>
</ol>



<p class="wp-block-paragraph">Anything that fails those checks lives on the internal VLAN and is reachable only via WireGuard from my own devices. That covers the vast majority of my homelab: monitoring, dashboards, IPMI, backup UIs, the lot.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">The Mental Model</h3>



<p class="wp-block-paragraph">The way I think about this whole layered setup:</p>



<ul class="wp-block-list">
<li><strong>WireGuard + VPS</strong> is the <em>front door</em>. It decides who can knock.</li>



<li><strong>HAProxy</strong> is the <em>receptionist</em>. It decides where the visitor is allowed to go.</li>



<li><strong>VLANs and firewall rules</strong> are the <em>locked doors inside the building</em>. Even if the receptionist makes a mistake, most rooms are still inaccessible.</li>



<li><strong>ACME</strong> keeps the locks well-oiled without me having to think about it.</li>
</ul>



<p class="wp-block-paragraph">No single layer is doing all the work. Each one is simple, focused, and replaceable. If I ever swap HAProxy for Caddy or Traefik, the rest of the architecture doesn&#8217;t have to change.</p>



<p class="wp-block-paragraph">That&#8217;s the part I find most satisfying — not the specific tools, but the way the layers compose. It&#8217;s also, honestly, how I&#8217;ve come to think about a lot of things outside the homelab: small, well-defined responsibilities beat one tool trying to do everything.</p>



<p class="wp-block-paragraph">Everything from here builds on this same foundation: <a href="https://bizr.net/hardening-your-homelabs-public-edge-geoip-blocking-crowdsec-and-syncing-fail2ban-with-opnsense/">GeoIP/CrowdSec hardening</a> and <a href="https://bizr.net/from-app-logs-to-firewall-blocks-building-a-fail2ban-to-opnsense-ban-pipeline/">fail2ban-to-firewall syncing</a> on the WAN side, <a href="https://bizr.net/one-login-to-rule-them-all-wiring-authelia-as-sso-for-open-webui-and-nextcloud/">Authelia SSO</a> and <a href="https://bizr.net/locking-down-authelia-totp-real-smtp-and-the-hostname-vs-ip-tls-trap/">real 2FA</a> on the app side, <a href="https://bizr.net/ditch-google-self-hosting-searxng-behind-authelia-for-desktop-and-open-webui/">a self-hosted search engine</a> behind that same Authelia layer, and <a href="https://bizr.net/never-lock-yourself-out-building-a-self-updating-anti-lockout-rule-for-opnsense/">a dynamic anti-lockout rule</a> for the management plane underneath all of it. </p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://bizr.net/beyond-the-tunnel-adding-haproxy-acme-and-vlan-isolation-behind-my-opnsense-wireguard-setup/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Bypassing CGNAT with an OPNsense WireGuard Tunnel</title>
		<link>https://bizr.net/bypassing-cgnat-with-an-opnsense-wireguard-tunnel/</link>
					<comments>https://bizr.net/bypassing-cgnat-with-an-opnsense-wireguard-tunnel/#respond</comments>
		
		<dc:creator><![CDATA[Bizr]]></dc:creator>
		<pubDate>Mon, 25 May 2026 14:51:09 +0000</pubDate>
				<category><![CDATA[Networking]]></category>
		<category><![CDATA[CGNAT]]></category>
		<category><![CDATA[Homelab]]></category>
		<category><![CDATA[OPNsense]]></category>
		<category><![CDATA[Self-Hosting]]></category>
		<category><![CDATA[VPN]]></category>
		<category><![CDATA[VPS]]></category>
		<category><![CDATA[WireGuard]]></category>
		<guid isPermaLink="false">https://bizr.net/?p=2045</guid>

					<description><![CDATA[Stuck behind your ISP's CGNAT and can't expose your homelab services? Here's how I bypassed it using an OPNsense firewall and a WireGuard tunnel to a cheap VPS — no port forwarding required.]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">If you live in Costa Rica and run a home lab, you have probably hit the same wall I did: your ISP gives you a private IP behind Carrier-Grade NAT (CGNAT). No port forwarding, no inbound connections, no way to expose anything you host at home to the public internet. Calling support gets you nowhere — there is no static IP option on residential plans, and even business plans are inconsistent depending on the area.</p>



<p class="wp-block-paragraph">This post walks through the setup I ended up with: an OPNsense VM in Miami acting as a WireGuard server with a public IPv4, and my home lab in Cartago dialing out to it as a WireGuard client. Inbound traffic hits Miami, gets forwarded over the tunnel, and lands on services running in my house — just as if I had a normal public IP.</p>



<h2 class="wp-block-heading">What CGNAT actually breaks</h2>



<p class="wp-block-paragraph">Before fixing the problem, it helps to understand it.</p>



<p class="wp-block-paragraph">A normal residential ISP gives you a public IPv4 address on your router&#8217;s WAN interface. You can port-forward tcp/443 to a server inside, and anyone on the internet can reach it.</p>



<p class="wp-block-paragraph">With CGNAT, your router gets something like 100.64.x.x (the official CGNAT range, RFC 6598). The ISP then NATs many customers behind a smaller pool of real public IPs. You cannot port-forward anything because the public IP is not yours — it is shared, and the ISP controls the NAT table. Dynamic DNS does not help. UPnP does not help. The connection can only be opened from the inside out.</p>



<p class="wp-block-paragraph">This is fine for browsing Reddit. It is not fine if you want to host a blog, run a Plex server reachable from outside, expose Home Assistant to your phone over Tailscale-without-Tailscale, or — like me — run a private blog like bizr.net from a VM at home.</p>



<h2 class="wp-block-heading">The architecture</h2>



<p class="wp-block-paragraph">The fix is conceptually simple: rent a small VPS somewhere with a real public IP, and use it as a reverse proxy / tunnel endpoint. WireGuard is ideal because it is fast, stateless-feeling, and built into the Linux kernel.</p>



<pre class="wp-block-code has-ast-global-color-5-color has-text-color has-link-color wp-elements-74d4462f97b869d84cc4fd358b138ff5"><code>Internet
   │
   │ inbound :443, :80, etc.
   ▼
┌────────────────────────────────┐
│      Miami VPS (public IPv4)   │
│         OPNsense 25.x          │
│  – WireGuard server :51820     │
│  – NAT port-forward rules      │
└────────────┬───────────────────┘
             │ WireGuard tunnel (UDP 51820)
             │ 10.10.0.0/24
             ▼
┌────────────────────────────────┐
│   Home lab in Cartago, CR      │
│      Behind ISP CGNAT          │
│  WireGuard client on OPNsense  │
│   or directly on the server    │
└────────────────────────────────┘</code></pre>



<p class="wp-block-paragraph">Traffic to bizr.net resolves to the Miami public IP. Miami receives it on :443, a port-forward rule rewrites the destination to the home lab&#8217;s tunnel address (e.g. 10.10.0.2), and WireGuard pushes the packet down the encrypted tunnel to the house. The home server replies through the tunnel, Miami SNATs it back out, and the client never knows there was a hop.</p>



<h2 class="wp-block-heading">Why OPNsense on both ends</h2>



<p class="wp-block-paragraph">You can do this with plain <code>wg-quick</code> on a Linux VPS and iptables rules. I used to. It works. But once you have more than one service, the iptables rules get ugly, and troubleshooting at 2am from your phone is painful.</p>



<p class="wp-block-paragraph">OPNsense gives you a few things for free:</p>



<ul class="wp-block-list">
<li>a clean web UI for WireGuard peers, NAT rules, and firewall rules</li>



<li>proper state tracking and live connection visibility</li>



<li>aliases so you can group ports and IPs sanely</li>



<li>backups you can restore in five minutes if the VPS dies</li>



<li>the same mental model on both ends, since I run OPNsense at home too</li>
</ul>



<p class="wp-block-paragraph">The trade-off is RAM: OPNsense wants at least 2 GB. A 2 GB / 1 vCPU VPS in Miami runs about $6–$10/month depending on provider. I use Vultr because their Miami region has been stable for me and they support custom ISO uploads, which you need to install OPNsense.</p>



<h2 class="wp-block-heading">Provisioning the Miami VPS</h2>



<p class="wp-block-paragraph">A few things matter when picking the VPS:</p>



<ul class="wp-block-list">
<li><strong>Custom ISO support.</strong> OPNsense is not a one-click app on most providers. You will upload the ISO and install it yourself via the VNC console.</li>



<li><strong>A real public IPv4.</strong> Sounds obvious, but confirm it is not itself behind any provider NAT.</li>



<li><strong>Low latency to Costa Rica.</strong> Miami is the natural choice — most CR traffic already transits there. Expect 40–70 ms RTT.</li>



<li><strong>UDP not blocked.</strong> WireGuard is UDP. Some budget VPS providers shape or block non-TCP traffic. Vultr, Linode, Hetzner (their US locations), and DigitalOcean are all fine.</li>
</ul>



<p class="wp-block-paragraph">After installation, do the boring-but-critical steps first:</p>



<ol class="wp-block-list">
<li>set a strong root password and disable password SSH, key-only</li>



<li>enable the OPNsense firewall on the WAN with the default &#8220;block all inbound&#8221; stance</li>



<li>open only udp/51820 (WireGuard) and tcp/22 from your admin IP for now</li>



<li>update to the latest OPNsense release before configuring anything else</li>
</ol>



<h2 class="wp-block-heading">Setting up the WireGuard server in OPNsense (Miami)</h2>



<p class="wp-block-paragraph">In OPNsense, WireGuard lives under VPN → WireGuard. The model has two parts: an <strong>Instance</strong> (the local server) and <strong>Peers</strong> (the clients).</p>



<h3 class="wp-block-heading">1. Create the instance</h3>



<p class="wp-block-paragraph">Under VPN → WireGuard → Instances, add a new instance:</p>



<ul class="wp-block-list">
<li><strong>Name:</strong> <code>wg-cgnat-bypass</code></li>



<li><strong>Listen port:</strong> <code>51820</code></li>



<li><strong>Tunnel address:</strong> <code>10.10.0.1/24</code> — this is the Miami side of the tunnel</li>



<li><strong>MTU:</strong> 1420 is the sane default for WireGuard over Ethernet; drop to 1380 if you see weird packet loss</li>



<li><strong>Private key:</strong> click the cogwheel to auto-generate. Copy the public key — you will need it on the home side.</li>
</ul>



<p class="wp-block-paragraph">Save and enable.</p>



<h3 class="wp-block-heading">2. Add the home lab as a peer</h3>



<p class="wp-block-paragraph">Under Peers, add a new peer:</p>



<ul class="wp-block-list">
<li><strong>Name:</strong> <code>home-cartago</code></li>



<li><strong>Public key:</strong> the public key you generate on the home side (we will get to that)</li>



<li><strong>Allowed IPs:</strong> <code>10.10.0.2/32</code> — only let this peer use that single tunnel address</li>



<li><strong>Endpoint:</strong> leave empty. Miami should never try to dial home, because home is behind CGNAT and cannot accept inbound packets. Home dials Miami.</li>



<li><strong>Keepalive:</strong> leave empty on the server side. The client will send keepalives.</li>
</ul>



<p class="wp-block-paragraph">Link the peer to the instance and save.</p>



<h3 class="wp-block-heading">3. Assign the WireGuard interface</h3>



<p class="wp-block-paragraph">This is the step people miss and then spend three hours debugging.</p>



<p class="wp-block-paragraph">Go to Interfaces → Assignments, find the <code>wg0</code> (or whatever WireGuard created) device in the dropdown, click + to assign it. Open the new interface, enable it, leave the IPv4 configuration on &#8220;None&#8221; (the tunnel address is set on the instance, not here), and save + apply.</p>



<p class="wp-block-paragraph">You now have a real interface OPNsense can write firewall and NAT rules against. Without this assignment, the tunnel will come up but you cannot route anything through it from the GUI.</p>



<h3 class="wp-block-heading">4. Firewall rule on the WireGuard interface</h3>



<p class="wp-block-paragraph">Under Firewall → Rules → WireGuard (the new tab that appeared after assigning), add a rule:</p>



<ul class="wp-block-list">
<li><strong>Action:</strong> Pass</li>



<li><strong>Protocol:</strong> any</li>



<li><strong>Source:</strong> <code>10.10.0.0/24</code></li>



<li><strong>Destination:</strong> any</li>
</ul>



<p class="wp-block-paragraph">This allows traffic from the tunnel into Miami. Without it, packets arrive but get dropped.</p>



<h2 class="wp-block-heading">Setting up the home side</h2>



<p class="wp-block-paragraph">Two options here:</p>



<ol class="wp-block-list">
<li>run WireGuard directly on the server you want to expose (simplest, one service)</li>



<li>run WireGuard on your home OPNsense and route inbound traffic to internal LAN hosts (cleaner, scales to many services)</li>
</ol>



<p class="wp-block-paragraph">I run it on home OPNsense. The setup mirrors Miami:</p>



<ul class="wp-block-list">
<li>Instance, tunnel address <code>10.10.0.2/24</code>, no listen port required (it dials out)</li>



<li>Peer for Miami:
<ul class="wp-block-list">
<li><strong>Public key:</strong> the Miami instance public key</li>



<li><strong>Allowed IPs:</strong> <code>10.10.0.0/24</code> — and add <code>0.0.0.0/0</code> only if you want to route all internet traffic out through Miami (I do not — adds latency and burns VPS bandwidth)</li>



<li><strong>Endpoint:</strong> <code>your.miami.public.ip:51820</code></li>



<li><strong>Keepalive:</strong> 25 seconds — this is the magic number. The home side must send something every ~30s or the ISP&#8217;s CGNAT mapping expires and inbound packets stop arriving</li>
</ul>
</li>
</ul>



<p class="wp-block-paragraph">Generate the home keypair, paste the public key back into Miami&#8217;s peer config, save both sides.</p>



<p class="wp-block-paragraph">Assign the WireGuard interface on home OPNsense too, enable it, add a firewall rule on the tunnel interface allowing <code>10.10.0.0/24</code> to your LAN destinations (or just <code>any</code> while testing).</p>



<p class="wp-block-paragraph">Within ~30 seconds of enabling both sides, the handshake should complete. Check VPN → WireGuard → Status in Miami — you should see a recent handshake timestamp and bytes in/out incrementing.</p>



<p class="wp-block-paragraph">If it does not handshake: 99% of the time it is a firewall rule, an MTU issue, or a public key copy-paste with a stray newline.</p>



<h2 class="wp-block-heading">Forwarding inbound traffic to the home lab</h2>



<p class="wp-block-paragraph">Now the actual point of the exercise. Let&#8217;s say bizr.net resolves to the Miami public IP and runs on a <a href="https://bizr.net/from-app-logs-to-firewall-blocks-building-a-fail2ban-to-opnsense-ban-pipeline/">Caddy reverse proxy</a> on a home LAN server at <code>192.168.10.20:443</code> — the same Caddy that ends up doing reverse-proxy and forward-auth duty for several other services later in this series, including <a href="https://bizr.net/ditch-google-self-hosting-searxng-behind-authelia-for-desktop-and-open-webui/">a self-hosted SearXNG instance behind Authelia</a>. </p>



<p class="wp-block-paragraph">On Miami OPNsense, go to Firewall → NAT → Port Forward and add:</p>



<ul class="wp-block-list">
<li><strong>Interface:</strong> WAN</li>



<li><strong>Protocol:</strong> TCP</li>



<li><strong>Destination:</strong> WAN address</li>



<li><strong>Destination port range:</strong> 443</li>



<li><strong>Redirect target IP:</strong> <code>192.168.10.20</code> (the LAN IP of the home server — Miami will route this through the tunnel because the home LAN is reachable via WireGuard)</li>



<li><strong>Redirect target port:</strong> 443</li>



<li><strong>NAT reflection:</strong> disable</li>



<li>Tick &#8220;Filter rule association: Add associated filter rule&#8221; so OPNsense auto-creates the pass rule on WAN.</li>
</ul>



<p class="wp-block-paragraph">Two more things make this actually work:</p>



<ul class="wp-block-list">
<li>On Miami, under System → Routes → Configuration, make sure there is a route for your home LAN (e.g. <code>192.168.10.0/24</code>) via the WireGuard interface&#8217;s tunnel gateway. OPNsense usually figures this out from the peer&#8217;s Allowed IPs, but verify.</li>



<li>On the home OPNsense, under Peer → Allowed IPs for the Miami peer, include <code>10.10.0.0/24</code> so return traffic knows to go back through the tunnel. If you want Miami to be able to reach your whole home LAN, you would also add <code>192.168.10.0/24</code> to the Miami peer&#8217;s Allowed IPs on the Miami side — but that gives Miami access to your LAN, so think about whether you want that.</li>
</ul>



<p class="wp-block-paragraph">Repeat the port-forward block for any other service. I run one for :80 (redirect to https), one for :443, and a non-standard port for SSH that I rate-limit aggressively.</p>



<h2 class="wp-block-heading">Performance, latency, and what to expect</h2>



<p class="wp-block-paragraph">A few numbers from my setup:</p>



<ul class="wp-block-list">
<li><strong>Latency overhead:</strong> Cartago → Miami direct is ~55 ms. Through the tunnel it is ~60 ms. WireGuard&#8217;s overhead is genuinely tiny.</li>



<li><strong>Throughput:</strong> I get about 180–220 Mbps through the tunnel on a 300/300 home line. The bottleneck is OPNsense CPU on a 1 vCPU VPS doing the crypto. If you need more, give the VPS 2 vCPUs and you will saturate a gigabit.</li>



<li><strong>TTFB to bizr.net from outside CR:</strong> ~80–120 ms. Perfectly fine for a blog. For latency-sensitive things (gaming, voice), think harder about the geography.</li>
</ul>



<p class="wp-block-paragraph">These numbers are also why the WireGuard tunnel ends up being the <em>primary</em> path for management traffic between the two sites rather than just a traffic-forwarding pipe — low enough latency, stable enough handshake, that it&#8217;s trustworthy for day-to-day admin work too, not only for routing bizr.net&#8217;s HTTPS traffic.</p>



<h2 class="wp-block-heading">Things I wish I had known sooner</h2>



<ul class="wp-block-list">
<li><strong>The keepalive on the client side is non-negotiable.</strong> Without it, the CGNAT mapping will drop after a few minutes of idle and the tunnel goes one-way-dead. This is the exact same CGNAT-mapping fragility that shows up again, from a different angle, in <a href="https://bizr.net/never-lock-yourself-out-building-a-self-updating-anti-lockout-rule-for-opnsense/">Never Lock Yourself Out</a> — there, the problem isn&#8217;t a dropped tunnel but a dynamic-DNS allowlist that needs to track a home IP CGNAT keeps reassigning. Same underlying instability, two different symptoms depending on which side of the connection you&#8217;re standing on.</li>



<li><strong>Do not enable NAT reflection on the Miami WAN.</strong> It causes weird loops when you try to reach bizr.net from inside the tunnel.</li>



<li><strong>Back up your OPNsense config weekly</strong> (System → Configuration → Backups). The whole reason this is resilient is that the VPS is replaceable in 20 minutes if you have a recent config.</li>



<li><strong>Use a DNS provider with low TTL on the A record pointing to Miami.</strong> If you ever need to move VPS providers, you want propagation in minutes, not hours.</li>



<li><strong>Monitor the handshake.</strong> I have a small script that hits the WireGuard status API every 5 minutes and pages me if the last handshake is older than 3 minutes. Saved me twice.</li>
</ul>



<h2 class="wp-block-heading">Why not Cloudflare Tunnel or Tailscale Funnel?</h2>



<p class="wp-block-paragraph">Both are great, and for a lot of people they are the right answer. I picked the OPNsense + WireGuard route because:</p>



<ul class="wp-block-list">
<li>I want the public IP to be mine, not a CDN&#8217;s. Some services I run do not like being behind Cloudflare.</li>



<li>I want raw TCP/UDP on arbitrary ports, not just HTTP(S).</li>



<li>I already run OPNsense and enjoy understanding the whole stack end to end. This is a homelab blog. The point is partly to learn.</li>
</ul>



<p class="wp-block-paragraph">If you just want to expose a single web service and never think about it again, Cloudflare Tunnel is one <code>cloudflared</code> install away and is genuinely excellent. Pick the boring solution when it fits.</p>



<h2 class="wp-block-heading">Wrapping up</h2>



<p class="wp-block-paragraph">CGNAT used to feel like a hard limit on what I could do from my home lab in Costa Rica. It is not — it is a $7/month and one weekend problem. The setup above has been running for a while now with effectively zero unplanned downtime, and it is the foundation for everything else I host, including this blog.</p>



<p class="wp-block-paragraph">Next post I wrote about the reverse-proxy layer that sits behind this — <a href="https://bizr.net/beyond-the-tunnel-adding-haproxy-acme-and-vlan-isolation-behind-my-opnsense-wireguard-setup/">HAProxy with automatic ACME, internal-only services on a separate VLAN, and how I think about exposing things safely</a>. From there, the same DMZ this tunnel feeds eventually picks up <a href="https://bizr.net/hardening-your-homelabs-public-edge-geoip-blocking-crowdsec-and-syncing-fail2ban-with-opnsense/">GeoIP blocking, CrowdSec, and a fail2ban sync to the firewall edge</a>, and the apps living behind it get <a href="https://bizr.net/one-login-to-rule-them-all-wiring-authelia-as-sso-for-open-webui-and-nextcloud/">Authelia as a shared login layer</a> with <a href="https://bizr.net/locking-down-authelia-totp-real-smtp-and-the-hostname-vs-ip-tls-trap/">real two-factor authentication</a>. Every one of those posts assumes this tunnel is already up and stable — which, in practice, has turned out to be a safe assumption to make. </p>



<p class="wp-block-paragraph">If you have questions or are stuck on a specific part of the setup, the contact form on bizr.net reaches me directly.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>
]]></content:encoded>
					
					<wfw:commentRss>https://bizr.net/bypassing-cgnat-with-an-opnsense-wireguard-tunnel/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
