Skip to content
FreeBSDHow-To Published Updated 6 min readViews unavailable

How to Set Up a WireGuard VPN on FreeBSD

A least-privilege FreeBSD WireGuard setup covering if_wg, protected keys, cryptokey routing, PF exposure, NAT peers, rc startup, traffic tests, and rotation.

FreeBSD provides an in-kernel WireGuard interface and a base wg control utility. The wireguard-tools package adds wg-quick and an rc script that makes a small deployment convenient. WireGuard encrypts IP packets between authenticated public keys; it does not assign identities to users, distribute keys, configure DNS policy, protect endpoints after decryption, or decide which networks a peer may route.

This example creates a narrow peer-to-peer tunnel: a reachable server at 10.66.0.1 and a roaming client at 10.66.0.2. It does not send all Internet traffic through the VPN.

Verify the FreeBSD implementation and tools

Record the platform and check the module, base tool, listeners, routes, and firewall:

freebsd-version -kru
which wg
wg --version
kldstat | grep if_wg
service pf status
netstat -rn

The kernel module is if_wg, not wg. Load it explicitly when needed:

kldload if_wg
kldstat | grep if_wg

Creating a WireGuard interface can also trigger module loading on a standard system. If if_wg is absent from a custom kernel installation, resolve the kernel/module mismatch rather than installing an unrelated binary.

FreeBSD 15.1 includes wg in base, but wg-quick and the packaged rc integration come from net/wireguard-tools:

pkg install wireguard-tools
pkg info wireguard-tools
pkg audit -F

The default package’s WGQUICK option adds Bash as a runtime dependency. If a lite flavor or custom package omits it, this article’s wg-quick and rc commands will not exist.

Generate keys without exposing private material

Generate a distinct pair on each peer. Set the umask before creating files so a default world-readable mode never exists, even briefly:

install -d -o root -g wheel -m 0700 \
  /usr/local/etc/wireguard/keys
umask 077
wg genkey > /usr/local/etc/wireguard/keys/wg0.key
wg pubkey < /usr/local/etc/wireguard/keys/wg0.key \
  > /usr/local/etc/wireguard/keys/wg0.pub
ls -l /usr/local/etc/wireguard/keys

Run this independently on server and client. Exchange only .pub contents through an authenticated channel and verify them out of band. Never reuse a private key across machines, paste it into tickets or chat, or store it in a world-readable backup.

WireGuard public keys are device credentials. Maintain an inventory mapping each key to owner, device, allowed prefixes, issue time, and revocation status. Removing a lost peer’s public key from every server is the revocation mechanism.

Design tunnel addresses and AllowedIPs

Choose a tunnel subnet that does not overlap LANs, cloud networks, containers, or other VPNs. In each peer entry, AllowedIPs has two roles: it selects outbound traffic for that peer and restricts which inner source addresses are accepted from that peer. Overlapping entries can route traffic to the wrong key or be rejected by tooling.

For a single client, the server should assign only 10.66.0.2/32 to that client’s key. The client needs only 10.66.0.1/32 when it accesses the server itself. Do not copy 0.0.0.0/0 or ::/0 unless a reviewed full-tunnel design includes forwarding, NAT or routed prefixes, DNS, IPv6, kill-switch behavior, and recovery when the tunnel fails.

Create a protected server configuration

Create /usr/local/etc/wireguard/wg0.conf mode 0600. Substitute the server’s private key and the client’s public key:

[Interface]
PrivateKey = SERVER_PRIVATE_KEY
Address = 10.66.0.1/24
ListenPort = 51820

[Peer]
PublicKey = CLIENT_PUBLIC_KEY
AllowedIPs = 10.66.0.2/32

Protect and inspect the file:

chown root:wheel /usr/local/etc/wireguard/wg0.conf
chmod 0600 /usr/local/etc/wireguard/wg0.conf

The server does not need a fixed client Endpoint for a roaming or NATed peer. It learns the authenticated source endpoint from received traffic. Adding an obsolete client address can prevent the first working path.

If this server routes a client LAN as well as its tunnel address, add that exact LAN prefix to this peer’s AllowedIPs, enable IP forwarding, add return routes, and authorize forwarding in PF. A peer-to-peer tunnel alone does not make the host a gateway.

Create the roaming client configuration

On the client, create its own protected wg0.conf:

[Interface]
PrivateKey = CLIENT_PRIVATE_KEY
Address = 10.66.0.2/32

[Peer]
PublicKey = SERVER_PUBLIC_KEY
AllowedIPs = 10.66.0.1/32
Endpoint = vpn.example.net:51820
PersistentKeepalive = 25

Resolve vpn.example.net to the server’s reachable address. PersistentKeepalive = 25 is useful on the NATed client to keep a mapping available; it is not a universal performance setting and usually need not be placed on the publicly reachable server.

Configuration files contain private keys, so back them up encrypted and exclude them from ordinary source control. An optional preshared key adds symmetric material but increases distribution and rotation obligations; it does not replace the public-key design.

Permit only the WireGuard listener in PF

On the server, a PF fragment can allow the UDP listener on the actual external interface:

ext_if = "em0"
pass in on $ext_if inet proto udp from any \
    to ($ext_if) port 51820 keep state

This is intentionally to ($ext_if), not to any. Restrict the source when peers have stable addresses. Roaming clients may require from any, but authentication still occurs through WireGuard keys and PF rate/logging policy should be monitored.

Validate the complete ruleset before loading it:

pfctl -nf /etc/pf.conf
pfctl -f /etc/pf.conf
pfctl -sr

Also check cloud firewalls, router port forwarding, carrier NAT, and upstream ACLs. Never expose management TCP ports merely because the VPN is not working yet.

Bring up, test, and tear down manually

Start the server, then client:

wg-quick up wg0
ifconfig wg0
wg show wg0
netstat -rn

WireGuard is intentionally quiet until traffic exists. From the client, generate traffic and inspect both peers:

ping -c 3 10.66.0.1
wg show wg0
wg show wg0 latest-handshakes
wg show wg0 transfer

A current handshake with increasing counters proves encrypted packets were exchanged, not that every application works. Test a real service bound to the correct address and verify PF policy inside the tunnel. On the server, capture encrypted UDP on the external interface and inner traffic on wg0 when diagnosing:

tcpdump -ni em0 udp port 51820
tcpdump -ni wg0

No outer packets means DNS, routing, upstream NAT, or firewall. Outer packets without a handshake suggests keys, time, or peer configuration. A handshake without inner traffic points toward AllowedIPs, routes, service binds, or inner firewall rules. Stalls only on large packets may require an evidence-based MTU adjustment; do not lower MTU randomly.

After the manual test, stop the interface cleanly before testing rc startup:

wg-quick down wg0

Enable boot startup and prove recovery

The port’s rc script uses these settings:

sysrc wireguard_enable=YES
sysrc wireguard_interfaces="wg0"
service wireguard start
service wireguard status

Do not run wg-quick up and then start the service against the same existing interface. Test service wireguard reload only after validating configuration, because route-changing edits can disconnect remote access.

Reboot during a maintenance window and verify module loading, interface address, routes, PF, DNS for the endpoint, handshake after generated traffic, and application access. Monitor key inventory, last handshake, transfer counters, listener availability, route drift, and certificate-independent endpoint DNS.

Practice key rotation: add the new peer key with a temporary narrow address, test it, move authorized prefixes deliberately, then remove the old key. A WireGuard deployment is complete when private keys remain private, each key has minimal prefixes, the external attack surface is narrow, boot recovery works, and revocation is rehearsed.

Related:

Sources:

Comments