WireGuard is fast, lean, and cryptographically elegant — but it's also deliberately minimal. There's no built-in health check, no status API, and no daemon logs of consequence. When a WireGuard tunnel silently fails, you find out when something stops working, not from the VPN layer itself.
This tutorial shows you how to build complete observability for WireGuard deployments using Vigilmon: monitoring tunnel connectivity, the services running over the VPN, and the periodic jobs that keep your tunnels healthy.
Why WireGuard needs external monitoring
WireGuard's minimalism is also its blind spot:
- Silent handshake failure — WireGuard only initiates a handshake when there's traffic. An idle tunnel might look "up" at the kernel level (the interface exists, the peer is configured) while being completely unable to pass traffic — because the remote peer's key changed, the UDP port is firewalled, or the peer's public IP roamed.
- No daemon, no logs — WireGuard runs in the kernel. There's no daemon process to check, no log file to tail, and
wg showonly tells you when the last handshake was. - IP roaming without reconnect — WireGuard peers that roam between networks update their endpoint automatically, but this requires a successful handshake. If the roaming device is behind a strict NAT, the handshake might never complete.
- Peer key rotation — if you rotate WireGuard keys (a security best practice), every peer must be updated before the old key expires. A missed update silently drops a peer from the tunnel.
- Services vs. tunnel — even a healthy tunnel doesn't mean your services are running. A database or API on the WireGuard interface could be down while
wg showshows a recent handshake.
Monitoring WireGuard means checking the things WireGuard itself won't tell you.
What you'll need
- A WireGuard server and at least one client peer
- Services accessible over the WireGuard interface
- A free Vigilmon account
Step 1: Expose a health endpoint on services over WireGuard
Every service reachable over your WireGuard tunnel should expose a /health route:
Python / FastAPI:
from fastapi import FastAPI
app = FastAPI()
@app.get("/health")
async def health_check():
return {"status": "ok"}
Go:
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"status":"ok"}`)
})
// Bind to WireGuard interface IP
http.ListenAndServe("10.0.0.1:8080", nil)
}
Nginx reverse proxy (wraps any backend):
server {
listen 10.0.0.1:80; # WireGuard interface IP
location /health {
access_log off;
return 200 '{"status":"ok"}';
add_header Content-Type application/json;
}
location / {
proxy_pass http://127.0.0.1:3000;
}
}
Binding to the WireGuard IP (e.g., 10.0.0.1) means the health endpoint is only reachable from peers inside the tunnel — keeping it off the public internet.
Step 2: Deploy a Vigilmon probe inside the WireGuard network
Since Vigilmon's global probes can't reach WireGuard-only endpoints, run a probe script on your WireGuard server. This script checks services on the tunnel and signals Vigilmon via a heartbeat:
#!/bin/bash
# wg-service-probe.sh
HEARTBEAT_URL="https://vigilmon.online/api/heartbeat/YOUR_HEARTBEAT_ID"
# Services to check — use WireGuard IPs
SERVICES=(
"http://10.0.0.1:8080/health"
"http://10.0.0.2:8080/health"
"http://10.0.0.3:3000/health"
)
all_ok=true
for svc in "${SERVICES[@]}"; do
http_code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "$svc")
if [ "$http_code" != "200" ]; then
echo "$(date) FAIL: $svc returned HTTP $http_code" >> /var/log/wg-probe.log
all_ok=false
fi
done
if $all_ok; then
curl -s "$HEARTBEAT_URL" --max-time 10 --retry 2
fi
Add to crontab:
* * * * * /opt/scripts/wg-service-probe.sh
In Vigilmon, create a Heartbeat monitor at 5-minute interval. If the probe fails to ping — either because a service is down or because the WireGuard server itself has a problem — Vigilmon alerts.
Step 3: Monitor WireGuard tunnel connectivity with handshake checks
WireGuard's wg show command outputs the last handshake time for each peer. A handshake older than ~3 minutes means the tunnel is stale. Use this to drive a heartbeat:
#!/bin/bash
# wg-tunnel-check.sh
# Checks that all configured peers had a handshake within the last 3 minutes
HEARTBEAT_URL="https://vigilmon.online/api/heartbeat/YOUR_TUNNEL_HEARTBEAT"
MAX_AGE_SECONDS=180 # 3 minutes
tunnel_ok=true
# Get peer public keys
peers=$(wg show wg0 peers 2>/dev/null)
while IFS= read -r peer; do
[ -z "$peer" ] && continue
last_handshake=$(wg show wg0 latest-handshakes | grep "^$peer" | awk '{print $2}')
if [ -z "$last_handshake" ] || [ "$last_handshake" = "0" ]; then
echo "$(date): Peer $peer — no handshake yet"
tunnel_ok=false
continue
fi
now=$(date +%s)
age=$((now - last_handshake))
if [ "$age" -gt "$MAX_AGE_SECONDS" ]; then
echo "$(date): Peer $peer — last handshake ${age}s ago (stale)"
tunnel_ok=false
fi
done <<< "$peers"
if $tunnel_ok; then
curl -s "$HEARTBEAT_URL" --max-time 10
fi
Set up in Vigilmon:
- Monitors → New Monitor → Heartbeat
- Name:
WireGuard Tunnel Health - Interval: 5 minutes
- Add to crontab:
*/5 * * * * /opt/scripts/wg-tunnel-check.sh
Step 4: Monitor the WireGuard server itself from the outside
Even if you can't check the tunnel from outside, you can verify the WireGuard UDP port is open and accepting handshakes, and that the machine running WireGuard is reachable:
SSH port check (TCP):
- In Vigilmon, add a TCP Port monitor
- Hostname: your server's public IP or domain
- Port:
22(SSH) — verifies the machine is up - Name it
WireGuard Server SSH
WireGuard UDP port check: WireGuard listens on UDP, and Vigilmon's TCP port checks don't cover UDP. Use a heartbeat from inside the server instead — if the server is unreachable (DDoS, OOM kill, provider issue), the heartbeat will stop pinging and Vigilmon fires the alert.
Alternatively, use a public HTTP endpoint on the same machine as a proxy for machine availability:
server {
listen 80;
server_name vpn-server.example.com;
location /ping {
return 200 'ok';
add_header Content-Type text/plain;
}
}
Add an HTTP monitor in Vigilmon:
- URL:
http://vpn-server.example.com/ping - Expected status:
200 - Name:
WireGuard Server Reachability
Step 5: Monitor key rotation and peer management jobs
If you rotate WireGuard keys automatically (or run scripts to add/remove peers), use Vigilmon heartbeats to verify these jobs run on schedule.
Example for a key rotation script:
#!/bin/bash
# wg-key-rotation.sh — run weekly
HEARTBEAT_URL="https://vigilmon.online/api/heartbeat/YOUR_KEY_ROTATION_HEARTBEAT"
# Generate new keypair
new_privkey=$(wg genkey)
new_pubkey=$(echo "$new_privkey" | wg pubkey)
# Update server config
sed -i "s|^PrivateKey = .*|PrivateKey = $new_privkey|" /etc/wireguard/wg0.conf
# Restart interface to apply new key
wg-quick down wg0 && wg-quick up wg0
if [ $? -eq 0 ]; then
echo "Key rotated. New public key: $new_pubkey"
curl -s "$HEARTBEAT_URL" --max-time 10
else
echo "Key rotation FAILED" >&2
fi
Set up the heartbeat:
- Monitors → New Monitor → Heartbeat
- Name:
WireGuard Key Rotation - Interval: 8 days (fires alert if weekly rotation is missed)
Step 6: Configure alerting
Route WireGuard alerts to your networking team:
- Alert Channels → Add Channel → Slack — use your
#networkingor#security-opschannel - Add Channel → Email — on-call distribution list
- Assign all WireGuard monitors to both channels
For tunnel staleness (handshake check) and server reachability, consider immediate (0-minute) escalation — a stale WireGuard tunnel means all peers in your VPN are affected.
Step 7: Build a VPN infrastructure status page
Give your team visibility into VPN health without making them ask:
- Status Pages → New Status Page
- Name:
VPN Infrastructure - Add monitors: Server Reachability, Tunnel Health, Service Probe, Key Rotation
- Publish and post the URL in
#infra
Complete monitoring reference
| Monitor | Type | What it catches | |---------|------|-----------------| | Server SSH | TCP | Machine down, provider issue | | Server HTTP ping | HTTP | Web layer reachable | | Tunnel handshake check | Heartbeat | Stale peers, NAT traversal failure | | Service probe | Heartbeat | Service down over WireGuard | | Key rotation job | Heartbeat | Missed rotation, script failure |
Troubleshooting common failures
- Stale handshake (>3 min) — on the server, run
wg show wg0; if a peer shows(none)for endpoint, the peer's IP changed. Runwg set wg0 peer <pubkey> endpoint <new-ip>:51820to update it. - Tunnel connectivity probe down —
ping 10.0.0.2from the server; if it fails, checkwg show wg0 transferfor the specific peer — zero sent/received usually means UDP is blocked. - Service probe heartbeat missed but tunnel is fine — the service process crashed. Check
systemctl status <service>andjournalctl -u <service> -n 50. - Key rotation heartbeat missed — run the script manually and check for permission errors on
/etc/wireguard/wg0.conf.
Next steps
- SSL monitoring — if any WireGuard-exposed services are proxied over HTTPS, add SSL expiry monitors in Vigilmon
- Bandwidth alerting — parse
wg show wg0 transferin your probe script; if a peer's traffic drops to zero for an extended period, log it as a connectivity event
Start monitoring your WireGuard infrastructure today at vigilmon.online — free, no credit card.