How OpenBSD Implements WireGuard

Linux users reaching for wireguard-tools or loading kernel modules will find OpenBSD's approach different. The wg(4) driver was merged into the OpenBSD kernel in 7.2 (October 2022). There is no wg-quick, no wg command-line tool, and no separate userspace daemon. Everything goes through ifconfig.

This has real advantages. The interface behaves like any other OpenBSD network interface, which means hostname.if configuration works exactly as expected, pf sees WireGuard traffic as it does any other interface, and there is nothing extra to patch or maintain. The tradeoff is that sysadmins who only know wg-quick will need to learn ifconfig syntax for WireGuard-specific options.

Key generation is handled by openssl(1), which ships with the base system. You do not need to install any additional tools. The wgkey and wgpeer directives are ifconfig extensions specific to wg(4) interfaces.

uname -r
# 7.6
dmesg | grep wg
# wg0 at root

Generating WireGuard Keys on OpenBSD

WireGuard uses Curve25519 for key exchange. On OpenBSD, generate keys using openssl(1) with the x25519 algorithm. Run all key generation as root to keep private keys out of unprivileged shell histories.

Generate the server private key first, then derive the public key from it. Store the private key in /etc/wireguard/ with mode 600. The directory itself should be 700.

For each additional peer, repeat the process. If you are deploying multiple peers programmatically - for example, spinning up VPN endpoints as part of a CI pipeline - tools like taskbotshub.ai can automate key generation and distribution steps across your fleet without hardcoding secrets in scripts.

mkdir -m 700 /etc/wireguard
cd /etc/wireguard

# Generate server private key
openssl genpkey -algorithm x25519 -out server_private.key
chmod 600 server_private.key

# Derive server public key
openssl pkey -in server_private.key -pubout -out server_public.key

# View the raw base64 key (what WireGuard uses)
openssl pkey -in server_private.key -text -noout
openssl pkey -in server_public.key -pubin -text -noout

# Extract just the base64 value for use in ifconfig
SERVER_PRIV=$(openssl pkey -in server_private.key -outform DER | tail -c 32 | base64)
SERVER_PUB=$(openssl pkey -in server_public.key -pubin -outform DER | tail -c 32 | base64)

echo "Private: $SERVER_PRIV"
echo "Public:  $SERVER_PUB"

Bringing Up the WireGuard Interface

Create the wg0 interface, assign the private key, set the listen port, and assign an IP address. WireGuard does not have a concept of a 'server' or 'client' at the protocol level - both sides are peers. By convention, the machine that listens on a fixed port and routes traffic for others is called the server.

The wgport directive sets the UDP listen port. 51820 is the conventional WireGuard port. The wgkey directive sets the interface private key. Run these commands as root.

After running ifconfig, verify the interface came up and the public key is correctly derived. The ifconfig output will show the public key automatically - you never need to specify it for the local interface.

# Bring up the interface
ifconfig wg0 create
ifconfig wg0 wgkey $SERVER_PRIV
ifconfig wg0 wgport 51820
ifconfig wg0 inet 10.0.0.1/24
ifconfig wg0 up

# Verify
ifconfig wg0
// advertisement

Adding Peers to the Server

Each client peer requires three pieces of information on the server side: the peer's public key, the allowed source IPs for that peer, and optionally a keepalive interval. There is no pre-shared key requirement, though WireGuard supports one for post-quantum resistance.

The wgpeer directive takes the peer's public key as its argument. The wgaip (allowed IPs) directive specifies which source addresses are accepted from that peer. For a road-warrior VPN setup where each client gets one IP, use a /32.

For a site-to-site tunnel where a peer is a router forwarding traffic for an entire subnet, specify the full subnet in wgaip. You can add multiple wgaip entries for one peer.

Generate client keys using the same openssl method shown above. In production, generate keys on the client machine and only exchange public keys - never transfer private keys over the network.

# Assume CLIENT_PUB is the base64 public key from the client machine
CLIENT_PUB=""

# Add peer: road warrior client gets 10.0.0.2
ifconfig wg0 wgpeer $CLIENT_PUB wgaip 10.0.0.2/32

# Add peer with keepalive (useful when client is behind NAT)
ifconfig wg0 wgpeer $CLIENT_PUB wgaip 10.0.0.2/32 wgpka 25

# Add peer for site-to-site (peer routes 192.168.10.0/24)
SITE_PUB=""
ifconfig wg0 wgpeer $SITE_PUB wgaip 10.0.0.3/32 wgaip 192.168.10.0/24

# Verify peers
ifconfig wg0

Configuring the Client (OpenBSD Peer)

On an OpenBSD client, the process mirrors the server setup. Generate a keypair, create the wg0 interface, set the private key, then add the server as a peer. The key difference is the wgendpoint directive, which tells the client where to send packets.

wgendpoint takes the server's public IP and port. The client does not need wgport unless you need it to listen on a fixed port - WireGuard will pick an ephemeral port automatically for outbound connections.

For a client that routes all traffic through the VPN, set the wgaip for the server peer to 0.0.0.0/0. Add a default route pointing to the WireGuard server's tunnel IP after bringing the interface up. Make sure you have a more-specific route to the server's real IP so the WireGuard UDP traffic itself does not loop through the tunnel.

