From ecdc27b155363bb98bb2f5c665e2a14ef48490d0 Mon Sep 17 00:00:00 2001 From: Moritz Graf Date: Mon, 6 Jul 2026 22:09:37 +0200 Subject: [PATCH] feat(wireguard): add prometheus monitoring, alerts, and grafana dashboard --- .../grafana-dashboard-wireguard.yaml | 224 ++++++++++++++++++ k8s/wireguard/deployment.yaml | 22 ++ k8s/wireguard/kustomization.yaml | 1 + k8s/wireguard/monitoring.yaml | 71 ++++++ k8s/wireguard/secret.secret.yaml | Bin 412 -> 443 bytes 5 files changed, 318 insertions(+) create mode 100644 k8s/monitoring/dashboards/grafana-dashboard-wireguard.yaml create mode 100644 k8s/wireguard/monitoring.yaml diff --git a/k8s/monitoring/dashboards/grafana-dashboard-wireguard.yaml b/k8s/monitoring/dashboards/grafana-dashboard-wireguard.yaml new file mode 100644 index 0000000..e6d7191 --- /dev/null +++ b/k8s/monitoring/dashboards/grafana-dashboard-wireguard.yaml @@ -0,0 +1,224 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: grafana-dashboard-wireguard + namespace: monitoring + labels: + grafana_dashboard: '1' + annotations: + grafana_dashboard_folder: 🏠 Home +data: + wireguard.json: | + { + "uid": "wireguard", + "title": "🛡️ WireGuard VPN", + "tags": ["network", "vpn", "infrastructure", "prometheus"], + "timezone": "browser", + "schemaVersion": 39, + "version": 2, + "refresh": "10s", + "editable": false, + "graphTooltip": 2, + "time": { + "from": "now-24h", + "to": "now" + }, + "panels": [ + { + "title": "⚡ VPN Status", + "type": "row", + "gridPos": {"h": 1, "w": 24, "x": 0, "y": 0}, + "collapsed": false + }, + { + "title": "Exporter Status", + "type": "stat", + "datasource": {"type": "prometheus", "uid": "prometheus"}, + "gridPos": {"h": 4, "w": 6, "x": 0, "y": 1}, + "fieldConfig": { + "defaults": { + "mappings": [ + {"type": "value", "options": {"0": {"text": "OFFLINE", "color": "red"}, "1": {"text": "ONLINE", "color": "green"}}} + ], + "color": {"mode": "thresholds"} + } + }, + "options": { + "graphMode": "none", + "colorMode": "background", + "textMode": "auto", + "reduceOptions": {"values": false, "calcs": ["lastNotNull"]} + }, + "targets": [ + {"expr": "up{job=\"wireguard-exporter\"}", "legendFormat": "Exporter Connectivity", "refId": "A"} + ] + }, + { + "title": "Interface wg0", + "type": "stat", + "datasource": {"type": "prometheus", "uid": "prometheus"}, + "gridPos": {"h": 4, "w": 6, "x": 6, "y": 1}, + "fieldConfig": { + "defaults": { + "mappings": [ + {"type": "value", "options": {"0": {"text": "DOWN", "color": "red"}, "1": {"text": "UP", "color": "green"}}} + ], + "color": {"mode": "thresholds"} + } + }, + "options": { + "graphMode": "none", + "colorMode": "background", + "textMode": "auto", + "reduceOptions": {"values": false, "calcs": ["lastNotNull"]} + }, + "targets": [ + {"expr": "count(wireguard_sent_bytes_total{interface=\"wg0\"}) or on() vector(0)", "legendFormat": "wg0 Status", "refId": "A"} + ] + }, + { + "title": "FritzBox Peer Status", + "type": "stat", + "datasource": {"type": "prometheus", "uid": "prometheus"}, + "gridPos": {"h": 4, "w": 6, "x": 12, "y": 1}, + "fieldConfig": { + "defaults": { + "mappings": [ + {"type": "value", "options": {"0": {"text": "DISCONNECTED", "color": "red"}, "1": {"text": "CONNECTED", "color": "green"}}} + ], + "color": {"mode": "thresholds"} + } + }, + "options": { + "graphMode": "none", + "colorMode": "background", + "textMode": "auto", + "reduceOptions": {"values": false, "calcs": ["lastNotNull"]} + }, + "targets": [ + {"expr": "(time() - wireguard_latest_handshake_seconds{interface=\"wg0\"} < 300) or on() vector(0)", "legendFormat": "Peer Connectivity", "refId": "A"} + ] + }, + { + "title": "FritzBox Last Handshake", + "type": "stat", + "datasource": {"type": "prometheus", "uid": "prometheus"}, + "gridPos": {"h": 4, "w": 6, "x": 18, "y": 1}, + "fieldConfig": { + "defaults": { + "unit": "s", + "color": {"mode": "thresholds"}, + "thresholds": { + "mode": "absolute", + "steps": [ + {"color": "green", "value": null}, + {"color": "orange", "value": 300}, + {"color": "red", "value": 600} + ] + } + } + }, + "options": { + "graphMode": "none", + "colorMode": "value", + "textMode": "auto", + "reduceOptions": {"values": false, "calcs": ["lastNotNull"]} + }, + "targets": [ + {"expr": "time() - wireguard_latest_handshake_seconds{interface=\"wg0\"}", "legendFormat": "Handshake Delay", "refId": "A"} + ] + }, + { + "title": "📊 Traffic & Performance", + "type": "row", + "gridPos": {"h": 1, "w": 24, "x": 0, "y": 5}, + "collapsed": false + }, + { + "title": "FritzBox Traffic Rate", + "type": "timeseries", + "datasource": {"type": "prometheus", "uid": "prometheus"}, + "gridPos": {"h": 10, "w": 24, "x": 0, "y": 6}, + "fieldConfig": { + "defaults": { + "unit": "Bps", + "custom": { + "lineWidth": 2, + "fillOpacity": 10, + "spanNulls": true, + "showPoints": "never" + } + }, + "overrides": [ + { + "matcher": {"id": "byName", "options": "Received (Rx)"}, + "properties": [ + {"id": "color", "value": {"fixedColor": "#4CAF50", "mode": "fixed"}} + ] + }, + { + "matcher": {"id": "byName", "options": "Sent (Tx)"}, + "properties": [ + {"id": "color", "value": {"fixedColor": "#2196F3", "mode": "fixed"}} + ] + } + ] + }, + "options": { + "legend": {"displayMode": "table", "placement": "bottom", "calcs": ["lastNotNull", "mean", "max"]}, + "tooltip": {"mode": "multi", "sort": "desc"} + }, + "targets": [ + {"expr": "rate(wireguard_received_bytes_total{interface=\"wg0\"}[5m])", "legendFormat": "Received (Rx)", "refId": "A"}, + {"expr": "rate(wireguard_sent_bytes_total{interface=\"wg0\"}[5m])", "legendFormat": "Sent (Tx)", "refId": "B"} + ] + }, + { + "title": "Total Data Transferred", + "type": "stat", + "datasource": {"type": "prometheus", "uid": "prometheus"}, + "gridPos": {"h": 5, "w": 12, "x": 0, "y": 16}, + "fieldConfig": { + "defaults": { + "unit": "bytes", + "color": {"mode": "palette-classic"} + } + }, + "options": { + "graphMode": "none", + "colorMode": "value", + "textMode": "auto", + "reduceOptions": {"values": true, "calcs": ["lastNotNull"]} + }, + "targets": [ + {"expr": "wireguard_received_bytes_total{interface=\"wg0\"}", "legendFormat": "Total Received (Rx)", "refId": "A"}, + {"expr": "wireguard_sent_bytes_total{interface=\"wg0\"}", "legendFormat": "Total Sent (Tx)", "refId": "B"} + ] + }, + { + "title": "Handshake Delay History", + "type": "timeseries", + "datasource": {"type": "prometheus", "uid": "prometheus"}, + "gridPos": {"h": 5, "w": 12, "x": 12, "y": 16}, + "fieldConfig": { + "defaults": { + "unit": "s", + "custom": { + "lineWidth": 2, + "fillOpacity": 5, + "spanNulls": true, + "showPoints": "never" + }, + "color": {"mode": "fixed", "fixedColor": "#FF9800"} + } + }, + "options": { + "legend": {"displayMode": "list", "placement": "bottom", "calcs": ["lastNotNull", "max"]}, + "tooltip": {"mode": "single"} + }, + "targets": [ + {"expr": "time() - wireguard_latest_handshake_seconds{interface=\"wg0\"}", "legendFormat": "Handshake Delay", "refId": "A"} + ] + } + ] + } diff --git a/k8s/wireguard/deployment.yaml b/k8s/wireguard/deployment.yaml index df821dc..b43b8e7 100644 --- a/k8s/wireguard/deployment.yaml +++ b/k8s/wireguard/deployment.yaml @@ -7,6 +7,8 @@ metadata: app: wireguard spec: replicas: 1 + strategy: + type: Recreate selector: matchLabels: app: wireguard @@ -25,6 +27,7 @@ spec: apk add --no-cache wireguard-tools iptables cp /config/wg0.conf /etc/wireguard/wg0.conf chmod 600 /etc/wireguard/wg0.conf + wg-quick down wg0 || true wg-quick up wg0 echo "Wireguard is up" trap "wg-quick down wg0" SIGINT SIGTERM @@ -44,6 +47,25 @@ spec: - name: lib-modules mountPath: /lib/modules readOnly: true + - name: exporter + image: mindflavor/prometheus-wireguard-exporter:latest + args: + - "-n" + - "/config/wg0.conf" + ports: + - name: http-metrics + containerPort: 9586 + protocol: TCP + securityContext: + runAsUser: 0 + capabilities: + add: + - NET_ADMIN + volumeMounts: + - name: wg-config + mountPath: /config/wg0.conf + subPath: wg0.conf + readOnly: true volumes: - name: wg-config secret: diff --git a/k8s/wireguard/kustomization.yaml b/k8s/wireguard/kustomization.yaml index 3b9db94..4e1af92 100644 --- a/k8s/wireguard/kustomization.yaml +++ b/k8s/wireguard/kustomization.yaml @@ -7,3 +7,4 @@ resources: - namespace.yaml - secret.secret.yaml - deployment.yaml + - monitoring.yaml diff --git a/k8s/wireguard/monitoring.yaml b/k8s/wireguard/monitoring.yaml new file mode 100644 index 0000000..4dcbd7b --- /dev/null +++ b/k8s/wireguard/monitoring.yaml @@ -0,0 +1,71 @@ +apiVersion: v1 +kind: Service +metadata: + name: wireguard-exporter + namespace: wireguard + labels: + app: wireguard +spec: + ports: + - name: http-metrics + port: 9586 + targetPort: 9586 + protocol: TCP + selector: + app: wireguard +--- +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: wireguard + namespace: wireguard + labels: + release: prometheus-operator +spec: + selector: + matchLabels: + app: wireguard + namespaceSelector: + matchNames: + - wireguard + endpoints: + - port: http-metrics + path: /metrics + interval: 30s + scrapeTimeout: 10s +--- +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + name: wireguard-alerts + namespace: wireguard + labels: + release: prometheus-operator +spec: + groups: + - name: wireguard.rules + rules: + - alert: WireguardExporterDown + expr: up{job="wireguard-exporter"} == 0 + for: 5m + labels: + severity: critical + annotations: + summary: "WireGuard exporter is down" + description: "The WireGuard Prometheus exporter is unreachable. The pod may have crashed or is unresponsive." + - alert: WireguardInterfaceDown + expr: absent(wireguard_sent_bytes_total{interface="wg0"}) == 1 + for: 5m + labels: + severity: critical + annotations: + summary: "WireGuard interface wg0 is down" + description: "The WireGuard interface wg0 is not reporting any statistics. The VPN tunnel might be down or inactive." + - alert: WireguardPeerOffline + expr: (time() - wireguard_latest_handshake_seconds{interface="wg0"}) > 300 + for: 5m + labels: + severity: critical + annotations: + summary: "WireGuard peer {{ $labels.friendly_name }} offline" + description: "No handshake received for WireGuard peer {{ $labels.friendly_name }} ({{ $labels.public_key }}) on device {{ $labels.interface }} for more than 5 minutes." diff --git a/k8s/wireguard/secret.secret.yaml b/k8s/wireguard/secret.secret.yaml index 26e07271b417b4dd4c4c490bd911a3be9fc192f2..e05ee2d4181d27f5d1090a1d885bcf54f6c89daf 100644 GIT binary patch literal 443 zcmV;s0Yv@)M@dveQdv+`0NiQMuX%^24MLAbZ4!jwx=zwJ1iTOzP;=@E=3wR(=Tk^= zn;uS$$*yAp1jC_9_@MecjVMKcFc zKqA-ASW66K`KKj4Nt#=!)oHy;;kk?q(K+nI6I8chaP5oDqMk~_%S=OTkIK_oZTlUy zaE*0O>ENzmjpS|^1KY~#8`Y*6Lev%H280OnF)YjbVUQ4)#l`5bV9(W#ZH?NSf)yyp zw=59+ZzDVzN{E-zdLh-7bZQSo*a_aj8>Z`jMoA`=rgH#DtWn0LC8R*jgt#-^83sWDX)YRaLn7MXMrg3+TI`Yl?%Bk*=|6k5O~*tYsVVi4v@Mi4_apZnvf~i zGa?hF;WY<&*wBm8R994sknY(_^l|0k$Ga)xp@my73r07HdSJNjT4qTXYIqQE7lpY9 zzLcXAVN1*57l6m=UJXtr-5yBQ2EbmhDoxwyvGcXEzgK1e1Lyfa0bzxssHn8Ot3Y(o z9ij3NAX&)jL>E1^&TzmXHlxlgXv@k-0zApQTAX$}{}WJJX)VJi@f*(}+T81c{uGkS zuq`eyN%pNolK4QR2&X1AfAnDmPTJKw?ikCIM&C_?j@HTYr7dI3-7NbcIBGAN@)Sun z?A4}Wod*CIjcF!1%64CNfh^d~A