How to Set Up WireGuard VPN on FreeBSD
WireGuard is a modern, high-performance VPN protocol that has become the preferred choice for securing network traffic on FreeBSD servers. Unlike older solutions such as OpenVPN or IPsec, WireGuard uses a minimal codebase, modern cryptography, and a straightforward configuration model. This tutorial walks through every step of setting up a WireGuard VPN server on FreeBSD and connecting clients from Linux, macOS, Windows, iOS, and Android.
By the end of this guide you will have a fully working WireGuard VPN with NAT, PF firewall integration, DNS forwarding, and multiple peer support.
Why WireGuard on FreeBSD
Three reasons make WireGuard the right VPN choice for FreeBSD administrators.
Simplicity. A WireGuard configuration file is typically under 20 lines. Compare that to the hundreds of lines required for OpenVPN or the complexity of IPsec/IKEv2 setups. Fewer lines mean fewer misconfigurations and faster audits.
Performance. WireGuard runs inside the kernel as the if_wg module on FreeBSD. It avoids the overhead of userspace TLS processing that OpenVPN relies on. In benchmarks, WireGuard consistently delivers higher throughput and lower latency, often saturating a 1 Gbps link with minimal CPU usage.
Modern cryptography. WireGuard uses Curve25519 for key exchange, ChaCha20 for symmetric encryption, Poly1305 for authentication, BLAKE2s for hashing, and SipHash24 for hashtable keys. There are no cipher negotiations, no legacy algorithm choices, and no configuration knobs that could weaken security. The protocol either uses its fixed, audited cryptographic suite or it does not connect at all.
FreeBSD has shipped the if_wg kernel module since FreeBSD 13.0. The implementation is maintained in the base kernel tree and receives regular updates. If you are running FreeBSD 13 or later, WireGuard support is available without patching or compiling a custom kernel.
Prerequisites
Before starting, confirm you have the following:
- FreeBSD 13.0 or later -- The
if_wgkernel module is included in the base system starting with FreeBSD 13. Earlier versions require a third-party port. - Root access -- All commands in this guide require root privileges. Use
su -orsudoas appropriate. - A public IP address -- Your FreeBSD server needs a publicly reachable IP (or at minimum, a port-forwarded UDP port). A FreeBSD-compatible VPS works well for this.
- UDP port 51820 open -- WireGuard uses UDP exclusively. Ensure your hosting provider or upstream firewall permits inbound UDP on port 51820.
- A basic understanding of networking -- You should be comfortable with IP addresses, subnets, and routing concepts.
Verify your FreeBSD version:
shfreebsd-version
Expected output: 13.2-RELEASE or newer.
Step 1: Install WireGuard Tools
The if_wg kernel module ships with FreeBSD, but the userspace tools for key generation and interface management come from the wireguard-tools package.
shpkg install wireguard-tools
This installs wg (the configuration utility) and wg-quick (the interface management wrapper).
Step 2: Load the Kernel Module
Load the if_wg kernel module immediately:
shkldload if_wg
Verify it loaded:
shkldstat | grep if_wg
You should see a line containing if_wg.ko.
To load the module automatically at boot, add it to /etc/rc.conf:
shsysrc kld_list+="if_wg"
This appends if_wg to the kld_list variable. After a reboot, the module will load before any network services start.
Step 3: Generate Server Keys
WireGuard uses asymmetric Curve25519 key pairs. Each peer -- server and client -- needs its own private and public key.
Create the configuration directory and set restrictive permissions:
shmkdir -p /usr/local/etc/wireguard chmod 700 /usr/local/etc/wireguard
Generate the server key pair:
shwg genkey | tee /usr/local/etc/wireguard/server_private.key | wg pubkey > /usr/local/etc/wireguard/server_public.key chmod 600 /usr/local/etc/wireguard/server_private.key
View the keys (you will need them for configuration):
shcat /usr/local/etc/wireguard/server_private.key cat /usr/local/etc/wireguard/server_public.key
Each key is a single line of Base64-encoded text. Keep the private key secret. The public key is safe to share with clients.
Step 4: Generate Client Keys
Repeat the key generation for each client. You can do this on the server or on the client machine itself. Generating on the client is more secure because the private key never crosses the network.
On the server (for convenience):
shwg genkey | tee /usr/local/etc/wireguard/client1_private.key | wg pubkey > /usr/local/etc/wireguard/client1_public.key chmod 600 /usr/local/etc/wireguard/client1_private.key
If you generate keys on the client side, use the same wg genkey | wg pubkey pipeline and transfer only the public key to the server.
Step 5: Create the Server Configuration
Create the file /usr/local/etc/wireguard/wg0.conf with the following complete configuration. Replace placeholder values with your actual keys and network details.
ini# /usr/local/etc/wireguard/wg0.conf # FreeBSD WireGuard Server Configuration [Interface] # Server private key (from server_private.key) PrivateKey = SERVER_PRIVATE_KEY_HERE # VPN subnet address for this server Address = 10.0.0.1/24 # UDP listen port ListenPort = 51820 # Optional: run commands after interface comes up / goes down PostUp = /usr/local/etc/wireguard/postup.sh PostDown = /usr/local/etc/wireguard/postdown.sh # --- Peer: Client 1 --- [Peer] # Client 1 public key (from client1_public.key) PublicKey = CLIENT1_PUBLIC_KEY_HERE # IP address assigned to this client inside the VPN AllowedIPs = 10.0.0.2/32 # Optional: keep NAT mappings alive (useful if client is behind NAT) # PersistentKeepalive = 25
Key points:
Address = 10.0.0.1/24assigns the server the first address in the VPN subnet.ListenPort = 51820is the standard WireGuard port. Change it if you need to avoid detection or conflict.- Each
[Peer]block defines one client.AllowedIPsrestricts which source IPs are accepted from that peer. PersistentKeepaliveis only needed when a client sits behind NAT and needs to keep the connection alive. The server typically does not need it.
Step 6: Create the Client Configuration
Each client needs its own wg0.conf. Below is a complete client configuration that routes all traffic through the VPN (full tunnel).
ini# Client WireGuard Configuration (wg0.conf) [Interface] # Client private key PrivateKey = CLIENT1_PRIVATE_KEY_HERE # VPN IP address for this client Address = 10.0.0.2/24 # DNS server to use through the VPN DNS = 10.0.0.1 [Peer] # Server public key PublicKey = SERVER_PUBLIC_KEY_HERE # Route all traffic through VPN (full tunnel) AllowedIPs = 0.0.0.0/0, ::/0 # Server public IP and port Endpoint = YOUR_SERVER_PUBLIC_IP:51820 # Keep connection alive behind NAT PersistentKeepalive = 25
If you only want to route VPN subnet traffic through the tunnel (split tunnel), change AllowedIPs to:
iniAllowedIPs = 10.0.0.0/24
Client Setup by Platform
Linux:
shsudo apt install wireguard # Debian/Ubuntu sudo cp wg0.conf /etc/wireguard/wg0.conf sudo wg-quick up wg0
macOS:
Install the WireGuard app from the Mac App Store or use Homebrew:
shbrew install wireguard-tools sudo wg-quick up /path/to/wg0.conf
Alternatively, import the configuration file into the WireGuard macOS GUI app.
Windows:
Download the WireGuard installer from wireguard.com/install. Open the application, click "Import tunnel(s) from file", select your wg0.conf, and click "Activate".
iOS and Android:
Install the WireGuard app from the App Store or Google Play. You can either import a .conf file or scan a QR code. To generate a QR code from the server:
shpkg install libqrencode qrencode -t ansiutf8 < /usr/local/etc/wireguard/client1.conf
This prints a scannable QR code directly in your terminal.
Step 7: Configure PF Firewall Rules
FreeBSD's PF firewall needs rules to allow WireGuard traffic and (for full-tunnel VPNs) NAT the outbound traffic. If you are not yet familiar with PF, read the PF firewall guide first.
Add the following to /etc/pf.conf:
pf# /etc/pf.conf -- WireGuard additions # Define interfaces and networks ext_if = "vtnet0" # Your external interface (check with ifconfig) wg_if = "wg0" # WireGuard interface wg_net = "10.0.0.0/24" # WireGuard VPN subnet # NAT: masquerade VPN traffic going out the external interface nat on $ext_if from $wg_net to any -> ($ext_if) # Allow WireGuard UDP port pass in on $ext_if proto udp from any to any port 51820 # Allow all traffic on the WireGuard interface pass in on $wg_if from $wg_net to any pass out on $wg_if from any to $wg_net # Allow forwarded traffic from VPN to external pass out on $ext_if from $wg_net to any # Allow established connections back pass in on $ext_if from any to $wg_net flags any
Check your external interface name with ifconfig. Common names on FreeBSD VPS providers include vtnet0, em0, igb0, or bge0.
Verify the PF configuration syntax:
shpfctl -n -f /etc/pf.conf
If no errors appear, reload the ruleset:
shpfctl -f /etc/pf.conf
Enable PF if it is not already running:
shsysrc pf_enable="YES" sysrc pflog_enable="YES" service pf start
Step 8: Enable IP Forwarding and NAT
For the server to route traffic between the WireGuard interface and the external network, IP forwarding must be enabled.
Enable it immediately:
shsysctl net.inet.ip.forwarding=1
For IPv6 (if needed):
shsysctl net.inet6.ip6.forwarding=1
Make it persistent across reboots by adding to /etc/rc.conf:
shsysrc gateway_enable="YES" sysrc ipv6_gateway_enable="YES"
The gateway_enable directive sets net.inet.ip.forwarding=1 at boot time. This is the FreeBSD-specific equivalent of Linux's net.ipv4.ip_forward.
Step 9: Start and Enable WireGuard
Start the WireGuard interface:
shwg-quick up wg0
You should see output indicating the interface was created and configured. Verify the interface exists:
shifconfig wg0
Expected output:
shellwg0: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> metric 0 mtu 1420 options=80000<LINKSTATE> inet 10.0.0.1 netmask 0xffffff00 groups: wg nd6 options=109<PERFORMNUD,IFDISABLED,NO_DAD>
To start WireGuard automatically at boot, add to /etc/rc.conf:
shsysrc wireguard_enable="YES" sysrc wireguard_interfaces="wg0"
Here is what your complete /etc/rc.conf additions should look like:
sh# /etc/rc.conf -- WireGuard related entries kld_list="if_wg" gateway_enable="YES" ipv6_gateway_enable="YES" pf_enable="YES" pflog_enable="YES" wireguard_enable="YES" wireguard_interfaces="wg0"
To stop the interface:
shwg-quick down wg0
To restart after configuration changes:
shwg-quick down wg0 && wg-quick up wg0
Step 10: Test the Connection
On the server
Check WireGuard status:
shwg show
Sample output:
shellinterface: wg0 public key: <server_public_key> private key: (hidden) listening port: 51820 peer: <client1_public_key> allowed ips: 10.0.0.2/32 latest handshake: 23 seconds ago transfer: 1.24 MiB received, 3.87 MiB sent
The latest handshake field confirms the client connected successfully. If this field is missing, the client has not yet initiated a connection.
On the client
After running wg-quick up wg0 on the client:
sh# Ping the server's VPN address ping 10.0.0.1 # Verify your public IP changed (full tunnel only) curl ifconfig.me # Trace the route traceroute 1.1.1.1
If the ping succeeds and curl ifconfig.me returns your FreeBSD server's public IP, the full tunnel is working correctly.
Step 11: Adding Multiple Peers
To add more clients, generate a new key pair for each and append a [Peer] block to the server's wg0.conf.
Generate keys for a second client:
shwg genkey | tee /usr/local/etc/wireguard/client2_private.key | wg pubkey > /usr/local/etc/wireguard/client2_public.key chmod 600 /usr/local/etc/wireguard/client2_private.key
Add the peer to the server configuration:
ini# --- Peer: Client 2 --- [Peer] PublicKey = CLIENT2_PUBLIC_KEY_HERE AllowedIPs = 10.0.0.3/32
Create the client configuration for client 2:
ini# Client 2 WireGuard Configuration [Interface] PrivateKey = CLIENT2_PRIVATE_KEY_HERE Address = 10.0.0.3/24 DNS = 10.0.0.1 [Peer] PublicKey = SERVER_PUBLIC_KEY_HERE AllowedIPs = 0.0.0.0/0, ::/0 Endpoint = YOUR_SERVER_PUBLIC_IP:51820 PersistentKeepalive = 25
Assign each client a unique IP address within the 10.0.0.0/24 subnet. With a /24 subnet you can support up to 253 peers (10.0.0.2 through 10.0.0.254).
Reload the server without dropping existing connections:
shwg syncconf wg0 <(wg-quick strip wg0)
This applies configuration changes without restarting the interface, so existing peer sessions remain active.
Step 12: DNS Configuration
If clients use the VPN as a full tunnel, they need DNS resolution through the VPN. You have three options.
Option A: Use a Public DNS Resolver
Set DNS = 1.1.1.1, 9.9.9.9 in the client configuration. This is the simplest approach but means DNS queries exit the VPN tunnel at the server and go to a third-party resolver.
Option B: Run a Local DNS Resolver on the Server
Install Unbound on your FreeBSD server:
shpkg install unbound
Configure /usr/local/etc/unbound/unbound.conf:
yamlserver: interface: 10.0.0.1 interface: 127.0.0.1 access-control: 10.0.0.0/24 allow access-control: 127.0.0.0/8 allow hide-identity: yes hide-version: yes do-not-query-localhost: no forward-zone: name: "." forward-addr: 1.1.1.1 forward-addr: 9.9.9.9
Enable and start Unbound:
shsysrc unbound_enable="YES" service unbound start
Set DNS = 10.0.0.1 in client configurations. Now all DNS queries from VPN clients are resolved by your own server, preventing DNS leaks.
Option C: Use DNS Over TLS
Configure Unbound to forward queries over TLS for additional privacy:
yamlserver: interface: 10.0.0.1 interface: 127.0.0.1 access-control: 10.0.0.0/24 allow access-control: 127.0.0.0/8 allow tls-cert-bundle: /etc/ssl/cert.pem forward-zone: name: "." forward-tls-upstream: yes forward-addr: 1.1.1.1@853#cloudflare-dns.com forward-addr: 9.9.9.9@853#dns.quad9.net
This encrypts DNS queries between your server and the upstream resolver.
Step 13: Performance Tuning
WireGuard is fast by default, but a few tweaks can extract more throughput on FreeBSD.
MTU Optimization
The default MTU for WireGuard is 1420. If your external interface has a standard 1500-byte MTU, 1420 is correct (1500 minus 80 bytes of WireGuard overhead). If your provider uses jumbo frames or a non-standard MTU, adjust accordingly:
ini[Interface] MTU = 1420
For providers with a 1500-byte path MTU, do not increase this value. If you experience fragmentation issues (large transfers stalling), try lowering it to 1400 or 1380.
Increase Socket Buffer Sizes
For high-throughput scenarios, increase the UDP socket buffers:
shsysctl net.inet.udp.recvspace=4194304 sysctl net.inet.udp.maxdgram=4194304
Make them persistent in /etc/sysctl.conf:
shellnet.inet.udp.recvspace=4194304 net.inet.udp.maxdgram=4194304
CPU Affinity
On multi-core servers handling many peers, you can pin the WireGuard processing to specific CPU cores using cpuset. This avoids cache thrashing:
shcpuset -l 0-1 -p $(pgrep -f wg0)
Disable TSO/LRO on the External Interface
If you observe poor throughput or packet drops, disabling TCP segmentation offload and large receive offload on the external interface can help:
shifconfig vtnet0 -tso -lro
Make it persistent in /etc/rc.conf:
shifconfig_vtnet0="inet YOUR_IP netmask YOUR_MASK -tso -lro"
Step 14: Troubleshooting Common Issues
Problem: Client cannot reach the server
- Check UDP port. Confirm port 51820 is open. From another machine:
nc -zu YOUR_SERVER_IP 51820. If it times out, check your PF rules or hosting provider firewall. - Check the kernel module. Run
kldstat | grep if_wg. If missing, runkldload if_wg. - Check the interface. Run
ifconfig wg0. If the interface does not exist, runwg-quick up wg0.
Problem: Handshake does not complete
- Verify keys. The most common cause is mismatched keys. Confirm the server's
[Peer] PublicKeymatches the client's actual public key, and vice versa. - Check AllowedIPs. The server's
AllowedIPsfor the peer must include the client's assigned IP (e.g.,10.0.0.2/32). - Check Endpoint. The client config must have the correct server IP and port in the
Endpointfield. - Clock skew. WireGuard uses timestamps for replay protection. If the server or client clock is significantly off, handshakes can fail. Install and enable NTP:
sysrc ntpd_enable="YES" && service ntpd start.
Problem: Handshake completes but no traffic flows
- IP forwarding. Verify
sysctl net.inet.ip.forwardingreturns 1. - NAT rule. Confirm PF is running (
pfctl -s info) and the NAT rule is active (pfctl -s nat). - Routing. On the server, check that packets from
10.0.0.0/24are being NATed. Runtcpdump -i wg0 -nto see if packets arrive on the WireGuard interface.
Problem: DNS is not working through the VPN
- Check the DNS setting. The client
wg0.confmust have aDNSline. - Check the resolver. If using
DNS = 10.0.0.1, Unbound must be running and listening on that address. - Test directly. From the client:
dig @10.0.0.1 example.com. If this works but normal resolution fails, the client's DNS override is not being applied. On Linux, check/etc/resolv.conf. On macOS, checkscutil --dns.
Problem: Slow performance
- Check MTU. Run
ping -D -s 1392 10.0.0.1from the client. If packets are lost, reduce the MTU in your WireGuard config. - Check CPU. Run
topon the server. If a single core is at 100%, WireGuard processing is CPU-bound. Consider a server with faster single-core performance. - Check the host. On virtualized environments, network performance can vary. Consider a dedicated server or a higher-tier VPS plan.
Problem: Connection drops after some time
- PersistentKeepalive. If the client is behind NAT, ensure
PersistentKeepalive = 25is set in the client's[Peer]block. - Firewall state timeout. PF's default UDP state timeout may be too short. Add to
/etc/pf.conf:set timeout { udp.first 120, udp.multiple 120, udp.single 60 }.
Step 15: Server Hardening
A VPN server is a high-value target. Apply these additional measures. For a comprehensive approach, follow the FreeBSD server hardening guide.
Restrict SSH to the VPN
Once WireGuard is working, restrict SSH access to the VPN subnet:
pfpass in on $wg_if proto tcp from $wg_net to any port 22 block in on $ext_if proto tcp from any to any port 22
Log blocked traffic
Enable PF logging for dropped packets:
pfblock log all
View the log with:
shtcpdump -n -e -ttt -r /var/log/pflog
Restrict WireGuard access by source IP
If your clients have known IPs, restrict UDP 51820 to those addresses:
pftable <wg_clients> { 203.0.113.10, 198.51.100.20 } pass in on $ext_if proto udp from <wg_clients> to any port 51820
PostUp and PostDown Scripts
Earlier the server configuration referenced postup.sh and postdown.sh. These scripts handle firewall rules dynamically so you do not need to manually edit /etc/pf.conf every time WireGuard starts or stops.
Create /usr/local/etc/wireguard/postup.sh:
sh#!/bin/sh # Enable IP forwarding sysctl net.inet.ip.forwarding=1 # Reload PF rules pfctl -f /etc/pf.conf
Create /usr/local/etc/wireguard/postdown.sh:
sh#!/bin/sh # Reload PF rules (NAT will be removed since wg0 is down) pfctl -f /etc/pf.conf
Make them executable:
shchmod 700 /usr/local/etc/wireguard/postup.sh chmod 700 /usr/local/etc/wireguard/postdown.sh
If you prefer to keep your PF rules static (as shown in Step 7), you can remove the PostUp and PostDown lines from wg0.conf.
Complete Configuration Reference
Below are the final, complete configuration files for reference.
Server: /usr/local/etc/wireguard/wg0.conf
ini[Interface] PrivateKey = aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuU= Address = 10.0.0.1/24 ListenPort = 51820 PostUp = /usr/local/etc/wireguard/postup.sh PostDown = /usr/local/etc/wireguard/postdown.sh [Peer] # Client 1 - Laptop PublicKey = xXyYzZaAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrR= AllowedIPs = 10.0.0.2/32 [Peer] # Client 2 - Phone PublicKey = wWxXyYzZaAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQ= AllowedIPs = 10.0.0.3/32 [Peer] # Client 3 - Remote office PublicKey = vVwWxXyYzZaAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpP= AllowedIPs = 10.0.0.4/32
Client: wg0.conf (full tunnel)
ini[Interface] PrivateKey = uUvVwWxXyYzZaAbBcCdDeEfFgGhHiIjJkKlLmMnNoO= Address = 10.0.0.2/24 DNS = 10.0.0.1 [Peer] PublicKey = pPqQrRsStTuUvVwWxXyYzZaAbBcCdDeEfFgGhHiIjJ= AllowedIPs = 0.0.0.0/0, ::/0 Endpoint = 203.0.113.1:51820 PersistentKeepalive = 25
Server: /etc/rc.conf additions
shkld_list="if_wg" gateway_enable="YES" ipv6_gateway_enable="YES" pf_enable="YES" pflog_enable="YES" wireguard_enable="YES" wireguard_interfaces="wg0" unbound_enable="YES" ntpd_enable="YES"
Server: /etc/pf.conf
pfext_if = "vtnet0" wg_if = "wg0" wg_net = "10.0.0.0/24" set skip on lo0 set timeout { udp.first 120, udp.multiple 120, udp.single 60 } scrub in all nat on $ext_if from $wg_net to any -> ($ext_if) block all # Allow SSH (restrict to VPN for better security) pass in on $ext_if proto tcp from any to any port 22 pass in on $wg_if proto tcp from $wg_net to any port 22 # Allow WireGuard pass in on $ext_if proto udp from any to any port 51820 # Allow all VPN traffic pass in on $wg_if from $wg_net to any pass out on $wg_if from any to $wg_net # Allow VPN traffic out to the internet pass out on $ext_if from $wg_net to any # Allow outbound from server itself pass out on $ext_if from ($ext_if) to any # Allow DNS to Unbound pass in on $wg_if proto { tcp, udp } from $wg_net to 10.0.0.1 port 53
Frequently Asked Questions
Is WireGuard included in the FreeBSD base system?
The if_wg kernel module is included in the FreeBSD kernel source tree starting with FreeBSD 13.0 and is available as a loadable module on all supported releases from 13.0 onward. The userspace tools (wg, wg-quick) are installed separately via pkg install wireguard-tools.
Can I run WireGuard and OpenVPN on the same server?
Yes. They use different interfaces and different ports. WireGuard uses UDP 51820 by default and creates a wg0 interface, while OpenVPN typically uses UDP 1194 and creates a tun0 interface. Assign them non-overlapping VPN subnets (e.g., 10.0.0.0/24 for WireGuard and 10.0.1.0/24 for OpenVPN) and add appropriate PF rules for both.
How do I revoke a client's access?
Remove the client's [Peer] block from the server's wg0.conf and reload:
shwg syncconf wg0 <(wg-quick strip wg0)
There is no certificate revocation list as in OpenVPN. Removing the peer's public key from the server configuration is sufficient. The client will no longer be able to complete a handshake.
Does WireGuard support IPv6?
Yes. WireGuard natively supports IPv6. Add an IPv6 address to the [Interface] section:
ini[Interface] Address = 10.0.0.1/24, fd00:wg::1/64
Add IPv6 to peer AllowedIPs:
ini[Peer] AllowedIPs = 10.0.0.2/32, fd00:wg::2/128
On the client, include ::/0 in AllowedIPs to route all IPv6 traffic through the VPN.
How many clients can a single FreeBSD WireGuard server support?
There is no hard protocol limit. Practical limits depend on CPU speed, network bandwidth, and available memory. A modern 4-core VPS can handle hundreds of peers with light traffic. For high-throughput deployments with many simultaneous clients, monitor CPU usage and consider scaling horizontally with multiple servers.
Can I use WireGuard for site-to-site VPN between FreeBSD servers?
Yes. Configure each server as a peer of the other. Set AllowedIPs to include the remote site's LAN subnet. For example, if Site A has 192.168.1.0/24 behind it and Site B has 192.168.2.0/24, configure Site A's peer block for Site B with AllowedIPs = 10.0.0.2/32, 192.168.2.0/24 and vice versa. Enable IP forwarding on both sides and add routes for the remote subnets.
How do I update WireGuard on FreeBSD?
Update the userspace tools via pkg:
shpkg upgrade wireguard-tools
The kernel module updates with FreeBSD system updates (freebsd-update fetch install). After a kernel update, reboot to load the new module.
Summary
WireGuard on FreeBSD gives you a fast, auditable, and simple VPN with kernel-level performance. The setup consists of five essential steps: install the tools, load the kernel module, generate keys, write the configuration, and open the firewall. Everything after that -- DNS, multi-peer, performance tuning -- builds on the same straightforward foundation.
For related guides, see OpenVPN on FreeBSD for a comparison with the traditional approach, PF firewall configuration for deeper firewall coverage, and FreeBSD server hardening for locking down the rest of your system.