Skip to main content
RunBook Academy

← All labs in OPNsense

Lab · intermediate · ~90 min

Lab: Build a WireGuard remote-access tunnel on OPNsense

B · Nested virtualisationC · Simulation

Objectives

  • Install the os-wireguard plugin on OPNsense
  • Generate a WireGuard key pair for the tunnel and one for a peer
  • Configure the tunnel with a tunnel-address subnet and the peer with AllowedIPs that drives a full tunnel
  • Add the firewall rule that lets the WireGuard interface respond to handshake and accepts tunnel-internal traffic
  • Add the outbound NAT rule for traffic from the tunnel subnet to the LAN
  • Install the WireGuard client on a remote host and confirm handshake + ping through the tunnel

Prerequisites

This lab builds a WireGuard remote-access tunnel on OPNsense: one peer (a road-warrior laptop) connecting into the firewall, with firewall rules and outbound NAT that put the peer’s traffic on the LAN. By the end, the peer can ping a host on the LAN and the LAN sees traffic coming from the tunnel subnet.

The lab uses a single peer because the production discipline does not change with N. When you have a hundred peers, you have a hundred rows in the GUI and one firewall rule on the WireGuard group interface.

Objective

By the end of this lab, you can:

  • Install the os-wireguard plugin and confirm the kernel module is loaded.
  • Generate Curve25519 key pairs on both ends without leaking the private keys to a screenshot or shell history.
  • Configure a tunnel with Listen Port = 51820 and a tunnel address of 10.10.99.1/24.
  • Configure a peer with AllowedIPs = 0.0.0.0/0 (full tunnel) and a Persistent Keepalive = 25.
  • Add the firewall rules and NAT entries that make the tunnel actually pass traffic.
  • Validate a current handshake from both ends and prove a ping traverses the tunnel.

Requirements

You need:

  • An OPNsense instance with WAN reachable from the WireGuard client. In nested mode, that means host-only NAT on the WAN interface and a client on the host.
  • A second VM, container, or physical host that will act as the WireGuard peer. Linux, macOS, Windows, iOS, or Android — any current client works.
  • A host on the LAN that the peer can ping through the tunnel. The DHCP-scope lab’s 10.10.10.100 - 10.10.10.200 pool is fine; pick any leased address.
  • Outbound UDP/51820 from the peer to OPNsense’s WAN. Confirm upstream firewalls permit this before troubleshooting OPNsense.

Tasks

Task 1: Install the plugin

From the OPNsense GUI: System → Firmware → Plugins. Search for os-wireguard, click Install, accept the prompt. The plugin pulls the kernel module and the management GUI; it does not require a reboot on most builds.

After install:

# On OPNsense shell
kldstat | grep -i wireguard
wg --version

If kldstat is empty, the module is not loaded. Try:

kldload wireguard

If kldload errors with module already loaded you are done; if it errors with cannot find file, the plugin install did not finish — re-install from the GUI and check System → Log → General for pkg errors.

Task 2: Generate the key pairs

The WireGuard keys are Curve25519. Generate them on the host that will hold them, never copy a private key to a chat window:

# On OPNsense (the server's keys)
umask 077
wg genkey | tee /etc/wireguard/server_private.key | \
    wg pubkey > /etc/wireguard/server_public.key
cat /etc/wireguard/server_public.key
# On the peer (the laptop's keys)
umask 077
wg genkey | tee ~/wg_private.key | \
    wg pubkey > ~/wg_public.key
cat ~/wg_public.key

Copy the public keys somewhere safe. You will paste the client’s public key into OPNsense, and the server’s public key into the client. The private keys stay on their respective hosts.

Task 3: Configure the server tunnel

VPN → WireGuard → Tunnels → + Add:

FieldValue
Enabledon
Namewg0
Listen Port51820
Tunnel10.10.99.1/24
Peers(add in Task 4)
Gatewayleave blank

Save and apply.

The GUI writes /etc/wireguard/wg0.conf and brings up the interface. Confirm:

wg show wg0
ifconfig wg0

The wg show output should show the interface as peer: (none) until you add a peer, and the listen port as 51820. The ifconfig output should show inet 10.10.99.1 netmask 255.255.255.0.

Task 4: Configure the peer

VPN → WireGuard → Peers → + Add:

FieldValue
Enabledon
Nameroadwarrior-laptop
Public Key(paste the client’s public key)
AllowedIPs10.10.99.2/32
Endpoint Address(empty for now; filled by the client)
Endpoint Port(empty)
Persistent Keepalive25
Pre-shared Keyoptional; add one if your policy requires it

