| pick your distro, get ZFS on root
kldload — your platform, your way, free
Source

WireGuard Masterclass

The elephant in the room

WireGuard is a kernel module. On kldload, it ships with every install — CentOS and Debian — as part of the stock kernel. No DKMS, no patches, no extra packages. It’s just there.

This matters more than most people realize.

When you create a WireGuard interface, the kernel creates a network device that looks and behaves exactly like a physical NIC. Applications don’t know they’re running on a tunnel. ping, ssh, curl, nginx, postgres — they all see a normal network interface with an IP address. The encryption happens below them, in the kernel, before any packet hits the physical wire.

From the outside — from the perspective of anyone scanning your network, your ISP, or anything sitting between your nodes — all they see is UDP traffic on a single port. They can’t see what’s inside. They can’t see how many services are running. They can’t fingerprint what you’re doing. They can’t even tell it’s WireGuard without deep packet inspection, and even then, WireGuard’s packets look like random noise because they don’t respond to unauthenticated traffic at all.

A WireGuard interface that has no peers configured is completely silent. It doesn’t respond to anything. It doesn’t even acknowledge that it exists.

This means you can build entire private networks — backplanes, control planes, data planes — that run silently underneath whatever your “visible” OS is doing. The applications running on top don’t know. The outside world doesn’t know. It’s an invisible encrypted layer between your machines.

kldload has this built in to every install. Here’s how to use it.

Stop thinking about WireGuard as a "VPN." A VPN is something you connect to. WireGuard is a network interface — it exists in the kernel like eth0 exists. You don’t "connect" to it any more than you "connect" to your Ethernet card. You configure it and it’s there. This mental shift matters because everything on this page — backplanes, multi-plane isolation, stealth configs, mesh topologies — follows from that one fact. It’s not a tunnel you open and close. It’s a permanent part of the machine’s network stack. Every packet that matches an AllowedIPs rule gets encrypted and sent through it automatically, at kernel speed, with zero application awareness. That’s not a VPN. That’s a new network layer.

What makes WireGuard different

It’s in the kernel

OpenVPN runs in userspace. IPSec has a complex kernel/userspace split. WireGuard runs entirely in the kernel as a network device. This means:

  • No context switches between kernel and userspace for packet processing
  • No TUN/TAP overhead — packets go directly through the kernel network stack
  • Kernel-level routing — WireGuard interfaces participate in the routing table like any physical NIC
  • Applications are oblivious — they bind to addresses and send packets. The kernel handles encryption transparently.

It’s silent by default

WireGuard implements a concept called cryptokey routing. A peer is identified by its public key and a list of allowed IP ranges. If a packet arrives that doesn’t match any peer’s public key, WireGuard doesn’t respond. At all. No RST, no ICMP unreachable, no “port closed” message. Nothing.

This means: - Port scanners see nothing - Unauthenticated packets are silently dropped - The UDP port appears “filtered” or non-existent to outsiders - There is no handshake until both sides have the correct keys

Cryptokey routing is the single most important concept on this page. In every other VPN, routing and authentication are separate systems. You authenticate with certificates, then routing rules decide where packets go. In WireGuard, the public key IS the routing entry. "Packets for 10.200.0.2/32 go to the peer with public key X" — that’s it. Authentication and routing are the same thing. This means there’s no state to get out of sync, no certificate expiry that silently breaks routing, no "authenticated but can’t route" failure mode. If the key matches, the route works. If it doesn’t, the packet is silently dropped. This is why WireGuard configs are so small — the peer block IS the routing table and the ACL and the authentication, all in one.

It’s stateless on disk

A WireGuard configuration is just a key pair and a peer list. There’s no certificate authority, no PKI infrastructure, no certificate renewal, no CRL checking. Two machines that know each other’s public keys can communicate. Period.

It roams automatically

If a peer’s IP address changes (laptop moves from WiFi to cellular, VM migrates, dynamic IP renews), WireGuard detects it from the next authenticated packet and updates the endpoint automatically. No reconnection, no renegotiation.


Building a silent backplane

A backplane is a private network that runs underneath your production services. Your services bind to the backplane addresses. The outside world only sees the physical interface.