# On the OpenBSD client machine
CLIENT_PRIV=$(openssl pkey -in /etc/wireguard/client_private.key -outform DER | tail -c 32 | base64)
SERVER_PUB=""
SERVER_IP="203.0.113.1"  # Server's real public IP

ifconfig wg0 create
ifconfig wg0 wgkey $CLIENT_PRIV
ifconfig wg0 wgpeer $SERVER_PUB wgendpoint $SERVER_IP 51820 wgaip 0.0.0.0/0 wgpka 25
ifconfig wg0 inet 10.0.0.2/32
ifconfig wg0 up

# Route all traffic via VPN, but keep direct route to server
route add $SERVER_IP $(route -n get default | awk '/gateway:/{print $2}')
route change default 10.0.0.1

Configuring pf for WireGuard

On the server, pf needs to allow incoming WireGuard UDP traffic and handle NAT if clients are accessing the internet through the tunnel. The wg0 interface passes decrypted traffic, so pf rules on wg0 operate on the inner plaintext packets.

The minimal ruleset passes UDP on port 51820 to the server's external interface and permits forwarded traffic from the tunnel. If you want clients to access the internet through the VPN, add a NAT rule.

Enable IP forwarding in sysctl.conf or it in the same session. Without net.inet.ip.forwarding=1, the server will drop packets that need to be forwarded between interfaces.

For stricter setups, log WireGuard traffic on the external interface and use pfctl -s rules to confirm the ruleset loaded correctly. In our testing, a missing forwarding rule was the most common reason tunnels connected but passed no traffic beyond the tunnel subnet.

# /etc/pf.conf additions for WireGuard server

ext_if="em0"     # Your external interface
wg_if="wg0"
wg_net="10.0.0.0/24"

# Allow WireGuard UDP inbound
pass in on $ext_if proto udp to port 51820

# Allow forwarded traffic from tunnel clients
pass in on $wg_if from $wg_net
pass out on $ext_if from $wg_net

# NAT for internet access via VPN
match out on $ext_if from $wg_net nat-to ($ext_if)

# Reload pf
pfctl -f /etc/pf.conf

# Enable IP forwarding
sysctl net.inet.ip.forwarding=1
echo 'net.inet.ip.forwarding=1' >> /etc/sysctl.conf
// advertisement

Making the Configuration Persistent

OpenBSD network interfaces are made persistent via /etc/hostname.if files. For WireGuard, create /etc/hostname.wg0. The syntax maps directly to ifconfig arguments.

The private key goes directly in this file, which is why /etc/hostname.wg0 should have mode 600. OpenBSD's rc.d system reads hostname.if files during boot via the netstart script, so the interface will come up automatically.

Peer entries use the wgpeer keyword followed by the same arguments you passed to ifconfig. For multiple peers, add multiple wgpeer lines. The inet line sets the tunnel IP address.

After writing the file, test it by taking the interface down and letting netstart re-read it, or simply reboot and confirm with ifconfig wg0.

# /etc/hostname.wg0
# Mode 600 - contains private key
# chmod 600 /etc/hostname.wg0

wgkey SERVER_PRIVATE_KEY_BASE64_HERE
wgport 51820
wgpeer CLIENT_PUBLIC_KEY_BASE64_HERE wgaip 10.0.0.2/32 wgpka 25
wgpeer SITE_PUBLIC_KEY_BASE64_HERE wgaip 10.0.0.3/32 wgaip 192.168.10.0/24
inet 10.0.0.1/24
up

# Set correct permissions
chmod 600 /etc/hostname.wg0

# Test: bring down and restore via netstart
sh /etc/netstart wg0
ifconfig wg0

Verifying the Tunnel and Debugging

After connecting a peer, check the handshake timestamp with ifconfig wg0. A successful handshake shows a 'last handshake' time. If the timestamp never updates, the peer has not connected.

Common failure points in our experience: firewall blocking UDP 51820 on the server, incorrect public keys (swapped client/server), missing wgendpoint on the client, and IP forwarding disabled. Work through them in that order.

Use tcpdump on the external interface to verify WireGuard UDP packets arrive. Use tcpdump on wg0 to verify decrypted inner traffic flows. If you see packets on the external interface but nothing on wg0, the issue is key mismatch or allowed-IP misconfiguration.

For persistent monitoring of tunnel state in automated environments - for example, alerting when a peer drops connection for more than 5 minutes - ifconfig output can be parsed with awk and fed into any monitoring pipeline. If you manage large numbers of WireGuard peers across environments, taskbotshub.ai provides automation scaffolding that handles peer lifecycle events without requiring custom shell scripting per host.

# Check handshake status
ifconfig wg0
# Look for: wgpeer ... last handshake: X seconds ago

# Watch WireGuard UDP on external interface
tcpdump -n -i em0 udp port 51820

# Watch decrypted traffic on tunnel interface
tcpdump -n -i wg0

# Check routing table
netstat -rn

