Andrew Mercer
on this page

A complete, from-scratch guide to building a WireGuard road-warrior VPN so a laptop can securely reach services on an OPNsense-fronted home lab from anywhere.

1. Prerequisites & Network Plan

What you need:

  • OPNsense running as your home lab's router/firewall, with a stable WAN IP (or a Dynamic DNS hostname if your ISP assigns a dynamic IP)
  • Admin access to the OPNsense web UI
  • A laptop running Windows, macOS, or Linux
  • Router/firewall port forwarding capability (you'll forward one UDP port to OPNsense)
  • 10–15 minutes

How WireGuard fits in: WireGuard is a lightweight, kernel-level VPN protocol. Unlike OpenVPN, it uses a small, fixed set of UDP packets and public-key authentication (no certificates, no CA to manage). Each side — OPNsense and the laptop — holds a private/public keypair, and each side lists the other's public key as an authorized peer. Traffic is encrypted point-to-point; OPNsense then routes decrypted traffic to your LAN like any other client.

IP addressing plan (adjust to avoid conflicts with your existing LAN):

Purpose Value
WireGuard tunnel subnet 10.10.10.0/24
OPNsense tunnel address 10.10.10.1/24
Laptop tunnel address 10.10.10.2/32
Home LAN subnet (example) 192.168.1.0/24
WireGuard listen port 51820/UDP

The tunnel subnet is a separate address space from your LAN — it exists only between OPNsense and its VPN peers. OPNsense will route between the two.

Dynamic DNS note: if your home ISP connection doesn't have a static IP, set up a DDNS hostname (OPNsense has a built-in Dynamic DNS service under Services → Dynamic DNS) so the laptop always has a stable endpoint to connect to, even if your public IP changes.

2. OPNsense: WireGuard Server Setup

Step 1 — Install the plugin (if not already present). As of recent OPNsense releases, WireGuard is a core service under VPN → WireGuard (older versions need os-wireguard installed via System → Firmware → Plugins).

Step 2 — Create the local (server) instance. 1. Go to VPN → WireGuard → Local (or Instances) → Add. 2. Name it something like wg0-roadwarrior. 3. Click Generate to create OPNsense's own keypair — it stores the private key internally and shows you the public key. Copy that public key somewhere; you'll need it for the laptop config. 4. Set Listen Port to 51820. 5. Set Tunnel Address to 10.10.10.1/24. 6. Leave Peers empty for now — you'll add the laptop as a peer after generating its keys. 7. Save.

Step 3 — Generate a keypair for the laptop (client). You can generate this on OPNsense itself (Services → WireGuard often has a key generator) or later on the laptop directly with wg genkey. Either way you need: - Laptop private key (goes in the laptop's config only — never share it) - Laptop public key (goes into OPNsense's peer entry)

Step 4 — Add the laptop as a peer on OPNsense. 1. Go to VPN → WireGuard → Endpoints/Peers → Add. 2. Name: laptop. 3. Public Key: paste the laptop's public key. 4. Allowed IPs: 10.10.10.2/32 — this tells OPNsense which tunnel address this peer is allowed to use. 5. Leave Endpoint blank (the laptop is the one initiating the connection, usually from a changing IP). 6. Save, then edit your local instance from Step 2 and attach this peer to it.

Step 5 — Enable the service. Go to VPN → WireGuard → General, check Enable WireGuard, and apply.

3. Firewall & NAT Rules

OPNsense won't route or accept anything for the tunnel until you add rules — this is the step people most often forget.

Step 1 — Assign the WireGuard interface. 1. Interfaces → Assignments, assign the wg0-roadwarrior device as a new interface (e.g. name it WIREGUARD). 2. Enable the interface, no need to set an IP (WireGuard already has the tunnel address).

Step 2 — Allow WAN traffic to reach the WireGuard port. 1. Firewall → Rules → WAN → Add. 2. Protocol: UDP, Destination: WAN address, Destination port: 51820. 3. Description: Allow WireGuard. Save and apply.

Step 3 — Port forward (only if OPNsense is behind another router/modem). If OPNsense's WAN interface is directly on the public internet, skip this. If there's an ISP modem/router in front of it, forward UDP 51820 to OPNsense's WAN IP on that upstream device.

Step 4 — Allow traffic from the VPN peers. 1. Firewall → Rules → WIREGUARD (the interface tab you created) → Add. 2. Source: 10.10.10.0/24 (or narrower, just the laptop's /32 if you want to be strict). 3. Destination: your LAN subnet (192.168.1.0/24) or any, depending how much access you want the laptop to have. 4. Save and apply.

Step 5 — NAT (usually not required). Because both the tunnel and LAN are private routed subnets behind OPNsense, you typically do not need an outbound NAT rule for VPN → LAN traffic — OPNsense routes it natively as long as the firewall rule in Step 4 allows it. Only add an outbound NAT rule if your LAN devices need to reply to the laptop and don't have a route back to 10.10.10.0/24 (rare, since OPNsense is already their gateway).

4. Laptop Client Setup

Install the WireGuard client: - Linux: sudo apt install wireguard (Debian/Ubuntu) or sudo dnf install wireguard-tools (Fedora); Arch: pacman -S wireguard-tools - macOS: brew install wireguard-tools, or the official WireGuard app from the Mac App Store - Windows: download the official installer from wireguard.com/install

Generate the laptop's keypair (if you didn't already generate it on OPNsense in step 2 above):

wg genkey | tee laptop-private.key | wg pubkey > laptop-public.key

This prints the private key to laptop-private.key and derives the public key into laptop-public.key. Copy the public key into OPNsense's peer entry (Section 2, Step 4) if you haven't already.

Get OPNsense's public key and confirm your DDNS hostname or static WAN IP — you generated the server's public key in Section 2, Step 2.

Client config file (wg0.conf on Linux, or import via the GUI app on macOS/Windows):

[Interface]
PrivateKey = <laptop-private-key-here>
Address = 10.10.10.2/32
DNS = 192.168.1.1

[Peer]
PublicKey = <opnsense-public-key-here>
Endpoint = your-ddns-hostname.example.com:51820
AllowedIPs = 192.168.1.0/24, 10.10.10.0/24
PersistentKeepalive = 25

Notes on the config: - AllowedIPs controls what gets routed through the tunnel. The example above only routes home-lab-bound traffic through the VPN (a split tunnel) — everything else (general internet browsing) stays on your normal connection. If you want all traffic to route through home (a full tunnel, useful on public Wi-Fi — see the companion hotspot-safety guide), set AllowedIPs = 0.0.0.0/0, ::/0 instead. - PersistentKeepalive = 25 sends a keepalive packet every 25 seconds — important because the laptop is usually behind NAT (hotel Wi-Fi, mobile hotspot) and needs to keep that NAT mapping alive so OPNsense can still reach it. - DNS (optional) points DNS queries through the tunnel to your OPNsense/router so you can resolve internal hostnames.

Bring up the connection: - Linux: sudo wg-quick up ./wg0.conf - macOS/Windows: import the .conf file into the WireGuard app and click Activate

5. Testing & Verification

1. Check the handshake. On the laptop:

sudo wg show

Look for a latest handshake timestamp a few seconds old. No handshake at all usually means the UDP packet isn't reaching OPNsense (check port forwarding, WAN firewall rule, or a mismatched public key).

2. Check from OPNsense. VPN → WireGuard → Status (or Diagnostics) should show the peer with a recent handshake and non-zero received/sent bytes.

3. Ping the tunnel itself.

ping 10.10.10.1

If this fails, the tunnel isn't up — recheck keys, endpoint, and the WAN firewall rule before moving on.

4. Ping something on the LAN (e.g. OPNsense's LAN IP or another home lab host):

ping 192.168.1.1

If the tunnel ping (step 3) works but this fails, the problem is the firewall rule on the WIREGUARD interface (Section 3, Step 4) or that OPNsense doesn't know to route back — double check AllowedIPs on the OPNsense peer entry includes exactly 10.10.10.2/32.

5. Confirm real access. Try reaching an actual home lab service by IP or internal hostname (SSH, a web dashboard, etc.) to confirm end-to-end connectivity, not just ICMP.

6. Troubleshooting & Hardening

Common issues:

Symptom Likely cause
No handshake at all Wrong public key on either side; endpoint hostname/port wrong; UDP 51820 not forwarded/allowed on WAN
Handshake succeeds, no ping to 10.10.10.1 Tunnel address mismatch, or a local firewall on the laptop blocking WireGuard
Ping to tunnel works, LAN ping fails Firewall rule missing on the WIREGUARD interface, or peer's AllowedIPs too narrow on OPNsense
Works at home, fails on other networks The other network blocks outbound UDP (some corporate/public Wi-Fi networks do) — consider also enabling WireGuard over a fallback like port 443/UDP, or use wg-quick's Table = off with custom routes if policy routing is an issue
Connection drops after a while Missing PersistentKeepalive, or a NAT timeout shorter than 25s upstream — try lowering it to 15

Hardening recommendations:

  • Rotate keys periodically, especially if a laptop is ever lost or stolen — just generate a new keypair and update both sides.
  • Restrict the OPNsense peer's AllowedIPs to the exact /32 tunnel address (already done above) so a compromised peer's traffic can't be tunnel-address-spoofed as another peer.
  • Scope the WIREGUARD interface firewall rule (Section 3, Step 4) as narrowly as your use case allows — e.g. only SSH and a couple of dashboard ports, rather than blanket access to the whole LAN.
  • Keep OPNsense and its WireGuard component patched — check System → Firmware → Updates regularly.
  • Consider a second WireGuard peer/keypair per device instead of reusing one identity across your laptop, phone, etc. — makes revocation clean if one device is lost.
  • Log and periodically review VPN connections via VPN → WireGuard → Status or the OPNsense logs to catch unexpected peers or handshakes.

7. Routing Only Browser Traffic (Windows, Firefox & Chrome)

If you don't want a full-system tunnel, but just want Firefox/Chrome routed through the lab while everything else uses your normal connection, layer a SOCKS5 proxy on top of the existing split-tunnel WireGuard connection.

Windows WireGuard has no built-in per-app routing, so the clean approach is: keep the WireGuard split tunnel as-is (Section 4), then run a SOCKS proxy over it and point just the browsers at that proxy.

Step 1 — Stand up a SOCKS proxy reachable over the tunnel. Easiest is SSH dynamic port forwarding to a box in the lab — no extra server software needed since Windows 10/11 ship the OpenSSH client:

ssh -D 1080 -N andrew@192.168.1.x

This opens a local SOCKS5 proxy at 127.0.0.1:1080 that tunnels through SSH, which itself rides the existing WireGuard tunnel (10.10.10.0/24 → 192.168.1.0/24). Leave that terminal/window running while browsing.

Step 2 — Point Firefox at it. 1. Settings → General → Network Settings → Settings… 2. Manual proxy configuration, SOCKS Host 127.0.0.1, Port 1080, SOCKS v5 3. Check Proxy DNS when using SOCKS v5 — without this, DNS queries leak outside the tunnel even though the traffic itself is proxied.

Step 3 — Point Chrome at it. Chrome has no per-browser proxy UI on its own — it inherits the system proxy unless launched with a flag. Make a dedicated shortcut just for tunneled browsing so your normal Chrome icon is untouched:

chrome.exe --proxy-server="socks5://127.0.0.1:1080"

Chromium generally sends DNS through the SOCKS5 proxy by default, but verify with a leak test rather than assuming.

Step 4 — Verify no leaks. With the proxy active, check browserleaks.com/ip and browserleaks.com/dns in both browsers — the IP shown should be your home WAN IP, not the local network's.

If this becomes routine: rather than an ad-hoc ssh -D session you have to remember to start, run a small persistent SOCKS server (microsocks or dante-server) as a container in the lab, reachable the same way over the WireGuard tunnel.

8. In-Depth Troubleshooting

This section covers the failure modes that don't show up until you're actually configuring OPNsense — mismatches between what the GUI shows, what's saved in config.xml, and what's actually loaded and enforced by the packet filter.

8.1 The interface won't show a tab under Firewall → Rules

A newly assigned interface only gets a Rules tab once it is both assigned (Interfaces → Assignments) and enabled (Interfaces → [that interface] → Enable Interface checkbox, saved separately). These are two different save actions on two different pages — assigning it does not enable it.

Confirm via CLI whether it's actually enabled:

awk '/<interfaces>/,/<\/interfaces>/' /conf/config.xml

Look at your interface's block (e.g. <opt1>). If it looks like this, it is not enabled — the tag is simply missing, regardless of what the checkbox appeared to show you:

<opt1>
  <descr>wg0</descr>
  <if>wg0</if>
</opt1>

It should instead read:

<opt1>
  <descr>wg0</descr>
  <if>wg0</if>
  <enable>1</enable>
</opt1>

Fix through the GUI (preferred): go to Interfaces → [OPT1] (the specific interface page, not the Assignments list), check Enable Interface, Save, then Apply Changes.

Fix via CLI: back up first, then hand-edit:

cp /conf/config.xml /conf/config.xml.bak-$(date +%F)
vi /conf/config.xml
# add <enable>1</enable> inside the relevant <optN> block
configctl interface reconfigure opt1
configctl webgui restart

If the tag is already present and it still doesn't show a tab, the GUI's tab list can go stale — restart the web process to force it to rebuild:

configctl webgui restart

8.2 The "WireGuard (Group)" interface vs. your own instance interface

OPNsense auto-creates a virtual group interface (<if>wireguard</if>, described as "WireGuard (Group)") that catches traffic from any WireGuard instance collectively, and it's enabled by default. This is separate from the specific interface you manually assigned to one instance (e.g. <if>wg0</if>, described as whatever name you gave it — often literally "wg0").

Two config.xml blocks, easy to conflate:

<wireguard>
  <descr>WireGuard (Group)</descr>
  <if>wireguard</if>
  <enable>1</enable>
  ...
</wireguard>
<opt1>
  <descr>wg0</descr>
  <if>wg0</if>
  <!-- needs <enable>1</enable> added -->
</opt1>

Both can appear as separate tabs under Firewall → Rules once both are enabled — "WireGuard (Group)" and whatever your optN interface is described as (e.g. "wg0"). A rule on the Group tab matches traffic from any WireGuard instance; a rule on your specific instance's tab matches only that instance. If you only ever plan to run one instance, using the Group tab is a perfectly valid shortcut and sidesteps the enable-flag issue in 8.1 entirely, since it's enabled out of the box.

Check which tabs actually exist right now:

awk '/<filter>/,/<\/filter>/' /conf/config.xml | grep -i "<interface>\|<descr>"

This lists the interface each saved rule targets alongside its description, so you can see exactly which tab(s) already have rules and which are still empty.

8.3 "X is not a valid source/destination alias" when saving a rule

This error means a literal interface name (like opt1) or some other raw string was typed into a field that OPNsense expects to be one of: a CIDR/IP, a network macro (e.g. "wg0 net", "LAN net"), or a defined alias (Firewall → Aliases).

Fix: 1. On the Source (or Destination) row, use the type dropdown first — set it to Network. 2. Then in the value field that appears, either pick the matching " net" option (e.g. "wg0 net") if OPNsense offers it, or type the CIDR directly (10.10.10.0/24, 192.168.1.0/24). 3. Never type an interface's internal name (opt1, wan, lan) into these fields — internal names are not valid rule values on their own; only their macro form (<name> net, <name> address) is.

8.4 Confirming what's saved vs. what's actually loaded

config.xml reflects what you've saved; it does not guarantee the packet filter has actually loaded it. These can diverge after a bad apply, a version-specific bug, or an interface that came up in the wrong order at boot.

What's saved (rules):

awk '/<filter>/,/<\/filter>/' /conf/config.xml

What's saved (WireGuard instances/peers):

awk '/<wireguard>/,/<\/wireguard>/' /conf/config.xml

What's actually loaded and enforced right now:

pfctl -sr -vv | grep -B2 -A2 -i wireguard

Whether the interfaces are actually up:

ifconfig wg0
ifconfig wireguard

If pfctl -sr doesn't show a rule that config.xml clearly has, force a reload rather than re-saving through the GUI again (which can just repeat whatever went wrong the first time):

configctl filter reload

8.5 Symptom → likely cause (expanded)

Symptom Likely cause Where to look
No Rules tab for the interface at all <enable>1</enable> missing from the <optN> block 8.1
Tab exists but is named unexpectedly Tab is named after the interface's <descr>, not its internal name (optN) or device (wg0) 8.1 / 8.2
Confused which tab a rule should go on Group interface (all instances) vs. your specific instance — pick one deliberately, don't assume 8.2
"X is not a valid source/destination alias" on save Raw interface name typed into a Network/Source field instead of using the type dropdown + macro or CIDR 8.3
Rule saved, GUI shows it, but traffic still blocked Saved config hasn't actually been loaded by pf — check pfctl -sr against config.xml 8.4
No handshake at all Wrong public key on either side; endpoint hostname/port wrong; UDP 51820 not forwarded/allowed on WAN Section 6
Handshake succeeds, no ping to 10.10.10.1 Tunnel address mismatch, or a local firewall on the laptop blocking WireGuard Section 6
Ping to tunnel works, LAN ping fails Firewall rule missing on the tunnel's interface tab, or peer's AllowedIPs too narrow on OPNsense 8.2 / Section 6
Works at home, fails on other networks The other network blocks outbound UDP — consider a fallback listen port like 443/UDP Section 6

8.6 General rule of thumb

When GUI behavior seems inconsistent — a checkbox that doesn't stick, a tab that won't appear, a rule that saves but doesn't take effect — treat config.xml as the source of truth for what's saved, and pfctl -sr / ifconfig as the source of truth for what's actually running. When the two disagree, configctl filter reload or configctl webgui restart almost always resolves it without needing a full reboot.

9. A Real Debugging Session, Step by Step

This walks through diagnosing a "handshake never completes" failure end-to-end, using the actual sequence of checks that isolates the cause — narrowing from "is anything reachable" down to the specific misconfigured field.

9.1 Start with the client log

On Windows, the WireGuard app writes a log accessible from the app itself (the log icon/panel) or exportable to a text file. Look for a repeating pattern like:

Sending handshake initiation to peer 1 (203.0.113.5:51820)
Handshake for peer 1 (203.0.113.5:51820) did not complete after 5 seconds, retrying

This endless retry loop with no successful handshake is the signature of the packet either never reaching OPNsense, or reaching it and being silently dropped — WireGuard never sends an error back for either case, so the client can't distinguish them. This narrows the problem to "row 1" territory (Section 6) but doesn't yet say which side.

Also check for tunnel-name mix-ups in the log — e.g. a [Home] (capital) tunnel that starts and immediately shuts down with no peer loaded, versus a [home] (lowercase) that actually has one. If you have more than one saved profile, confirm you're activating the one that's actually configured.

9.2 Confirm your public IP hasn't changed

From a machine on your home network:

curl ifconfig.me

Compare this against the Endpoint IP/hostname in your laptop's config. A mismatch means your ISP reassigned your address and the client is trying to reach a stale IP — fixed by updating the endpoint, or better, switching to DDNS (Section 1) so this can't recur.

9.3 Confirm OPNsense is actually listening

SSH into OPNsense:

sockstat -4 -6 -l | grep 51820

Expect to see udp4 *:51820 and udp6 *:51820 bound. The PID/user columns will show ? — that's normal, since OPNsense's WireGuard runs as an in-kernel interface, not a userland process with its own PID. The presence of the bound socket lines is what confirms the service itself is up.

9.4 Test the port from a real external network

Generic UDP scanners are close to useless against WireGuard specifically, because WireGuard is deliberately silent — it never responds to a packet that isn't a valid handshake from a known peer, which is an anti-scanning feature, not a bug. So expect open|filtered from tools like nmap even when everything is configured correctly:

sudo nmap -sU -p 51820 -Pn 174.92.117.145

-Pn is required — without it, nmap first tries to ping the host and gives up immediately if ICMP is blocked (which it usually is on a WAN interface by default), reporting "Host seems down" without ever actually testing the port.

An open|filtered result here is inconclusive by design — it neither confirms nor rules out a working setup. Don't stop here; move to the next check.

9.5 Check what pf is actually enforcing — the real smoking gun for rule bugs

pfctl -sr | grep 51820

Read the output carefully, field by field. A subtle but common misconfiguration:

pass in log quick on re0 reply-to (re0 x.x.x.x) inet proto udp from any port = 51820 to (re0) port = 51820 keep state

Notice from any port = 51820 — this pins the source port to 51820 as well as the destination. That's wrong: a WireGuard client sends its handshake from a random high ephemeral source port (whatever the OS picks), never from 51820 itself. Only the destination port should be locked to 51820; the source should be unrestricted (any).

This single misconfigured field causes exactly the symptom seen in 9.1 — OPNsense is listening (9.3 confirms it), the rule exists and is loaded (this step confirms it), but real client traffic gets silently rejected because its source port almost never happens to be 51820.

Where this comes from: in the WAN rule editor (Section 3, Step 2), the form has adjacent Source Port and Destination Port fields. It's an easy slip to fill in 51820 for both when your attention is on "the WireGuard port," rather than leaving Source Port blank/any.

Fix: 1. Firewall → Rules → WAN, edit the WireGuard rule 2. Clear the Source Port Range field back to blank/any 3. Leave Destination Port Range as 51820 4. Save, Apply Changes

Verify the fix actually loaded:

pfctl -sr | grep 51820

Should now read from any to (re0) port = 51820 with no port restriction on the from any side.

10. Verifying the Tunnel Is Actually Carrying Traffic (Smoking-Gun Tests)

A successful ping to a LAN address isn't fully conclusive on its own — in rare setups (misconfigured routes, split-DNS overlap, a stale ARP entry) it's theoretically possible to get a response that didn't actually traverse the tunnel. The checks below give you unambiguous proof, not just a plausible sign.

10.1 Negative control (the most convincing single test)

Before trusting a successful ping, disprove the alternative: confirm the LAN is unreachable without the tunnel from the same network you're testing from.

  1. From outside your home network (mobile hotspot, coffee shop, etc.), with WireGuard disconnected, try ping 192.168.1.1 (or whatever LAN host you're testing).
  2. It should fail outright — timeout or "destination unreachable." If it somehow succeeds, something else is exposing your LAN to the internet and that needs investigating independently of this guide.
  3. Now reconnect WireGuard and repeat the same ping. If it now succeeds where it failed a moment ago on the identical network path, that's a direct causal test — nothing else changed except the tunnel coming up.

10.2 Correlate with OPNsense's live firewall log (the actual smoking gun)

This is the most definitive proof available, because it shows the packet being seen and passed by OPNsense in real time, not just a ping response arriving back.

  1. On OPNsense: Firewall → Log Files → Live View
  2. Filter by Interface: WIREGUARD (or your instance's interface name/"WireGuard (Group)")
  3. From the laptop (tunnel connected), send a distinctive, countable burst: ping -c 5 192.168.1.1 (Linux/macOS) or ping -n 5 192.168.1.1 (Windows)
  4. Watch the live log — you should see exactly 5 matching pass entries appear in real time on the WIREGUARD interface, timestamped to the second you sent them, showing source 10.10.10.2 and destination 192.168.1.1

Seeing the packet count match your ping count, on the correct interface, with the correct tunnel-address source, is about as close to a smoking gun as this gets — it's OPNsense's own enforcement log confirming the traffic transited the tunnel, not a downstream response you're inferring the path of.

10.3 Cross-check transfer byte counters

Both ends track bytes transferred per peer, and they should move together with your test traffic.

On the laptop:

sudo wg show

On OPNsense: VPN → WireGuard → Status

Note the transfer (rx/tx) figures on both sides, send a known amount of traffic (e.g. the 5-ping burst from 10.2, or curl a small file from a LAN service), then check again. Both sides' counters should increase by a comparable amount, in the same direction relative to each side (your tx roughly matches OPNsense's rx, and vice versa). If OPNsense's counters aren't moving while you're generating traffic, packets aren't reaching it despite whatever the ping shows.

10.4 Confirm the route, not just reachability

tracert 192.168.1.1

(Windows) or traceroute 192.168.1.1 (Linux/macOS/WSL).

The first hop should be 10.10.10.1 (OPNsense's tunnel address) — if the trace shows a different first hop, or completes in an implausibly small number of hops for a route that should be leaving your local network, traffic may be taking a path you didn't expect (e.g. a VPN client that isn't actually engaged, or a stale route left over from a previous session).

10.5 Why ~200ms pings are a reasonable sign, but not proof on their own

A real-world RTT in that range for tunneled traffic (encrypted, traversing your home upload link, plus whatever network you're testing from) is plausible and not itself suspicious — but a sub-10ms "local-feeling" ping response would have been a stronger signal that something was wrong (suggesting the packet never really left the local network) than a 200ms one is a signal that everything's right. Latency alone can't rule out other explanations (like a cached/stale response or unrelated local route). That's exactly why 10.1–10.4 exist: they give you a positive, falsifiable confirmation rather than an inference from timing.

10.6 Quick reference: the full verification sequence

  1. Disconnect WireGuard, confirm the LAN target is unreachable from outside (10.1)
  2. Reconnect, confirm it becomes reachable on the identical path (10.1)
  3. Send a countable ping burst, watch it appear in OPNsense's live firewall log on the WIREGUARD interface with the correct 10.10.10.2 source (10.2)
  4. Confirm both sides' transfer counters move together during that same burst (10.3)
  5. Traceroute and confirm 10.10.10.1 is the first hop (10.4)

All five agreeing is about as conclusive as this gets without packet-capturing the encrypted payload itself.

11. How WireGuard's Encryption Actually Works

WireGuard uses a fixed, modern cryptographic stack — there's no cipher negotiation like TLS or IPsec do, which is part of what keeps it simple, fast, and low-attack-surface.

11.1 The core primitives

  • Curve25519 — elliptic-curve Diffie-Hellman key exchange, used to derive a shared secret between peers from their public/private keypairs (the PrivateKey/PublicKey values in your configs)
  • ChaCha20-Poly1305 — the actual data encryption. ChaCha20 is the stream cipher; Poly1305 is the authenticator. Together they form an AEAD (Authenticated Encryption with Associated Data) cipher — every packet is encrypted and tamper-proofed in one operation. If even a single bit is altered in transit, decryption fails outright rather than silently producing corrupted data.
  • BLAKE2s — hashing, used within the handshake and for the cookie mechanism (a DoS mitigation)
  • SipHash24 — fast keyed hashing used internally for hashtable lookups (a performance detail, not itself a security primitive)

11.2 The handshake: Noise Protocol Framework

WireGuard's handshake is built on the Noise_IK pattern from the Noise Protocol Framework. This is what you're seeing in the client log as Sending handshake initiation → Receiving handshake response → Keypair N created. Each handshake:

  1. Performs a fresh Curve25519 exchange
  2. Derives a brand-new symmetric session key for that keypair
  3. Authenticates both sides using their long-term static keys (the ones in your [Interface]/[Peer] blocks) — the "IK" in Noise_IK means the Initiator already Knows the responder's static public key in advance, which is exactly why you have to exchange public keys ahead of time (Section 2) rather than negotiating trust on the fly

11.3 Why keys get destroyed and recreated (perfect forward secrecy)

The handshake repeats automatically roughly every 2 minutes (REKEY-AFTER-TIME, a fixed protocol constant, not something you configure). This is what gives WireGuard perfect forward secrecy: each session key is ephemeral and derived fresh, so even if one session key were somehow compromised, it can only expose the ~2-minute window it was active for — not past or future traffic. The long-term static keys (PrivateKey/PublicKey) are only ever used to authenticate the handshake; they never directly encrypt data.

During the handover, WireGuard briefly keeps two keypairs alive (the new one plus the previous one, in case in-flight packets were still encrypted under the old key) before discarding the older one — this is the "Keypair N destroyed" message you'll see roughly every 2 minutes in a healthy, long-running tunnel. It's expected behavior, not an error.

11.4 Putting it together for this setup

  • Your laptop and OPNsense each hold a long-term Curve25519 keypair, generated once and exchanged as part of setup (Section 2)
  • Every ~2 minutes, they run a fresh Noise_IK handshake authenticated by those long-term keys, producing a new ephemeral session key
  • All tunneled packets (LAN traffic, or full-tunnel browsing per Section 7's companion hotspot guide) are encrypted with ChaCha20-Poly1305 using that session key
  • The whole exchange rides as a single UDP-based protocol with a minimal, fixed packet format — no TLS-style negotiation overhead, which is part of why WireGuard is both faster and has a smaller attack surface than OpenVPN or IPsec