A backplane is the single most useful thing you can build with WireGuard, and most people never think of it. The idea: your services don't listen on the public interface at all. SSH, databases, monitoring, internal APIs — they all bind to WireGuard addresses. The public interface only has one port open: WireGuard's UDP port. And that port doesn't respond to unauthenticated traffic. From the internet's perspective, your server doesn't exist. From the backplane's perspective, it's running a full production stack. This is how every serious infrastructure team runs their servers. The only difference is they usually use expensive SD-WAN products to do what WireGuard does for free.

The concept

┌─────────────────────────────────────────────────────────────┐
│                    The visible world                         │
│                                                             │
│  eth0: 203.0.113.10          eth0: 203.0.113.20            │
│  (public, scannable,         (public, scannable,            │
│   only UDP 51820 open)        only UDP 51820 open)          │
│         │                            │                      │
│         └────── UDP 51820 ───────────┘                      │
│                (encrypted noise)                            │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                    The invisible backplane                   │
│                                                             │
│  wg0: 10.200.0.1            wg0: 10.200.0.2               │
│  ├── SSH (port 22)           ├── SSH (port 22)              │
│  ├── Postgres (5432)         ├── nginx (80, 443)            │
│  ├── Prometheus (9090)       ├── node_exporter (9100)       │
│  └── Grafana (3000)          └── app server (8080)          │
│                                                             │
│  These services ONLY listen on wg0.                         │
│  They are invisible from eth0.                              │
│  They don't exist to the outside world.                     │
└─────────────────────────────────────────────────────────────┘

Step 1: Generate keys on every node

# On each machine
umask 077
wg genkey | tee /etc/wireguard/private.key | wg pubkey > /etc/wireguard/public.key

Step 2: Create the backplane config

Node A (10.200.0.1)/etc/wireguard/wg-backplane.conf:

[Interface]
Address = 10.200.0.1/24
ListenPort = 51820
PrivateKey = <node-A-private-key>

# Lock the interface to a specific routing table (optional, advanced)
# Table = 200
# PostUp = ip rule add from 10.200.0.1 table 200

[Peer]
# Node B
PublicKey = <node-B-public-key>
AllowedIPs = 10.200.0.2/32
Endpoint = 203.0.113.20:51820
PersistentKeepalive = 25

[Peer]
# Node C
PublicKey = <node-C-public-key>
AllowedIPs = 10.200.0.3/32
Endpoint = 203.0.113.30:51820
PersistentKeepalive = 25

Node B (10.200.0.2)/etc/wireguard/wg-backplane.conf:

[Interface]
Address = 10.200.0.2/24
ListenPort = 51820
PrivateKey = <node-B-private-key>

[Peer]
# Node A
PublicKey = <node-A-public-key>
AllowedIPs = 10.200.0.1/32
Endpoint = 203.0.113.10:51820
PersistentKeepalive = 25

[Peer]
# Node C
PublicKey = <node-C-public-key>
AllowedIPs = 10.200.0.3/32
Endpoint = 203.0.113.30:51820
PersistentKeepalive = 25

Step 3: Bring it up

systemctl enable --now wg-quick@wg-backplane

Step 4: Bind services to the backplane only

The key step — services only listen on the backplane address, not on eth0:

# SSH — only accessible over the backplane
cat >> /etc/ssh/sshd_config << 'EOF'
ListenAddress 10.200.0.1
EOF
systemctl restart sshd

# PostgreSQL — only accessible over the backplane
# In postgresql.conf:
# listen_addresses = '10.200.0.1'

# Prometheus — only on backplane
# --web.listen-address=10.200.0.1:9090

# nginx — only on backplane (or split: 443 on eth0, management on backplane)
# listen 10.200.0.1:8080;

Step 5: Firewall the physical interface

Now lock down eth0 so only WireGuard UDP passes through:

# CentOS/RHEL (firewalld)
firewall-cmd --permanent --zone=public --remove-service=ssh
firewall-cmd --permanent --zone=public --add-port=51820/udp
firewall-cmd --permanent --zone=trusted --add-interface=wg-backplane
firewall-cmd --reload

# Debian (nftables)
cat > /etc/nftables.d/backplane.nft << 'EOF'
table inet filter {
  chain input {
    # Allow WireGuard UDP on physical interface
    iifname "eth0" udp dport 51820 accept

    # Allow everything on the backplane
    iifname "wg-backplane" accept

    # Drop everything else on eth0
    iifname "eth0" drop
  }
}
EOF
systemctl reload nftables

Result: From the outside, port scans show nothing. Not even SSH. The only thing visible is UDP 51820, which doesn’t respond to unauthenticated traffic. All your services are running, fully functional, accessible only through the encrypted backplane.

Think about what you just did. You moved SSH off the public internet. No more brute force attempts. No more fail2ban. No more watching auth logs fill with garbage from Chinese botnets. The attack surface went from "every service on every port" to "one UDP port that silently drops anything without the right Curve25519 key." This is defense in depth that actually works, and it took five commands. Every kldload server you deploy should do this.

Multiple backplanes (traffic isolation)

One backplane is good. Multiple backplanes separate concerns:

wg-mgmt:     10.200.0.0/24  port 51820  — SSH, Salt, management
wg-data:     10.201.0.0/24  port 51821  — database replication, NFS, storage
wg-monitor:  10.202.0.0/24  port 51822  — Prometheus, Grafana, metrics
wg-app:      10.203.0.0/24  port 51823  — application traffic, API calls

Each plane has its own key pair, its own subnet, its own port. Traffic on one plane can’t leak to another. If the monitoring plane is compromised, the attacker can see metrics but can’t reach the database on the data plane — different keys, different network, different everything.

Generate keys for each plane

for plane in mgmt data monitor app; do
  wg genkey | tee /etc/wireguard/${plane}.key | wg pubkey > /etc/wireguard/${plane}.pub