Save and apply. The peer row now appears under the tunnel. The AllowedIPs for the server’s view of this peer is 10.10.99.2/32 — that is the address the server will hand the peer when the handshake completes. It is not the same as the peer’s view of AllowedIPs (which we set in Task 5 to 0.0.0.0/0 for full-tunnel).

Task 5: Configure the peer on the client

On the road-warrior laptop, create /etc/wireguard/wg0.conf:

[Interface]
PrivateKey = <paste client private key>
Address = 10.10.99.2/32
DNS = 10.10.10.1

[Peer]
PublicKey = <paste server public key>
Endpoint = <OPNsense WAN IP>:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25

The AllowedIPs = 0.0.0.0/0, ::/0 is the full-tunnel form. Replace with 10.10.10.0/24, 10.10.20.0/24 (the LAN ranges) for split-tunnel.

Bring up the tunnel:

# Linux (root required for wg-quick)
sudo wg-quick up wg0

# macOS / Windows / iOS / Android: use the GUI client and import
# the conf or scan the QR code.

Confirm:

sudo wg show

The output must show a peer with the server’s public key and latest handshake: 5 seconds ago. If latest handshake is never, the client has sent packets but the server has not replied — go to Troubleshooting.

Task 6: Firewall rules on the WireGuard group

A WireGuard tunnel on OPNsense is a group interface. By default it has no rules, which means the firewall drops everything that arrives from the tunnel. Add rules under Firewall → Rules → WireGuard (Group):

ActionSourceDestinationProtocol/Port
pass10.10.99.0/2410.10.10.0/24any
pass10.10.10.0/2410.10.99.0/24any (icmp only for the lab; relax as needed)

The first rule lets tunnel clients reach the LAN. The second rule lets LAN hosts reply to tunnel clients. In production, tighten both: scope by port, scope by host, scope by group.

Confirm the rules:

pfctl -sr | grep -A1 'wg '

Task 7: Outbound NAT for the tunnel subnet

For the peer to reach the LAN, traffic from 10.10.99.0/24 must be translated to the OPNsense WAN or LAN source address when it leaves the LAN interface. Firewall → NAT → Outbound:

If your outbound NAT is in Automatic mode, the rule is added for you. Switch to Hybrid mode to see what was generated:

pfctl -sn | grep -A1 '10.10.99'

You should see:

nat on em1 inet from 10.10.99.0/24 to any -> (em0)

If outbound NAT is in Manual mode, add the rule yourself:

FieldValue
InterfaceWAN
Source10.10.99.0/24
Destinationany
TranslationInterface address
DescriptionWireGuard outbound

Apply.

Task 8: Validate the handshake

The two wg show commands are the canonical evidence. From both ends:

# On OPNsense
wg show wg0 latest-handshakes

# On the peer
sudo wg show

latest handshake must be within the last 2-3 minutes on both sides. If only the client side shows a recent handshake, the server is dropping the reply — usually a firewall rule problem on the WireGuard group.

If the handshake is stale (older than 2 minutes) and the peer is idle, that is fine; PersistentKeepalive = 25 refreshes the handshake every 25 seconds only when traffic flows.

Task 9: Validate end-to-end with a ping

From the peer:

ping -c 4 10.10.10.50    # the reserved printer address

If the ping succeeds, the tunnel is up, the firewall rule is allowing traffic, and outbound NAT is working.

From a LAN host:

ping -c 4 10.10.99.2

This must also succeed. If the first ping works but the second does not, the LAN host does not have a route to 10.10.99.0/24 through OPNsense. Firewall → Rules → LAN → + Add an allow rule for the LAN subnet to the tunnel subnet.

Task 10: Watch the handshake on the wire

The signature of a working WireGuard tunnel on the WAN side is short UDP/51820 packets at regular intervals. Capture:

# On OPNsense WAN
sudo tcpdump -i em0 -nn -c 30 -s 0 udp port 51820

While the capture runs, from the peer, ping a LAN host. The capture shows UDP/51820 packets going both ways with payload lengths of 64-148 bytes — that is WireGuard’s encrypted transport. You will not see the inner ICMP echo in the capture because the payload is encrypted; the proof is the bidirectional UDP/51820 flow during a ping that the LAN can answer.

If the capture shows only outbound packets with no reply, the firewall is blocking the reply. The most common cause is the WireGuard group interface having no inbound allow rule.