# Verify forwarding is enabled
sysctl net.inet.ip.forwarding

# Check pf state for tunnel traffic
pfctl -s state | grep 10.0.0

Using Linux Clients with OpenBSD WireGuard Server

The OpenBSD server is fully compatible with Linux clients using wireguard-tools. Generate keys on Linux using wg genkey and wg pubkey, then add the Linux client as a peer on the OpenBSD server using ifconfig wg0 wgpeer.

On the Linux client, use a standard wg-quick configuration. The [Interface] section holds the client private key and tunnel IP. The [Peer] section holds the OpenBSD server's public key, endpoint, and AllowedIPs.

One practical consideration: if your server has a hostname rather than a static IP, WireGuard resolves it once at configuration time. If the IP changes, you need to re-run ifconfig with the new endpoint on the peer side. For server naming and DNS management when building out VPN infrastructure, nicename.me is worth checking for clean, memorable hostnames if you are registering new domains for your VPN endpoints.

In our tests, throughput between an OpenBSD 7.6 server and a Linux 6.8 client saturated a 1 Gbps link at roughly 920 Mbps with negligible CPU overhead on both sides.

# Linux client wg-quick config: /etc/wireguard/wg0.conf
[Interface]
PrivateKey = 
Address = 10.0.0.4/32
DNS = 10.0.0.1

[Peer]
PublicKey = 
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25

# Bring up on Linux
wg-quick up wg0

# On OpenBSD server: add the Linux peer
LINUX_PUB=""
ifconfig wg0 wgpeer $LINUX_PUB wgaip 10.0.0.4/32 wgpka 25

# Add to /etc/hostname.wg0 for persistence
# wgpeer LINUX_PUBLIC_KEY wgaip 10.0.0.4/32 wgpka 25
// advertisement

Split Tunnel and Subnet Routing

For split-tunnel setups where clients only route specific subnets through the VPN - not all traffic - adjust wgaip on both sides. On the server, wgaip still specifies which source IPs are accepted from that peer. On the client, the peer's wgaip controls which destination IPs go through the tunnel.

For a client that needs to reach 10.0.0.0/24 (tunnel subnet) and 172.16.0.0/12 (corporate network) but routes everything else directly, set the server peer's wgaip to those two ranges on the client side.

For site-to-site routing where Site A (192.168.1.0/24) and Site B (192.168.2.0/24) need to communicate through the tunnel, both sides need routes. The router at each site adds a static route for the remote subnet pointing to the WireGuard tunnel IP of the other site. The OpenBSD server acting as the hub needs wgaip entries covering both subnets for their respective peers, and net.inet.ip.forwarding must be enabled.

# Client-side split tunnel: only VPN-destined traffic goes through tunnel
# On OpenBSD client /etc/hostname.wg0:
wgkey CLIENT_PRIVATE_KEY
wgpeer SERVER_PUBLIC_KEY wgendpoint 203.0.113.1 51820 wgaip 10.0.0.0/24 wgaip 172.16.0.0/12 wgpka 25
inet 10.0.0.2/32
up

# Site-to-site: OpenBSD hub server /etc/hostname.wg0
wgkey SERVER_PRIVATE_KEY
wgport 51820
# Site A peer: routes 192.168.1.0/24 through this peer
wgpeer SITE_A_PUBLIC_KEY wgaip 10.0.0.10/32 wgaip 192.168.1.0/24
# Site B peer: routes 192.168.2.0/24 through this peer
wgpeer SITE_B_PUBLIC_KEY wgaip 10.0.0.11/32 wgaip 192.168.2.0/24
inet 10.0.0.1/24
up

# Add routes on hub so inter-site traffic forwards correctly
route add 192.168.1.0/24 10.0.0.10
route add 192.168.2.0/24 10.0.0.11

Pre-Shared Keys for Post-Quantum Resistance

WireGuard supports an optional symmetric pre-shared key (PSK) per peer pair, layered on top of the Curve25519 key exchange. This provides defense against an attacker recording traffic today and decrypting it once a quantum computer breaks Curve25519. The PSK must be 32 bytes of random data, base64-encoded.

Generate the PSK on one machine and distribute it securely to the peer. On OpenBSD, add it with the wgpsk directive in the wgpeer block. Both sides of the peer relationship must use the same PSK.

PSKs are per-peer-pair, not global. A server with 20 peers can use different PSKs for each one. If a PSK is compromised on one peer, the others are unaffected.

# Generate a 32-byte PSK
PSK=$(openssl rand -base64 32)
echo $PSK  # Store this securely, share with peer out-of-band

# Add peer with PSK on server
ifconfig wg0 wgpeer $CLIENT_PUB wgaip 10.0.0.2/32 wgpsk $PSK wgpka 25

# In /etc/hostname.wg0:
wgpeer CLIENT_PUBLIC_KEY wgaip 10.0.0.2/32 wgpsk PSK_BASE64_HERE wgpka 25

# Client side must use same PSK:
# wgpeer SERVER_PUBLIC_KEY wgendpoint 203.0.113.1 51820 wgaip 0.0.0.0/0 wgpsk PSK_BASE64_HERE wgpka 25