done
chmod 600 /etc/wireguard/*.key

Create configs for each plane

# Template — repeat for each plane with different addresses/ports
for plane in mgmt:10.200.0:51820 data:10.201.0:51821 monitor:10.202.0:51822 app:10.203.0:51823; do
  IFS=: read name subnet port <<< "$plane"
  cat > /etc/wireguard/wg-${name}.conf << EOF
[Interface]
Address = ${subnet}.1/24
ListenPort = ${port}
PrivateKey = $(cat /etc/wireguard/${name}.key)

# Add peers below
EOF
done

Then add [Peer] blocks for each node on each plane. Each peer needs a unique key pair per plane — don’t reuse keys across planes.

Bring all planes up

for plane in mgmt data monitor app; do
  systemctl enable --now wg-quick@wg-${plane}
done

Bind services to specific planes

# SSH only on management plane
ListenAddress 10.200.0.1        # sshd_config

# PostgreSQL only on data plane
listen_addresses = '10.201.0.1'  # postgresql.conf

# Prometheus only on monitor plane
--web.listen-address=10.202.0.1:9090

# Your app only on app plane
APP_BIND=10.203.0.1:8080

Now each service is only reachable on its designated plane. An attacker who somehow gets access to the monitoring network can’t reach the database, can’t SSH to anything, can’t touch the application.

This is network microsegmentation — the thing that Cisco sells for six figures with ACI, that VMware sells with NSX, that every compliance framework asks for. You just built it with four WireGuard interfaces per node. Each plane has its own key pair, so compromising one plane’s keys doesn’t give you access to any other plane. Each plane has its own subnet, so even if an attacker gets onto one network, there’s no route to the others. This is what "zero trust networking" actually looks like in practice — not a product, not a checkbox, just separate encrypted networks with separate keys and separate routes. And on kldload it’s four config files and four systemctl commands.

Full mesh vs hub-and-spoke

Hub-and-spoke

All traffic goes through a central hub. Simpler to configure (peers only need the hub’s endpoint), but the hub is a bottleneck and single point of failure.

     ┌─── Node B
     │
Hub ─┼─── Node C
     │
     └─── Node D

Good for: remote access, home labs, small deployments, road warriors.

Full mesh

Every node connects directly to every other node. No bottleneck, no single point of failure, but O(n²) peer configurations.

Node A ─── Node B
  │  ╲      │
  │   ╲     │
  │    ╲    │
Node C ─── Node D

Good for: clusters, production infrastructure, low-latency requirements.

How to decide: Hub-and-spoke if you have remote users or branch offices connecting to a central site. Full mesh if you have servers that need to talk to each other directly — database replication, ZFS send/receive, Kubernetes pod traffic, Prometheus scraping. The O(n²) peer problem in full mesh is real: 10 nodes = 90 peer entries, 50 nodes = 2,450. That's why the mesh generator script below exists. Past ~20 nodes, look at a coordination service (like WireGuard + Consul, or Tailscale's control plane running over your own mesh) to automate peer distribution. But for a 4-16 node cluster? Full mesh with the generator script is the right answer.

Full mesh configuration

For a 4-node full mesh, each node needs 3 peer blocks:

#!/bin/bash
# generate-mesh.sh — generate full mesh WireGuard configs
# Usage: ./generate-mesh.sh

declare -A NODES=(
  [node-a]="203.0.113.10:10.200.0.1"
  [node-b]="203.0.113.20:10.200.0.2"
  [node-c]="203.0.113.30:10.200.0.3"
  [node-d]="203.0.113.40:10.200.0.4"
)

PORT=51820

# Generate keys
for name in "${!NODES[@]}"; do
  wg genkey | tee "keys/${name}.key" | wg pubkey > "keys/${name}.pub"
done

# Generate configs
for name in "${!NODES[@]}"; do
  IFS=: read pub_ip wg_ip <<< "${NODES[$name]}"

  cat > "configs/${name}.conf" << CONF
[Interface]
Address = ${wg_ip}/24
ListenPort = ${PORT}
PrivateKey = $(cat keys/${name}.key)
CONF

  for peer in "${!NODES[@]}"; do
    [[ "$peer" == "$name" ]] && continue
    IFS=: read peer_pub peer_wg <<< "${NODES[$peer]}"

    cat >> "configs/${name}.conf" << CONF

[Peer]
# ${peer}
PublicKey = $(cat keys/${peer}.pub)
AllowedIPs = ${peer_wg}/32
Endpoint = ${peer_pub}:${PORT}
PersistentKeepalive = 25
CONF
  done
done

echo "Configs generated in configs/"
echo "Distribute each .conf to its respective node at /etc/wireguard/wg0.conf"
mkdir -p keys configs
chmod 700 keys
./generate-mesh.sh

Dynamic mesh with wg-quick and PostUp

For larger meshes, use a script to add peers dynamically:

[Interface]
Address = 10.200.0.1/24
ListenPort = 51820
PrivateKey = <key>
PostUp = /etc/wireguard/add-mesh-peers.sh %i
PostDown = /etc/wireguard/remove-mesh-peers.sh %i

NAT traversal and peers behind firewalls

WireGuard handles NAT naturally with PersistentKeepalive. But there are edge cases:

Both peers behind NAT

If neither peer has a public IP, neither can set an Endpoint for the other. Solutions:

  1. Use a relay node — one node with a public IP acts as a hub. Both NAT’d peers connect to it, and it forwards traffic.

  2. Use a STUN/TURN approach — not built into WireGuard, but you can use a coordination service to exchange endpoints.

  3. Use a cloud relay — spin up a tiny VM (t2.micro, free tier) as a WireGuard hub. Both peers connect to it.

# Tiny relay VM config
[Interface]
Address = 10.200.0.1/24
ListenPort = 51820
PrivateKey = <relay-key>
PostUp = sysctl -w net.ipv4.ip_forward=1

[Peer]
# Home lab (behind NAT)
PublicKey = <home-pub>
AllowedIPs = 10.200.0.2/32
# No Endpoint — the home lab connects to us

[Peer]
# Office (behind NAT)
PublicKey = <office-pub>
AllowedIPs = 10.200.0.3/32
# No Endpoint — the office connects to us

Both NAT’d peers set the relay as their Endpoint. The relay forwards traffic between them. They can reach each other via 10.200.0.x addresses through the relay.

The cloud relay trick is the cheapest possible way to connect two networks that are both behind NAT. A t2.micro or equivalent costs nothing (free tier) and handles WireGuard relay for dozens of peers — WireGuard’s CPU overhead is negligible. The relay doesn’t see your traffic in the clear (it’s encrypted end-to-end between the actual peers), it just forwards encrypted UDP packets. You can even run multiple relays in different regions for redundancy. This is essentially what Tailscale’s DERP servers do, but you own it.

Changing ports to avoid corporate firewalls

Some networks block non-standard UDP ports. Use 443 or 53 instead:

[Interface]
ListenPort = 443   # looks like HTTPS to basic firewalls

Or run WireGuard on port 53:

[Interface]
ListenPort = 53    # looks like DNS

Site-to-site: connecting entire networks

Connect two LANs so all devices on both sides can talk to each other:

Site A — LAN 192.168.1.0/24

[Interface]
Address = 10.200.0.1/24
ListenPort = 51820
PrivateKey = <site-a-key>
PostUp = sysctl -w net.ipv4.ip_forward=1

[Peer]
PublicKey = <site-b-key>
AllowedIPs = 10.200.0.2/32, 192.168.2.0/24
Endpoint = <site-b-public-ip>:51820
PersistentKeepalive = 25

Site B — LAN 192.168.2.0/24

[Interface]
Address = 10.200.0.2/24
ListenPort = 51820
PrivateKey = <site-b-key>
PostUp = sysctl -w net.ipv4.ip_forward=1

[Peer]
PublicKey = <site-a-key>
AllowedIPs = 10.200.0.1/32, 192.168.1.0/24
Endpoint = <site-a-public-ip>:51820
PersistentKeepalive = 25

The AllowedIPs field includes the remote LAN subnet. WireGuard routes packets for those subnets through the tunnel. Devices on LAN A (192.168.1.x) can reach devices on LAN B (192.168.2.x) transparently — no VPN client needed on individual devices.

This is a site-to-site VPN that replaces commercial SD-WAN products. Two WireGuard configs, two systemctl commands, and your entire LANs can talk to each other encrypted over the internet. The key insight is the AllowedIPs field — it’s not just an ACL, it’s a routing directive. "AllowedIPs = 10.200.0.2/32, 192.168.2.0/24" means "route the peer’s WG address AND their entire LAN through this tunnel." That one line replaces a routing table entry, a firewall rule, and an authentication check. Devices on either LAN don’t need any VPN client — the WireGuard gateway handles everything transparently.

Add routes on each site’s router pointing the remote subnet to the WireGuard gateway:

# On Site A's router (or the WireGuard host if it IS the router)
ip route add 192.168.2.0/24 via 10.200.0.1 dev wg0

Split tunnel vs full tunnel

Split tunnel (default)

Only traffic destined for the WireGuard subnet goes through the tunnel. Everything else goes out the normal internet connection.

[Peer]
AllowedIPs = 10.200.0.0/24    # only backplane traffic tunneled

Full tunnel

ALL traffic goes through the tunnel. Your exit IP becomes the remote peer’s IP. Useful for privacy, bypassing geo-restrictions, or routing all traffic through a trusted exit point.

[Peer]
AllowedIPs = 0.0.0.0/0, ::/0  # everything tunneled

When using full tunnel, set DNS on the interface:

[Interface]
Address = 10.200.0.2/24
PrivateKey = <key>
DNS = 1.1.1.1, 9.9.9.9
AllowedIPs is the most misunderstood field in WireGuard. It's not a firewall rule. It's not an ACL. It is simultaneously three things: (1) a routing directive — "send packets for these IPs through this peer," (2) a receive filter — "accept packets from this peer only if they claim to come from these IPs," and (3) a reverse path check — packets from IPs not in AllowedIPs get dropped. 0.0.0.0/0 means "route everything through this peer" — full tunnel. 10.200.0.2/32 means "only route this one IP." 10.200.0.0/24, 192.168.2.0/24 means "route the WG subnet and the remote LAN." Get this right and everything works. Get it wrong and you'll have asymmetric routing, packet drops, and hours of debugging.

Split tunnel with specific routes

Route only certain subnets through the tunnel:

[Peer]
AllowedIPs = 10.200.0.0/24, 10.0.0.0/8, 172.16.0.0/12
# Only RFC1918 private traffic goes through the tunnel
# Public internet traffic goes out normally

Stealth configuration

Maximum invisibility. The goal: nothing about this machine reveals that it’s part of a private network.

1. No listening port on the initiator

If a node only initiates connections (never receives them), it doesn’t need a ListenPort:

[Interface]
Address = 10.200.0.5/24
PrivateKey = <key>
# No ListenPort — uses a random ephemeral port
# No UDP port visible in port scans

[Peer]
PublicKey = <hub-key>
Endpoint = <hub-ip>:51820
AllowedIPs = 10.200.0.0/24
PersistentKeepalive = 25

The node connects outbound to the hub. The hub can reach it back through the established tunnel. But no port is open on this machine — nothing to scan, nothing to find.

2. Non-standard port on the hub

ListenPort = 8172   # or any random high port

3. Firewall everything except WireGuard

# Only allow WireGuard UDP, drop everything else
# CentOS
firewall-cmd --permanent --zone=drop --change-interface=eth0
firewall-cmd --permanent --zone=drop --add-port=51820/udp
firewall-cmd --permanent --zone=trusted --add-interface=wg-backplane
firewall-cmd --reload

The machine now has exactly one open port on its public interface: WireGuard. And WireGuard doesn’t respond to unauthenticated traffic. To the outside world, this machine appears to have no open ports at all.

4. No DNS, no hostname leaks

# Set hostname to something generic
hostnamectl set-hostname localhost

# Don't publish mDNS
systemctl disable --now avahi-daemon 2>/dev/null || true

5. Verify stealth

From another machine, scan the target:

nmap -sU -sT -p- 203.0.113.10

Expected result: all ports filtered or closed. No services detected. The machine appears to be off or non-existent, but it’s fully operational on the backplane.

You just built a machine that is invisible on the network but fully functional to authorized peers. This isn’t theoretical — this is how high-security infrastructure actually works. Military networks, financial trading systems, classified government systems — they all use this pattern: no public services, no discoverable ports, authentication happens at the transport layer before any application code runs. The difference is they use expensive proprietary hardware. You used a kernel module and a text file. The security properties are identical.

Monitoring WireGuard

Basic status

wg show

Shows: interfaces, public keys, endpoints, allowed IPs, latest handshake, transfer bytes.

Watch for problems

# Continuous monitoring
watch -n5 wg show

# Check if a specific peer has a recent handshake (within 3 minutes)
wg show wg0 latest-handshakes | while read pub ts; do
  age=$(( $(date +%s) - ts ))
  if (( age > 180 )); then
    echo "STALE: peer ${pub:0:8}... last handshake ${age}s ago"
  fi
done
wg show is your first diagnostic tool, every time. Latest handshake tells you if a peer is alive (handshakes happen every 2 minutes on active tunnels). Transfer bytes tell you if traffic is flowing. If the handshake timestamp is more than 3 minutes old and there should be traffic, something is wrong — usually a firewall blocking UDP, a changed endpoint IP, or a key mismatch. The most common failure mode: someone rotated keys on one side but not the other. WireGuard doesn't log errors for bad keys — it just silently drops packets. Check the handshake timestamp first, always.

Prometheus metrics

Use the wireguard_exporter or scrape wg show output:

cat > /usr/local/bin/wg-metrics.sh << 'SCRIPT'
#!/bin/bash
# Textfile exporter for node_exporter
echo "# HELP wireguard_peers Number of WireGuard peers"
echo "# TYPE wireguard_peers gauge"
for iface in $(wg show interfaces); do
  count=$(wg show "$iface" peers | wc -l)
  echo "wireguard_peers{interface=\"${iface}\"} ${count}"
done

echo "# HELP wireguard_transfer_bytes Bytes transferred per peer"
echo "# TYPE wireguard_transfer_bytes gauge"
wg show all transfer | while read iface pub rx tx; do
  short="${pub:0:8}"
  echo "wireguard_transfer_rx_bytes{interface=\"${iface}\",peer=\"${short}\"} ${rx}"
  echo "wireguard_transfer_tx_bytes{interface=\"${iface}\",peer=\"${short}\"} ${tx}"
done

echo "# HELP wireguard_latest_handshake_seconds Seconds since last handshake"
echo "# TYPE wireguard_latest_handshake_seconds gauge"
wg show all latest-handshakes | while read iface pub ts; do
  short="${pub:0:8}"
  age=$(( $(date +%s) - ts ))
  echo "wireguard_latest_handshake_seconds{interface=\"${iface}\",peer=\"${short}\"} ${age}"
done
SCRIPT
chmod +x /usr/local/bin/wg-metrics.sh

# Run every 30s via cron or systemd timer
mkdir -p /var/lib/node_exporter/textfile
echo '* * * * * root /usr/local/bin/wg-metrics.sh > /var/lib/node_exporter/textfile/wireguard.prom' >> /etc/crontab

eBPF tracing of WireGuard

Use the kldload eBPF tools to trace WireGuard at the kernel level:

# Count packets per WireGuard interface
bpftrace -e 'tracepoint:net:net_dev_xmit /str(args.name) == "wg0"/ { @packets = count(); }'

# Histogram of packet sizes on the backplane
bpftrace -e 'tracepoint:net:net_dev_xmit /str(args.name) == "wg-backplane"/ { @size = hist(args.len); }'

# Watch for WireGuard handshakes (new connections)
bpftrace -e 'kprobe:wg_packet_receive { printf("WG packet received: pid=%d comm=%s\n", pid, comm); }'

Key management best practices

Rotate keys periodically

# Generate new keys
wg genkey | tee /etc/wireguard/private.key.new | wg pubkey > /etc/wireguard/public.key.new

# Distribute the new public key to all peers
# Update the config
# Atomically swap:
wg-quick down wg-backplane
mv /etc/wireguard/private.key.new /etc/wireguard/private.key
wg-quick up wg-backplane

Pre-shared keys (PSK) for post-quantum protection

WireGuard supports a pre-shared key per peer that adds a symmetric encryption layer on top of the Curve25519 key exchange. If quantum computers break Curve25519, the PSK still protects the traffic:

# Generate a PSK
wg genpsk > /etc/wireguard/psk-node-b.key

# Add to the peer block
[Peer]
PublicKey = <node-B-public-key>
PresharedKey = <contents of psk-node-b.key>
AllowedIPs = 10.200.0.2/32
Endpoint = 203.0.113.20:51820

Both sides must have the same PSK. Distribute it out-of-band (USB stick, in-person, encrypted email — never over the same WireGuard tunnel you’re securing with it).

Pre-shared keys are the cheapest insurance policy in cryptography. WireGuard’s Curve25519 key exchange is considered safe today, but quantum computers could break it in the future. A 256-bit PSK adds symmetric encryption that no quantum computer can break (Grover’s algorithm only halves the effective key length — 128 bits of security remains). The cost: one wg genpsk command and one extra line per peer block. The benefit: your traffic stays encrypted even if Curve25519 is broken twenty years from now. For any tunnel carrying data with long-term sensitivity — financial records, medical data, infrastructure credentials — there’s no reason not to add it. kldload’s kwg tool generates PSKs by default.

ZFS and key backups

On kldload, /etc/wireguard/ is on ZFS. Your keys are included in ZFS snapshots. This is both good (rollback restores keys) and something to be aware of (snapshots contain your private keys). If you replicate snapshots offsite, those replicas contain your private keys.

# Back up keys separately (encrypted)
tar czf - /etc/wireguard/*.key | gpg -c > wireguard-keys-$(hostname)-$(date +%Y%m%d).tar.gz.gpg

Quick reference

I want to… Command
Generate a key pair wg genkey \| tee priv.key \| wg pubkey > pub.key
Generate a pre-shared key wg genpsk > psk.key
Start an interface wg-quick up wg0
Stop an interface wg-quick down wg0
Enable at boot systemctl enable wg-quick@wg0
Show all interfaces wg show
Show specific interface wg show wg0
Add a peer live (no restart) wg set wg0 peer <pubkey> allowed-ips 10.200.0.5/32 endpoint 1.2.3.4:51820
Remove a peer live wg set wg0 peer <pubkey> remove
Show latest handshakes wg show wg0 latest-handshakes
Show transfer stats wg show wg0 transfer
Dump current config wg showconf wg0
Save running config to file wg showconf wg0 > /etc/wireguard/wg0.conf

Topologies at a glance

Topology Peers per node Use case
Point-to-point 1 Two servers, site-to-site
Hub-and-spoke 1 (clients), N (hub) Remote access, road warriors
Full mesh N-1 Clusters, low-latency
Multi-plane N-1 per plane Traffic isolation, defense in depth
Relay 1 per NAT’d node Peers behind NAT

See also