tutorial

How to Monitor WireGuard VPN Tunnels and Services with Vigilmon

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 l...

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 show only 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 show shows 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:

  1. Monitors → New Monitor → Heartbeat
  2. Name: WireGuard Tunnel Health
  3. Interval: 5 minutes
  4. 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):

  1. In Vigilmon, add a TCP Port monitor
  2. Hostname: your server's public IP or domain
  3. Port: 22 (SSH) — verifies the machine is up
  4. 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:

  1. Monitors → New Monitor → Heartbeat
  2. Name: WireGuard Key Rotation
  3. Interval: 8 days (fires alert if weekly rotation is missed)

Step 6: Configure alerting

Route WireGuard alerts to your networking team:

  1. Alert Channels → Add Channel → Slack — use your #networking or #security-ops channel
  2. Add Channel → Email — on-call distribution list
  3. 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:

  1. Status Pages → New Status Page
  2. Name: VPN Infrastructure
  3. Add monitors: Server Reachability, Tunnel Health, Service Probe, Key Rotation
  4. 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. Run wg set wg0 peer <pubkey> endpoint <new-ip>:51820 to update it.
  • Tunnel connectivity probe downping 10.0.0.2 from the server; if it fails, check wg show wg0 transfer for 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> and journalctl -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 transfer in 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.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →