This lab builds a WireGuard site-to-site tunnel between two
OPNsense endpoints. One endpoint is the “left” site
(Site-A), the other is the “right” site (Site-B). The
tunnel is between the WAN addresses of the two firewalls and
carries inner traffic between the LANs on each side.
The point of this lab is the discipline of addressing the three layers separately: the outer tunnel (the WAN endpoints and the UDP port), the inner tunnel address (the WireGuard interface on each side), and the inner routed traffic (the LANs that traverse the tunnel). The operator who confuses the layers builds a tunnel that connects but doesn’t route, or that routes but won’t pass traffic.
Objective
By the end of this lab you can:
- Plan addresses for a WireGuard site-to-site tunnel.
- Generate WireGuard keys on both endpoints and exchange public keys.
- Configure the WireGuard interface and peer on both endpoints.
- Write firewall rules that permit the tunnel traffic.
- Verify the bidirectional handshake and the inner traffic flow.
- Test the tunnel’s behaviour when one endpoint is restarted.
Requirements
- Two OPNsense endpoints (physical or virtual) at different
sites —
Site-AandSite-B. The WAN addresses must be reachable from each other. - The
os-wireguardplugin installed on both endpoints. - A LAN-side host on each side that can ping the other side’s LAN through the tunnel.
- UDP port
51820(default) reachable fromSite-A’s WAN toSite-B’s WAN, and vice versa.
The lab can run with two nested OPNsense VMs with public addresses on the simulated WAN, or with two physical firewalls on different networks. The WireGuard handshake is the same.
Tasks
Task 1: Plan the addressing
Three layers, three sets of addresses:
| Layer | Site-A | Site-B |
|---|---|---|
| WAN (outer) | 203.0.113.1/24 | 198.51.100.1/24 |
| Tunnel (inner) | 10.10.10.1/32 | 10.10.10.2/32 |
| LAN (inner) | 192.168.10.0/24 | 192.168.20.0/24 |
The tunnel subnet is /32 per peer in the conventional
WireGuard layout — each peer has a single inner address and
routes the inner LANs through the tunnel. The /24 tunnel
subnet (10.10.10.0/24) is used for clarity, but route
statements point to the peer’s /32.
Task 2: Generate keys on both endpoints
On each endpoint, install the os-wireguard plugin if not
already installed. Generate the keypair on each:
# On Site-A
wg genkey | tee /etc/wireguard/site-a-private.key | wg pubkey > /etc/wireguard/site-a-public.key
chmod 600 /etc/wireguard/site-a-private.key
# On Site-B
wg genkey | tee /etc/wireguard/site-b-private.key | wg pubkey > /etc/wireguard/site-b-public.key
chmod 600 /etc/wireguard/site-b-private.key
Or use the OPNsense GUI: VPN → WireGuard → Local → Generate new keypair. The GUI also stores the keys securely.
Exchange the public keys. The discipline: the private key never leaves the endpoint that generated it. The public key is on the other endpoint.
Task 3: Configure the WireGuard interface on Site-A
In the GUI: VPN → WireGuard → Local, add:
- Name:
wg_s2s - Listen port:
51820 - Private key: Site-A’s private key
- Tunnel address:
10.10.10.1/24
Save and apply.
Task 4: Configure the peer on Site-A
In the GUI: VPN → WireGuard → Peers, add:
- Name:
Site-B - Public key: Site-B’s public key
- Endpoint:
198.51.100.1:51820(Site-B’s WAN) - AllowedIPs:
10.10.10.2/32, 192.168.20.0/24 - Persistent keepalive:
25(every 25 seconds — useful for NAT traversal in production; in a lab with no NAT, 0 is fine)
The AllowedIPs is the source of the routing behaviour.
WireGuard installs kernel routes for every entry in
AllowedIPs pointing to the peer’s tunnel address. The
192.168.20.0/24 entry tells Site-A “to reach Site-B’s
LAN, send via this peer”.
Save and apply.
Task 5: Configure the WireGuard interface on Site-B
The mirror of Site-A:
- Local name:
wg_s2s - Listen port:
51820 - Private key: Site-B’s private key
- Tunnel address:
10.10.10.2/24
Peers:
- Name:
Site-A - Public key: Site-A’s public key
- Endpoint:
203.0.113.1:51820(Site-A’s WAN) - AllowedIPs:
10.10.10.1/32, 192.168.10.0/24
Save and apply. The 192.168.10.0/24 entry tells Site-B
“to reach Site-A’s LAN, send via this peer”.
Task 6: Verify the handshake
On either endpoint:
wg show
The output should show:
- The local interface with its private key (redacted in show output) and tunnel address.
- The peer with its public key, endpoint, and latest handshake.
The “latest handshake” timestamp is the truth. A handshake that is recent (within seconds) means the peers have exchanged UDP packets and the tunnel is alive. A handshake that is minutes old means the tunnel is dead or the peers have not exchanged packets yet.
# Trigger a handshake if needed
ping -c 3 10.10.10.2
The ping should produce a handshake on the next
wg show.
Task 7: Write the firewall rules on both sides
The tunnel traffic must be permitted. WireGuard uses UDP 51820 for the handshake and re-key; the inner traffic is routed through the WireGuard interface.
On Site-A, in the GUI:
- Firewall → Rules → WAN: allow UDP from
198.51.100.1to203.0.113.1port51820(the WireGuard handshake). - Firewall → Rules → WireGuard: allow traffic from
192.168.10.0/24to192.168.20.0/24and vice versa.
On Site-B, mirror the rules.
The WireGuard interface must have firewall rules. By default it has no allow rules; the inner traffic is dropped at the firewall even if the tunnel is up.
Task 8: Verify the inner traffic flow
From a LAN-side host on Site-A:
ping -c 5 192.168.20.1
The ping should reach the LAN gateway on Site-B. The
WireGuard tunnel carries the packet, encrypted, from
Site-A’s WAN to Site-B’s WAN, and the inner packet has
source 192.168.10.x and destination 192.168.20.1.
Verify on the firewall:
# On Site-A
tcpdump -ni wg_s2s -c 5
# Should show the encrypted UDP packets
The tcpdump output shows the outer UDP packets on the
WireGuard interface; the inner payload is encrypted and
invisible.
Task 9: Capture the handshake with packet capture
On either endpoint, capture the WAN interface:
# Substitute your own value before running:
# WAN_IF is the WAN device as ifconfig(8) names it: em0, igb0, vtnet0 ...
WAN_IF=igb0
tcpdump -ni "$WAN_IF" 'udp port 51820' -c 10
The output should show the handshake packets (initial), keepalive packets (steady state), and re-key packets (every 2 minutes). The handshake is a single UDP exchange with the cookie reply; the steady state is a 1.0 packet heartbeat at the keepalive interval.
Task 10: Restart one endpoint and verify recovery
On Site-A, restart the WireGuard service:
configctl wireguard restart
The site-A’s wg interface comes back up. The peer
configuration is still there. The handshake should resume
within a few seconds — Site-A’s AllowedIPs will route
Site-B’s traffic via the tunnel, and Site-B will see the
handshake from Site-A’s WAN.
Verify:
# On Site-A
wg show | grep -A 5 'peer'
The “latest handshake” should be recent. The Site-B side should also show a recent handshake.
A tunnel that does not recover after a service restart is
configured without a PersistentKeepalive. The
PersistentKeepalive is the discipline that lets a peer
behind a NAT re-establish the tunnel.
Task 11: Document the tunnel
WIREGUARD SITE-TO-SITE TUNNEL
=============================
Site-A WAN: 203.0.113.1
Site-A tunnel: 10.10.10.1/32
Site-A LAN: 192.168.10.0/24
Site-B WAN: 198.51.100.1
Site-B tunnel: 10.10.10.2/32
Site-B LAN: 192.168.20.0/24
UDP port: 51820
Verification:
- wg show shows recent handshake on both endpoints
- ping 192.168.20.1 from Site-A's LAN succeeds
- ping 192.168.10.1 from Site-B's LAN succeeds
- Restart of Site-A's WireGuard service: tunnel recovers
within 5 seconds
Validation
- Both endpoints have a WireGuard interface with a unique tunnel address.
- Each endpoint has the other’s public key as a peer.
wg showshows a recent handshake on both endpoints.- Ping from Site-A’s LAN to Site-B’s LAN succeeds.
- Ping from Site-B’s LAN to Site-A’s LAN succeeds.
- The WireGuard firewall rules allow the inner traffic.
- Restarting the WireGuard service on one endpoint gives the handshake within 10 seconds.
Expected Result
You have a working WireGuard site-to-site tunnel between two OPNsense endpoints. The two LANs can route through the tunnel; the WAN-side handshake is established; the inner traffic is encrypted. The tunnel survives a service restart on either endpoint.
Troubleshooting
- No handshake. The public key is mismatched, or the
endpoint address is wrong. Verify both ends with
wg show. Thepeerrow on the other side should show the matching public key. - Handshake works but inner traffic is blocked. The
WireGuard firewall rules are missing. Add the allow rule
on the
WireGuardinterface for the source/destination subnets. - Endpoint is behind NAT and the handshake dies. The
Persistent keepaliveis missing. Add a value of25seconds on the peer without a static endpoint. pingworks for the tunnel address but not for the LAN. TheAllowedIPson the remote side does not include the LAN subnet. Add it to the peer configuration on the side that is the source of the ping.
Cleanup
The WireGuard tunnel is the new baseline. Snapshot both endpoints:
# On Site-A
configctl backup download
# Save as opnsense-site-a-with-wireguard.xml
# On Site-B
configctl backup download
# Save as opnsense-site-b-with-wireguard.xml
To remove the tunnel:
# On both endpoints
wg-quick down wg_s2s
# Or in the GUI: VPN → WireGuard → disable the local; remove the peer
The restore path is the snapshot from before the lab.
What you learned
- WireGuard site-to-site has three address layers: the WAN
endpoint, the inner tunnel address, and the inner routed
subnets. The
AllowedIPsis the source of the routing. - The handshake is the truth —
wg show’s “latest handshake” timestamp is the only health metric that matters. - The WireGuard firewall rules are required, not optional. The tunnel carries traffic; the firewall decides what is permitted.
- A
PersistentKeepaliveis the discipline that lets a peer behind a NAT re-establish the tunnel after a restart.