8 min read

Mastering WireGuard: Site-to-Site & Road Warrior Setups Behind OPNsense NAT

Stop running WireGuard on your firewall. Learn how to decouple your VPN using Docker or Linux VMs behind OPNsense for better performance, portability, and Site-to-Site routing.
Gloved hands hold a transparent tablet displaying a holographic network diagram where a secure WireGuard tunnel bypasses a red firewall shield in a server room.
Punching a secure hole through the fortress: A visualized WireGuard tunnel bypassing the edge firewall to connect directly to an internal host.

Introduction: Why Decouple Your VPN?

Welcome back to the lab. If you’ve followed my OPNsense series, you’ve built a fortress. You have VLANs, Intrusion Detection, and a strict firewall ruleset. But sometimes, you don't want your firewall to handle everything.

Maybe you want to run a cutting-edge version of WireGuard. Maybe you want to easily migrate your VPN server between hardware without touching your firewall config. Or maybe, like me, you just love Docker containers!

In this guide, we are going to do something advanced. We are going to punch a precise, secure hole in our OPNsense fortress to allow an internal Linux Host (running Docker or a VM) to act as our VPN Gateway.

We will cover two critical scenarios:

  1. The Road Warrior: Your phone/laptop connecting securely back to your home LAN from a coffee shop.
  2. The Mesh Peer (Site-to-Site): Connecting your lab to another network (like a friend's lab or a cloud VPS) so devices on both sides can talk to each other.

Let’s get to work.


Part 1: The Docker Method 🐳

For those who want lightweight, portable speed.

Prerequisites

  • A Linux host running Docker & Docker Compose (Ubuntu/Debian recommended).
  • OPNsense running as your edge firewall.
  • A static IP on your Docker host (e.g., 10.10.1.50).

Step 1: Prepare the Docker Host (Routing is Key!)

By default, Linux does not forward packets between interfaces. If you want to access other devices on your LAN (Scenario A) or bridge two networks (Scenario B), your host must act as a router.

  1. SSH into your Linux/Docker host.
  2. Edit the sysctl configuration:
# Create a persistent config file
sudo nano /etc/sysctl.d/99-wireguard.conf
  1. Paste the following to enable forwarding:
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
One small line for a config file, one giant leap for your packets.
One small line for a config file, one giant leap for your packets.
  1. Apply the changes immediately:
sudo sysctl -p /etc/sysctl.d/99-wireguard.conf
Core Lab Tip: I also add in some optional "optimizations"!

If you'd like to give it a whirl, do this:

  1. sudo nano /etc/sysctl.d/wireguard-tuning.conf
  2. Paste this in the new file:
# 1. Enable BBR for better speed over internet
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr

# 2. Increase UDP Buffer sizes (Critical for WireGuard/Tailscale speed)
net.core.rmem_max = 7500000
net.core.wmem_max = 7500000

# 3. Optimize ARP cache (Since you are routing a subnet)
net.ipv4.neigh.default.gc_thresh1 = 512
net.ipv4.neigh.default.gc_thresh2 = 2048
net.ipv4.neigh.default.gc_thresh3 = 4096

Step 2: The Docker Compose Stack (Easy way!)

We will use the excellent linuxserver/wireguard image. It’s stable, supports multiple architectures, and handles keys gracefully.

Create a folder for your project and a docker-compose.yml:

services:
  wireguard:
    image: lscr.io/linuxserver/wireguard:latest
    container_name: wireguard
    cap_add:
      - NET_ADMIN
      - SYS_MODULE # Optional: Only if using host kernel modules
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=America/New_York
      - SERVERURL=vpn.corelab.tech # Your DDNS or Static WAN IP
      - SERVERPORT=51820
      - PEERS=3 # Number of client configs to generate (Phone, Laptop, Tablet)
      - PEERDNS=1.1.1.1 # Or use your PiHole/AdGuard IP (e.g. 10.10.1.5)
      - INTERNAL_SUBNET=10.13.13.0 # The VIP range for the VPN tunnel
      - ALLOWEDIPS=0.0.0.0/0 # For Road Warriors (Tunnel Everything)
    volumes:
      - ./config:/config
      - /lib/modules:/lib/modules # Optional: For kernel module access
    ports:
      - 51820:51820/udp
    sysctls:
      - net.ipv4.conf.all.src_valid_mark=1
    restart: unless-stopped

Launch the stack:

docker compose up -d

Check the logs to see the QR codes generated for your peers!

docker compose logs -f wireguard
Voila! Scan the QR codes with your client app on your phone!
  • Phone: Scan the QR code from the terminal.
  • Laptop: You can't scan a screen with the same screen. Navigate to ./config/peer1/ and copy the text from peer1.conf into your WireGuard client.

Step 3: OPNsense Configuration (Opening the Gates)

Your container is listening, but your fortress (OPNsense) is currently blocking the door. We need to forward the traffic.

A. Port Forwarding (NAT)

  1. Navigate to Firewall -> NAT -> Port Forward.
  2. Click Add (+).
    • Interface: WAN
    • Protocol: UDP
    • Destination: WAN address
    • Destination Port Range: 51820
    • Redirect Target IP: 10.10.1.50 (Your Docker Host IP)
    • Redirect Target Port: 51820
    • Log: Checked (Optional, recommended for initial troubleshooting)
    • Filter Rule Association: Add associated filter rule.
  3. Save and Apply.

⚠️ Verification: Go to Firewall -> Rules -> WAN and ensure the rule created by the NAT wizard is present and enabled (Green play icon).

At this point, Scenario A (Road Warrior) works! Your phone can connect, and because we enabled IP Forwarding on the host, you can browse your LAN.


Step 4: Scenario B - Site-to-Site (The "Mesh" Config)

Advanced Level: Connecting two labs!!!

If this Docker container is connecting to a remote peer (e.g., a friend's network 192.168.99.0/24) and you want your entire LAN to reach it, we need Static Routes.

  1. Edit wg0.conf: Stop the container and edit config/wg0.conf. Add a peer block for the remote site:
[Peer]
# The Remote Site
PublicKey = <Remote_Public_Key>
Endpoint = remote.lab-friend.com:51820
# Allow traffic to the VPN IP AND the Remote LAN
AllowedIPs = 10.13.13.2/32, 192.168.99.0/24
PersistentKeepalive = 25
  1. Tell OPNsense the Path Exists: OPNsense knows about your LAN (10.10.1.0/24), but it has no idea that 192.168.99.0/24 exists inside that Docker container.
    • System -> Gateways -> Configuration: Add a new Gateway.
      • Name: Docker_WG_GW
      • Interface: LAN
      • IP Address: 10.10.1.50 (Docker Host IP)
Setting OPNsense up to route & allow your wireguard nets through the container host.
    • System -> Routes -> Configuration: Add a Static Route.
      • Network: 192.168.99.0/24 (The Remote Lab)
      • Gateway: Docker_WG_GW
The "Return Trip" is usually where people fail. OPNsense needs a map (Static Route) to find the remote network.

Part 2: The Full VM Method 🖥️

For those who want maximum kernel performance and cleaner routing.

Sometimes Docker networking (NAT inside NAT) gets messy. Running WireGuard on a dedicated micro-VM (like a Proxmox LXC or a small Ubuntu VM) removes the Docker abstraction layer.

Step 1: Install WireGuard

On your Ubuntu/Debian VM (10.10.1.60):

sudo apt update && sudo apt install wireguard resolvconf -y

Enable IP Forwarding just like we did in the Docker step (sysctl.conf).

Step 2: Configure the Server

Create your config file:

sudo nano /etc/wireguard/wg0.conf

The Config (Server Mode):

[Interface]
Address = 10.13.13.1/24
ListenPort = 51820
PrivateKey = <Server_Private_Key>
# The Magic Masquerade Rule (Required for returning traffic!)
# This ensures that traffic leaving the VM appears to come from the VM's IP,
# solving routing issues for lazy clients.
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

[Peer]
# Road Warrior Client (Phone)
PublicKey = <Phone_Public_Key>
AllowedIPs = 10.13.13.2/32

Start it up:

sudo wg-quick up wg0
sudo systemctl enable wg-quick@wg0

Step 3: OPNsense Integration

This is identical to the Docker method.

  1. Port Forward UDP 51820 to the VM IP (10.10.1.60).
  2. Static Routes (for Site-to-Site) pointing to the VM IP.

Part 3: Performance Tweaks & "The Need for Speed" 🚀

Whether you use Docker or a VM, WireGuard is fast. But we can make it faster.

1. MTU Tuning (The #1 Fix for "It connects but websites won't load")

When you wrap a packet inside a packet (VPN), it gets bigger. If it exceeds 1500 bytes, it fragments, causing massive slowdowns.

  • The Safe Bet: Set MTU = 1280 in your [Interface] block. This is the WireGuard minimum and works on almost every connection type (LTE, Starlink, Fiber).
  • The Optimized Bet: Try 1420.

2. Userspace vs. Kernel Mode (Docker Specific)

By default, Docker containers run WireGuard in "Userspace" (using Go). This is fine for 100-500Mbps. If you have Gigabit fiber, you want Kernel Mode.

  • How: Ensure your Docker Host has the wireguard module installed (sudo apt install wireguard).
  • Compose: In your docker-compose.yml, ensure you have:
volumes:
  - /lib/modules:/lib/modules
cap_add:
  - NET_ADMIN
  - SYS_MODULE
  • The container will detect the host's kernel module and use it automatically, drastically reducing CPU usage!
Kernel mode leaves Userspace in the dust for high-bandwidth applications.

3. Persistent Keepalive (NAT Survival)

If your connection drops silently after a few minutes of inactivity, your NAT router (or OPNsense) is closing the state.

  • Fix: Add PersistentKeepalive = 25 to your [Peer] configs. This sends a tiny "heartbeat" ping every 25 seconds to keep the firewall tunnel open.

Conclusion

You have now successfully decoupled your VPN from your edge firewall. You can update, restart, or nuke your WireGuard instance without ever taking your main internet connection offline. You can also connect to a friend's network across the world with a secure, performant and private VPN, 100% of your own creation! #PowerOverwhelming!

Your OPNsense handles the security policy, and your Linux host handles the heavy cryptographic lifting.

This is the Way!

What's Next? Now that you have a secure tunnel, why not use it to access your local LLMs remotely? Check out my guide on hosting local AI safely!


Now you can move on to enabling the "all seeing eye" of your LANs with Zenarmor, part 3 of the OPNsense series.

OPNsense Layer-7 Control: A Deep Dive into Zenarmor (Part 3)
In Part 1, we built the firewall (Layer 3/4). In Part 2, we hardened it with user accounts, 2FA, blocklists, and CrowdSec. Now, it’s time to add Layer 7 (Application) awareness. Your firewall is great at blocking IPs and ports, but it has no idea what is running inside

Or maybe keep things light and leave wireguard on OPNsense directly?

The Speed of Light: OPNsense WireGuard Setup Guide
Merry Christmas everyone! A little Christmas “treat” for all... You’ve built the fortress (OPNsense). Now you need a secure, invisible tunnel to get back inside when you’re on the road, travelling for work or on vacation! For many years, OpenVPN was the standard. But it’s slow, bloated,