strongSwan is the most widely deployed open source IPsec VPN and IKEv2 implementation — the routing backbone for site-to-site tunnels, road warrior remote access deployments, and enterprise VPN infrastructure across thousands of organizations. When the charon daemon (the IKE negotiation process at the heart of strongSwan) crashes due to a plugin initialization failure after a configuration change, all established IKE and IPsec Security Associations (SAs) are torn down simultaneously and every VPN tunnel in your deployment drops at once — remote offices lose connectivity to headquarters, remote workers lose access to internal services, and no process-level monitor catches it because charon exited cleanly; when a site-to-site tunnel's IKE_AUTH negotiation starts failing at 50% of the time because an intermediate device began blocking UDP port 4500 (NAT-T), the tunnel stays in an intermittent established/rekeying loop — traffic flows sometimes and fails other times, making the failure appear to be an application problem rather than a VPN problem; when the VPN certificate used for IKE authentication expires, new IKE sessions fail authentication but existing established tunnels continue working until their rekey period arrives, so tunnel count drops gradually over hours rather than immediately at expiry. These are VPN connectivity failures with infrastructure-wide blast radius that require external monitoring of the charon daemon, SA counts, and certificate lifecycle to catch before users report them.
Vigilmon gives you external visibility into strongSwan's IPsec health through HTTP probe monitoring and heartbeat monitors for certificate expiry and tunnel state checks. This tutorial covers both.
Why strongSwan Needs External Monitoring
strongSwan failure modes have broad VPN connectivity impact:
- charon daemon crash: The charon process handles all IKE negotiation and installs IPsec Security Associations into the Linux kernel's XFRM subsystem; when charon crashes (plugin fault, certificate parsing failure, OOM kill), all IKE sessions are torn down and all XFRM Security Associations are removed — every VPN tunnel configured through strongSwan drops simultaneously; strongSwan does not auto-restart charon by default unless supervised by systemd with
Restart=on-failure; the VICI Unix socket at/var/run/charon.vicibecomes unreachable the moment charon exits - IKE_AUTH failure rate spike: When IKE authentication begins failing — due to certificate expiry, RADIUS authentication backend failure, EAP server unavailability, or a misconfigured PSK — new VPN tunnel establishments fail while existing established tunnels continue working until their rekey timer fires; the failure presents as "I can't connect" rather than "VPN is down" because existing sessions still work
- Dead Peer Detection (DPD) failures: strongSwan uses DPD to detect when a remote peer has become unreachable (crashed, network path gone); excessive DPD timeouts indicate network instability between VPN endpoints; each DPD timeout forces a tunnel teardown and renegotiation, causing brief connectivity gaps even when the remote peer recovers quickly
- XFRM policy divergence: charon programs Security Policies (SPs) and Security Associations (SAs) into the Linux kernel's XFRM subsystem; under certain race conditions (charon restart while kernel SAs are still active, missed XFRM netlink events), the policies and SAs installed in the kernel can diverge from what charon believes is established — traffic matching XFRM policies is dropped because the associated SA is missing;
ip xfrm policy countandip xfrm state countdivergence is the symptom - Certificate expiry on VPN authentication: strongSwan uses X.509 certificates for IKEv2 peer authentication by default; when the VPN authentication certificate expires, charon rejects IKE_AUTH exchanges from peers presenting the expired certificate; existing tunnels continue until rekey, then fail silently — tunnel count drops over hours as tunnels attempt rekey and fail
- Entropy depletion slowing IKE key generation: strongSwan's IKE key exchange (Diffie-Hellman) requires cryptographic entropy from the kernel; on embedded VPN appliances or heavily loaded VMs,
/dev/randomcan become a bottleneck during simultaneous tunnel establishment storms, causing IKE handshakes to take 10–30 seconds instead of <1 second
External monitoring with Vigilmon adds:
- Proactive alerting when the VICI socket becomes unreachable (charon crash indicator)
- SA count trending to catch tunnel count drops before users report connectivity failures
- DPD failure rate tracking through heartbeat monitors on scheduled swanctl status checks
- Certificate expiry pre-alerting with configurable advance warning before VPN auth failures begin
Step 1: Build a strongSwan Health Endpoint
strongSwan does not expose an HTTP health endpoint. Build a sidecar that queries the VICI interface via swanctl and exposes health over HTTP.
Node.js Health Sidecar
// health/strongswan.js
const express = require('express');
const { execSync } = require('child_process');
const app = express();
const CERT_SUBJECT = process.env.STRONGSWAN_CERT_SUBJECT || '';
const VPN_HOSTNAME = process.env.VPN_HOSTNAME || 'localhost';
function runSwanctl(args) {
return execSync(`swanctl ${args} 2>&1`, { timeout: 10000 }).toString().trim();
}
function getIkeSaCount() {
const output = runSwanctl('--list-sas');
// Count IKE_SA blocks (each starts with a connection name line)
const matches = output.match(/^\S+.*IKE_SA/gm) || [];
return matches.length;
}
function getChildSaCount() {
const output = runSwanctl('--list-sas');
// CHILD_SA lines contain "INSTALLED"
const matches = output.match(/INSTALLED/g) || [];
return matches.length;
}
function getDpdStats() {
// Parse swanctl --stats for DPD-related counters
try {
const output = runSwanctl('--stats');
const dpd = (output.match(/dpd-peer-timeout:\s+(\d+)/) || [])[1] || '0';
return parseInt(dpd);
} catch {
return -1;
}
}
function checkViCiSocket() {
try {
runSwanctl('--version');
return true;
} catch {
return false;
}
}
function getCertDaysToExpiry(subject) {
if (!subject) return null;
try {
// List loaded certificates and find expiry
const output = runSwanctl('--list-certs');
// Look for "expires:" line after the subject
const idx = output.indexOf(subject);
if (idx === -1) return null;
const after = output.slice(idx);
const expiryMatch = after.match(/expires:\s+(.+)/);
if (!expiryMatch) return null;
const expiry = new Date(expiryMatch[1].trim());
return Math.floor((expiry - Date.now()) / 86400000);
} catch {
return null;
}
}
app.get('/health/strongswan', (req, res) => {
const checks = {};
let healthy = true;
// VICI socket / charon daemon check
const viciOk = checkViCiSocket();
checks.charon_vici = viciOk ? 'ok' : 'unreachable';
if (!viciOk) {
healthy = false;
return res.status(503).json({ status: 'down', checks });
}
// Active IKE SA count
try {
const ikeSaCount = getIkeSaCount();
checks.ike_sa_count = ikeSaCount;
} catch (err) {
checks.ike_sa_count = `error: ${err.message}`;
}
// Active CHILD SA (IPsec tunnel) count
try {
const childSaCount = getChildSaCount();
checks.child_sa_count = childSaCount;
} catch (err) {
checks.child_sa_count = `error: ${err.message}`;
}
// DPD timeout count
try {
const dpdTimeouts = getDpdStats();
checks.dpd_timeouts_total = dpdTimeouts;
} catch (err) {
checks.dpd_timeouts = `error: ${err.message}`;
}
// Certificate expiry
if (CERT_SUBJECT) {
const daysLeft = getCertDaysToExpiry(CERT_SUBJECT);
if (daysLeft !== null) {
checks.cert_days_to_expiry = daysLeft;
if (daysLeft < 30) {
checks.cert_warning = `VPN auth cert expires in ${daysLeft} days`;
healthy = false;
}
}
}
// XFRM policy/state counts for divergence check
try {
const policies = execSync('ip xfrm policy count 2>/dev/null || echo 0',
{ timeout: 5000 }).toString().trim();
const states = execSync('ip xfrm state count 2>/dev/null || echo 0',
{ timeout: 5000 }).toString().trim();
checks.xfrm_policy_count = parseInt(policies) || 0;
checks.xfrm_state_count = parseInt(states) || 0;
} catch (err) {
checks.xfrm = `error: ${err.message}`;
}
return res.status(healthy ? 200 : 503).json({
status: healthy ? 'ok' : 'degraded',
checks,
});
});
app.listen(3020, () => console.log('strongSwan health sidecar on :3020'));
Python (FastAPI) Alternative
# health/strongswan_health.py
import os
import re
import subprocess
import time
from datetime import datetime, timezone
from fastapi import FastAPI, Response
import json
app = FastAPI()
CERT_SUBJECT = os.environ.get("STRONGSWAN_CERT_SUBJECT", "")
VPN_HOSTNAME = os.environ.get("VPN_HOSTNAME", "localhost")
def run_swanctl(args: str) -> str:
result = subprocess.run(
f"swanctl {args}",
shell=True, capture_output=True, text=True, timeout=10
)
return result.stdout + result.stderr
def check_vici_socket() -> bool:
try:
run_swanctl("--version")
return True
except Exception:
return False
def get_sa_counts() -> dict:
output = run_swanctl("--list-sas")
ike_count = len(re.findall(r"IKE_SA", output))
child_count = len(re.findall(r"INSTALLED", output))
return {"ike_sa_count": ike_count, "child_sa_count": child_count}
def get_dpd_stats() -> int:
try:
output = run_swanctl("--stats")
match = re.search(r"dpd-peer-timeout:\s+(\d+)", output)
return int(match.group(1)) if match else 0
except Exception:
return -1
def get_cert_days_to_expiry(subject: str):
if not subject:
return None
try:
output = run_swanctl("--list-certs")
idx = output.find(subject)
if idx == -1:
return None
after = output[idx:]
match = re.search(r"expires:\s+(.+)", after)
if not match:
return None
expiry_str = match.group(1).strip()
expiry = datetime.strptime(expiry_str, "%b %d %H:%M:%S %Y")
expiry = expiry.replace(tzinfo=timezone.utc)
now = datetime.now(timezone.utc)
return (expiry - now).days
except Exception:
return None
def get_xfrm_counts() -> dict:
try:
policy = subprocess.run(
"ip xfrm policy count", shell=True,
capture_output=True, text=True, timeout=5
)
state = subprocess.run(
"ip xfrm state count", shell=True,
capture_output=True, text=True, timeout=5
)
return {
"xfrm_policy_count": int(policy.stdout.strip() or 0),
"xfrm_state_count": int(state.stdout.strip() or 0),
}
except Exception as e:
return {"xfrm_error": str(e)}
@app.get("/health/strongswan")
async def strongswan_health():
checks = {}
healthy = True
vici_ok = check_vici_socket()
checks["charon_vici"] = "ok" if vici_ok else "unreachable"
if not vici_ok:
healthy = False
return Response(
content=json.dumps({"status": "down", "checks": checks}),
status_code=503, media_type="application/json"
)
try:
sa_counts = get_sa_counts()
checks.update(sa_counts)
except Exception as e:
checks["sa_counts"] = f"error: {str(e)}"
dpd = get_dpd_stats()
checks["dpd_timeouts_total"] = dpd
if CERT_SUBJECT:
days_left = get_cert_days_to_expiry(CERT_SUBJECT)
if days_left is not None:
checks["cert_days_to_expiry"] = days_left
if days_left < 30:
checks["cert_warning"] = f"VPN auth cert expires in {days_left} days"
healthy = False
checks.update(get_xfrm_counts())
status_code = 200 if healthy else 503
return Response(
content=json.dumps({"status": "ok" if healthy else "degraded", "checks": checks}),
status_code=status_code, media_type="application/json"
)
Step 2: Configure Vigilmon Monitoring
HTTP Monitor — strongSwan Health Endpoint
In your Vigilmon dashboard, create an HTTP monitor for the strongSwan sidecar:
| Field | Value |
|-------|-------|
| Monitor name | strongSwan VPN Health |
| URL | http://vpn-gateway.internal:3020/health/strongswan |
| Method | GET |
| Check interval | Every 1 minute |
| Expected status | 200 |
| Alert threshold | 2 consecutive failures |
| Regions | Select 2+ regions for consensus |
Heartbeat Monitor — IKE SA Count Check
Monitor the active IKE session count and alert on unexpected drops. Configure a 5-minute timeout heartbeat:
#!/bin/bash
# /etc/cron.d/strongswan-sa-count — runs every 3 minutes
# Alert if IKE SA count drops below expected minimum
HEARTBEAT_URL="https://vigilmon.online/hb/your-strongswan-sa-count-heartbeat"
MIN_IKE_SAS="${STRONGSWAN_MIN_IKE_SAS:-1}"
IKE_COUNT=$(swanctl --list-sas 2>/dev/null | grep -c "IKE_SA" || echo 0)
if [ "$IKE_COUNT" -ge "$MIN_IKE_SAS" ]; then
curl -fsS --max-time 10 "$HEARTBEAT_URL" > /dev/null 2>&1
else
echo "strongSwan IKE SA count dropped to ${IKE_COUNT} (expected >=${MIN_IKE_SAS})" >&2
# Optionally notify ops channel
fi
Configure the heartbeat with a 5-minute timeout to catch drops within one check cycle.
Heartbeat Monitor — DPD Failure Rate
Watch for Dead Peer Detection failures indicating network instability:
#!/bin/bash
# /etc/cron.d/strongswan-dpd-check — runs every 10 minutes
HEARTBEAT_URL="https://vigilmon.online/hb/your-strongswan-dpd-heartbeat"
LOG_FILE="/var/log/strongswan.log"
MAX_DPD_PER_HOUR=10
# Count DPD timeout messages in the last 60 minutes
DPD_COUNT=$(grep "DPD" "$LOG_FILE" 2>/dev/null | \
awk -v cutoff="$(date -d '60 minutes ago' '+%Y-%m-%d %H:%M')" \
'$0 > cutoff' | grep -c "peer not responding" || echo 0)
if [ "$DPD_COUNT" -lt "$MAX_DPD_PER_HOUR" ]; then
curl -fsS --max-time 10 "$HEARTBEAT_URL" > /dev/null 2>&1
else
echo "strongSwan DPD failure rate: ${DPD_COUNT} timeouts in last hour (threshold: ${MAX_DPD_PER_HOUR})" >&2
fi
Configure with a 15-minute timeout (runs every 10 minutes with 5-minute slack).
Heartbeat Monitor — Certificate Expiry Pre-Alert
#!/bin/bash
# /etc/cron.d/strongswan-cert-check — runs daily
HEARTBEAT_URL="https://vigilmon.online/hb/your-strongswan-cert-heartbeat"
CERT_FILE="${STRONGSWAN_CERT_FILE:-/etc/ipsec.d/certs/vpn-cert.pem}"
DAYS_LEFT=$(openssl x509 -noout -enddate -in "$CERT_FILE" 2>/dev/null | \
sed 's/notAfter=//' | \
python3 -c "
import sys
from datetime import datetime, timezone
expiry = datetime.strptime(sys.stdin.read().strip(), '%b %d %H:%M:%S %Y %Z')
expiry = expiry.replace(tzinfo=timezone.utc)
from datetime import datetime as dt2
now = dt2.now(timezone.utc)
print((expiry - now).days)
" 2>/dev/null)
if [ -n "$DAYS_LEFT" ] && [ "$DAYS_LEFT" -gt 30 ]; then
curl -fsS --max-time 10 "$HEARTBEAT_URL" > /dev/null 2>&1
else
echo "strongSwan VPN certificate expires in ${DAYS_LEFT:-unknown} days" | \
mail -s "ALERT: strongSwan VPN Certificate Expiry" ops@your-org.example.com
fi
Configure with a 25-hour timeout for daily checks with slack.
Step 3: Configure Alerting
Alert Policies
charon Daemon Down Alert
- Trigger: Health endpoint returns 503 with
charon_vici: unreachable - Severity: Critical — P1
- Channels: PagerDuty (immediate) + Slack #infrastructure-critical
- Message: "strongSwan charon daemon is unreachable. All IKE SAs and IPsec tunnels have been torn down. VPN connectivity is completely unavailable. Restart charon via
systemctl restart strongswanand investigate logs."
IKE SA Count Drop Alert
- Trigger: Heartbeat not received for 5 minutes (SA count below minimum)
- Severity: High
- Channels: PagerDuty + Slack #vpn-alerts
- Message: "strongSwan IKE SA count has dropped below expected minimum. One or more VPN tunnels have disconnected. Check
swanctl --list-sasfor current state and/var/log/strongswan.logfor authentication or negotiation errors."
DPD Failure Rate Alert
- Trigger: Heartbeat not received for 15 minutes (>10 DPD timeouts/hour)
- Severity: High
- Channels: Slack #vpn-alerts + email to network team
- Message: "strongSwan Dead Peer Detection failure rate is elevated. VPN peers are not responding to DPD probes. Check network path stability between VPN endpoints and verify UDP 500/4500 are not being blocked."
VPN Certificate Expiry Alert
- Trigger: Heartbeat not received for 25 hours (cert <30 days to expiry)
- Severity: High
- Channels: Slack #infrastructure-alerts + email to ops
- Message: "strongSwan VPN authentication certificate expires within 30 days. Renew the certificate using your CA before expiry causes IKE_AUTH failures for all new tunnel establishments."
Key Metrics Summary
| Metric | Alert Threshold | Impact | |--------|----------------|--------| | charon daemon / VICI socket | Any failure | All VPN tunnels torn down immediately | | Active IKE SA count | Drop below expected baseline | VPN session loss; remote access failures | | Active CHILD SA (IPsec tunnel) count | Drop below expected baseline | Traffic-level tunnel failures | | IKE_AUTH failure rate | >10% of attempts | New VPN connections failing; rekey failures begin | | DPD timeout rate | >10/hour sustained | Network instability causing tunnel disruption | | VPN certificate days to expiry | <30 days | Scheduled IKE_AUTH failures at expiry | | XFRM policy vs SA count divergence | Any divergence | Encrypted traffic silently dropped | | Tunnel throughput (bytes/SA) | Drop >50% vs baseline | Routing or upstream network issue | | EAP authentication failure rate | >5% of attempts | Remote access (road warrior) failures | | Kernel entropy pool | Depletion | IKE key exchange slows; handshakes timeout |
Conclusion
strongSwan is the VPN backbone for site-to-site connectivity and remote access across enterprise and cloud deployments — when the charon daemon crashes or VPN certificates expire, every tunnel in the deployment is affected. Vigilmon HTTP probes on the strongSwan health sidecar catch charon process failures the moment the VICI socket becomes unreachable, while heartbeat monitors on SA count checks, DPD failure rates, and certificate expiry pre-alerts give you coverage of the gradual failures that process monitors miss. Configure the monitors in this tutorial and your network team will receive alerts about VPN problems before remote offices and workers report they cannot reach internal resources.