WireGuard VPN Routing on FreeBSD VPS for Remote Access

Why WireGuard VPN routing matters on a FreeBSD VPS
WireGuard VPN routing gives you a private path into a server without exposing admin services to the public internet. For Hostperl customers, that usually means safer SSH access, cleaner support workflows, and fewer port-forwarding exceptions when a client, agency, or remote admin needs an internal service.
This tutorial builds a production-ready WireGuard gateway on FreeBSD, then routes a private VPN subnet through the VPS so remote devices can reach the server and any allowed internal network behind it. FreeBSD fits this use case well because the native tools work together cleanly: pkg, pf, rc.conf, and freebsd-update.
Related reading: if you are planning a remote-access path for a new server, start with colocation hosting if you own hardware, or Hostperl VPS if you want a simpler managed entry point. For background on secure tunneling, our Linux WireGuard guide covers the same operational goal on Linux.
What you will build
You will create a FreeBSD VPN server at 203.0.113.10, assign the tunnel address 10.10.0.1/24, route client traffic through it, and verify that the tunnel stays up after reboot. The example IP is a reserved documentation address; replace it with the real public IP assigned to your server.
The design keeps the public SSH port open only on the VPS itself. The VPN handles private access for admin traffic, which is a practical setup for launch windows, migrations, and support handoffs when you do not want to publish internal service ports.
Prerequisites and network plan
- A FreeBSD 14.x VPS with root access.
- A second terminal or another device for testing the tunnel.
- One free UDP port, typically
51820. - A private VPN subnet that does not overlap your LAN or datacenter ranges.
We will use these example values throughout:
- Server public IP:
203.0.113.10 - VPN server tunnel IP:
10.10.0.1/24 - Client tunnel IP:
10.10.0.2/32 - WireGuard UDP port:
51820 - Interface name:
wg0
Connect to the FreeBSD VPS and confirm the OS
On your local computer, connect with SSH first.
ssh root@203.0.113.10203.0.113.10 is a documentation example and must be replaced with your server’s real public IP. Keep your original root session open while you complete the setup.
If your provider gives you a default non-root account, use it first and elevate with su - or sudo later:
ssh deploy@203.0.113.10On the VPS as root, confirm the operating system:
freebsd-versionYou should see a FreeBSD release line such as 14.x-RELEASE. That confirms this tutorial applies to your system.
Update FreeBSD and install WireGuard tools
On the VPS as root, patch the base system first. That keeps you from tunneling over an outdated kernel or userland.
freebsd-update fetch installRun it again if the tool reports that another pass is needed after a reboot.
Now install WireGuard support from the FreeBSD package repository:
pkg updatepkg install -y wireguard-toolsCheck that the commands are available:
wg --versionwg-quick --versionBoth commands should return version output. If wireguard-tools is missing, the rest of the tutorial will not work.
Create the tunnel keys safely
WireGuard uses one key pair per peer. Generate the server key pair on the VPS and lock down the files immediately.
On the VPS as root, create a working directory:
install -d -m 700 /usr/local/etc/wireguardGenerate the private and public keys:
umask 077wg genkey | tee /usr/local/etc/wireguard/server.key | wg pubkey > /usr/local/etc/wireguard/server.pubConfirm the files exist and are private:
ls -l /usr/local/etc/wireguard/server.*Generate the client key pair on your client device or another trusted workstation, not on the production server. Keep the client private key off the VPS. For example, on a trusted Linux client:
umask 077wg genkey | tee client.key | wg pubkey > client.pubCopy the contents of client.pub into the server config later. Save client.key for the client device.
Build the WireGuard server configuration
On the VPS as root, create /usr/local/etc/wireguard/wg0.conf:
ee /usr/local/etc/wireguard/wg0.confPaste this file content, replacing the placeholder public key with the actual client public key from client.pub:
[Interface]
PrivateKey = REPLACE_WITH_SERVER_PRIVATE_KEY
Address = 10.10.0.1/24
ListenPort = 51820
SaveConfig = false
PostUp = /sbin/pfctl -f /etc/pf.conf
PostDown = /sbin/pfctl -f /etc/pf.conf
[Peer]
PublicKey = REPLACE_WITH_CLIENT_PUBLIC_KEY
AllowedIPs = 10.10.0.2/32Before saving, replace REPLACE_WITH_SERVER_PRIVATE_KEY with the contents of /usr/local/etc/wireguard/server.key. The AllowedIPs line keeps the client limited to its own tunnel IP.
Save and exit the editor, then verify the file:
cat /usr/local/etc/wireguard/wg0.confConfirm that no extra whitespace or shell history accidentally exposed the key.
Enable packet forwarding and build the firewall rules
WireGuard can carry traffic only if the server forwards packets correctly. On FreeBSD, that means enabling kernel forwarding and writing a matching pf rule set.
On the VPS as root, enable IPv4 forwarding in /etc/rc.conf:
sysrc gateway_enable="YES"If you also plan to route IPv6 over the tunnel, enable IPv6 forwarding too:
sysrc ipv6_gateway_enable="YES"Now create a minimal PF configuration. First back up the current file:
cp /etc/pf.conf /etc/pf.conf.bakOpen the file for editing:
ee /etc/pf.confUse this example content. If your VPS uses a different public interface, replace em0 with the correct one from ifconfig output:
ext_if = "em0"
wg_if = "wg0"
wg_net = "10.10.0.0/24"
set skip on lo
scrub in all
nat on $ext_if from $wg_net to any -> ($ext_if)
pass in on $ext_if proto udp from any to ($ext_if) port 51820 keep state
pass on $wg_if from $wg_net to any keep state
pass out on $ext_if from any to any keep stateSave and exit. Then syntax-check PF before loading it:
pfctl -nf /etc/pf.confThe command should return no output. Any error here must be fixed before you enable PF.
Load the firewall rules now:
service pf enableservice pf startCheck that PF is running:
service pf statusEnable WireGuard at boot and start the tunnel
FreeBSD runs WireGuard through rc services. Enable the interface and bring it up:
sysrc wireguard_enable="YES"sysrc wireguard_interfaces="wg0"service wireguard start wg0Check the tunnel interface and listen state:
ifconfig wg0wg showYou should see 10.10.0.1/24 on wg0 and a listening UDP socket on port 51820.
Build the client configuration
On the client device, create a WireGuard profile that points to the server public IP. Use the same subnet values so routing stays consistent.
On your local computer, create a client config file such as client-wg0.conf:
[Interface]
PrivateKey = REPLACE_WITH_CLIENT_PRIVATE_KEY
Address = 10.10.0.2/32
DNS = 1.1.1.1
[Peer]
PublicKey = REPLACE_WITH_SERVER_PUBLIC_KEY
Endpoint = 203.0.113.10:51820
AllowedIPs = 10.10.0.0/24, 203.0.113.10/32
PersistentKeepalive = 25Replace the private key with the client private key, and replace the public key with the contents of /usr/local/etc/wireguard/server.pub. If you want full-tunnel routing, change AllowedIPs to 0.0.0.0/0, ::/0, but only do that after you confirm the site-to-site or admin path works.
Bring the tunnel up and test connectivity
On your local computer, activate the client interface. On Linux this may be wg-quick up client-wg0; on macOS or Windows, import the profile in the WireGuard app and enable it there.
For a Linux client, the command is:
sudo wg-quick up client-wg0Then inspect the handshake:
sudo wg showLook for a recent handshake time and transferred bytes. That confirms the keys, firewall, and endpoint all match.
Test basic reachability to the server tunnel IP:
ping -c 3 10.10.0.1From the client, you should get replies. If you do not, check the server logs and interface state before changing anything else.
From the VPS, confirm that the peer is visible:
wg show wg0You should see the client peer listed with its public key, endpoint, and handshake time.
Turn the tunnel into a useful admin path
A VPN is only useful if it fits an actual support or operations workflow. A common Hostperl use case is restricting SSH to the VPN and allowing direct admin access only after the tunnel proves stable.
Do not cut off public SSH until you have a working second terminal over the VPN. First, test that you can SSH to the server via the public IP while the VPN is connected. Then, if you want to lock SSH down, use PF to allow port 22 only from 10.10.0.0/24 and your trusted management IPs.
Here is a safer PF add-on rule set that keeps public SSH open during testing and narrows access later. Back up /etc/pf.conf first, then edit the file and replace the SSH rule only after you confirm VPN access works.
pass in on $ext_if proto tcp from any to ($ext_if) port 22 keep stateAfter validation, you can change the source restriction to a known management subnet. Always load the new rule only after running pfctl -nf /etc/pf.conf.
Verification from the server and the client
On the VPS as root, run these checks:
service wireguard status wg0netstat -an | grep 51820wg showpfctl -srExpected results: WireGuard is running, UDP port 51820 is listening, PF has the NAT and pass rules, and the peer shows a recent handshake.
On the client, run:
ping -c 3 10.10.0.1traceroute 10.10.0.1You should see the tunnel endpoint as the first hop or direct route. If you are routing additional private networks, test those next with a known host or service port.
Rollback and recovery
If the tunnel breaks or the firewall blocks access, recover in this order. First, keep your original root SSH session open. Second, restore the firewall backup:
cp /etc/pf.conf.bak /etc/pf.confpfctl -nf /etc/pf.confpfctl -f /etc/pf.confIf WireGuard itself is the problem, stop it without touching the firewall:
service wireguard stop wg0To disable boot startup until you fix the config, run:
sysrc -x wireguard_enablesysrc -x wireguard_interfacesWhen the issue is a bad key or wrong endpoint, fix wg0.conf, then restart the service:
service wireguard restart wg0Troubleshooting the most likely failures
Handshake never appears. Diagnose with:
wg show wg0If the endpoint is empty or stale, confirm the client is using 203.0.113.10:51820, the server public key is correct, and the firewall allows UDP 51820. Then re-run service pf status and pfctl -sr.
Pings fail but the handshake exists. Check routing and NAT with:
pfctl -snnetstat -rnIf NAT is missing, rebuild /etc/pf.conf and reload it. If the client uses a full-tunnel route, make sure the client config includes all required subnets.
The service fails at boot. Read the logs:
tail -50 /var/log/messagesLook for malformed wg0.conf, a typo in the interface name, or a PF syntax error. Fix the file, then test syntax before another restart.
You locked yourself out of SSH. Use the VPS console or provider rescue access, restore /etc/pf.conf from backup, and only then tighten the SSH rule again. This is exactly why you keep the first root session open until the VPN is verified.
Keep the setup healthy after launch
Update FreeBSD regularly with freebsd-update fetch install, and review wg show whenever a client says the tunnel feels slow or stale. For long-running admin paths, store the client profile securely and rotate keys if a laptop is lost or an agency handoff ends.
If you later move this setup onto a larger Hostperl environment, the same tunnel pattern works well for maintenance windows, remote hands coordination, and private service access. For capacity planning or a move to higher-throughput infrastructure, dedicated server hosting and colocation power planning are the usual next steps.
If you want a VPS ready for secure remote administration, Hostperl can provision the base server and leave you free to focus on the tunnel and access policy. A well-sized Hostperl VPS gives you a solid starting point for WireGuard, firewall control, and future growth.
For teams that need more headroom or hardware-level control, consider dedicated server hosting instead. That is often the cleaner choice when VPN traffic, admin access, and private services all share the same machine.
FAQ
Can I route all client internet traffic through FreeBSD WireGuard?
Yes. Change the client AllowedIPs to 0.0.0.0/0, ::/0 and make sure NAT and forwarding are correct. Test private admin access first, then enable full-tunnel routing.
Should I expose SSH on the public internet after the VPN is ready?
Only if you need a break-glass path. For most admin workflows, keep SSH reachable only from trusted management IPs or from the VPN subnet.
Why use PF instead of a minimal allow-all rule?
PF gives you explicit control over UDP port 51820, NAT, and later SSH restrictions. That matters when you are supporting real customers or transitioning servers during a migration.
What if I need IPv6 over the tunnel too?
Add IPv6 addresses to both peers, enable ipv6_gateway_enable, and extend PF rules for IPv6 forwarding and NAT66 or routed prefixes where appropriate. Test IPv4 first so you isolate problems cleanly.