Validation

  • wg show wg0 on OPNsense shows a peer with a latest handshake within the last 2 minutes.
  • wg show on the peer shows the same peer with a recent handshake and a transfer: X received, Y sent row with non-zero bytes.
  • ping 10.10.10.50 from the peer succeeds.
  • tcpdump -i em0 port 51820 shows bidirectional UDP traffic during the ping.
  • pfctl -sr | grep wg shows the firewall rules on the WireGuard group.
  • pfctl -sn | grep 10.10.99 shows the outbound NAT rule.

Expected Result

A WireGuard tunnel between the OPNsense firewall and one remote client, carrying the client’s traffic into the LAN. Both ends report a recent handshake; a ping from the client to a LAN host succeeds; the LAN host can reply. The WireGuard group interface has firewall rules that allow tunnel-internal traffic; outbound NAT translates the tunnel subnet to OPNsense’s WAN address on the way out.

Troubleshooting

Handshake never completes. UDP/51820 is being blocked somewhere between the peer and OPNsense. Check: (1) OPNsense WAN IP is reachable from the peer (ping <opnsense-wan>), (2) no upstream firewall (ISP CPE, cloud SG) blocks UDP/51820, (3) Firewall → Rules → WAN has an allow rule for UDP/51820 inbound to OPNsense’s WAN IP. The WireGuard plugin installs a default rule for this, but a recent change may have removed it.

Handshake completes but ping fails. The WireGuard interface has no firewall rule, or outbound NAT is missing. Confirm pfctl -sr | grep wg returns non-empty and pfctl -sn | grep 10.10.99 returns non-empty.

Handshake completes but only one direction. Split-tunnel configuration mismatch. The peer’s AllowedIPs must include the destination subnet, and the server’s view of the peer’s AllowedIPs must include the peer’s tunnel address.

Tunnel is up, but DNS leaks the client’s ISP. The client’s DNS is not the LAN’s DNS. Confirm wg0.conf on the client sets DNS = 10.10.10.1 and that the WireGuard client honours it (Linux’s wg-quick does; some mobile clients ignore the field). For full-tunnel, check https://dnsleaktest.com from inside the tunnel.

Tunnel drops after 2-3 minutes of idle. NAT timeout on the upstream firewall is killing the UDP flow. PersistentKeepalive = 25 keeps the UDP/51820 packets flowing every 25 seconds, which prevents most NATs from expiring the mapping. If even 25 seconds is too long, drop it to 15.

Cleanup

Tear the tunnel down and confirm no leftover state.

# On the peer
sudo wg-quick down wg0

# On OPNsense (GUI)
# VPN → WireGuard → Peers → delete the roadwarrior peer
# VPN → WireGuard → Tunnels → delete wg0 (or just uncheck Enable)

# Confirm no stale keys remain
ls -l /etc/wireguard/
shred -u /etc/wireguard/server_private.key 2>/dev/null || \
    rm -f /etc/wireguard/server_private.key

# On the peer
shred -u ~/wg_private.key 2>/dev/null || rm -f ~/wg_private.key

# Confirm the WireGuard group has no rules left
pfctl -sr | grep -E 'wg0|10.10.99' || echo "no WireGuard rules"

If you created the LAN-to-tunnel rule in Task 9 to allow replies, remove it. The lab’s setup is meant to be reverted, not left as a permanent policy.

What you learned

  • WireGuard key pairs are per-host. Each side holds its private key; both sides know the other side’s public key. That is the trust model.
  • AllowedIPs means different things on each side. Server-side it is “addresses I will accept from this peer.” Client-side it is “addresses I will route into the tunnel.”
  • A WireGuard tunnel on OPNsense is a group interface with no default rules. Without firewall rules and outbound NAT, the tunnel completes the handshake but cannot carry traffic.
  • PersistentKeepalive is the answer to NAT timeouts on upstream firewalls. Without it, idle tunnels drop after the NAT mapping expires.

Deliverables

  • · A WireGuard tunnel with one peer, both endpoints showing a current handshake
  • · Firewall rules on the WireGuard group interface
  • · An outbound NAT rule for the tunnel subnet
  • · A captured handshake from the wire (`tcpdump` on the WAN)

Verification status

Last reviewed
2026-08-14
Executed end to end
not yet run on hardware

The commands and configuration here have been reviewed against the verified software versions, but nobody has run this lab start to finish on a system meeting its prerequisites. Treat the Expected Outcome as the intended result rather than an observed one, and keep the Cleanup section to hand.