Unbound is a validating, recursive, caching DNS resolver trusted by privacy-focused organizations, ISPs, and infrastructure teams. When Unbound goes down, every service on the network that relies on DNS resolution — internal service discovery, API calls, database connections — stops working. Because Unbound runs as a system daemon rather than an application, it often lacks the same alerting coverage as web services.
This tutorial covers production-grade uptime monitoring for Unbound using Vigilmon. We will walk through:
- Exposing a health endpoint from Unbound via unbound-control
- Setting up TCP port monitoring for DNS availability
- Heartbeat monitoring for scheduled health checks
- Webhook alerts for DOWN/UP events
Prerequisites
- Unbound 1.17+ running on Linux (Debian/Ubuntu or RHEL/Rocky)
- A free account at vigilmon.online
Part 1: Enable unbound-control and verify Unbound is healthy
Unbound ships with unbound-control, a CLI tool for querying status, statistics, and managing the resolver at runtime. Enable it so you can inspect Unbound from scripts.
Configure unbound-control
# Generate the control certificates
unbound-control-setup
This writes keys and certificates to /etc/unbound/:
unbound_control.keyunbound_control.pemunbound_server.keyunbound_server.pem
Add the remote control block to your Unbound configuration:
# /etc/unbound/unbound.conf
server:
# Listen on all interfaces for DNS queries
interface: 0.0.0.0
port: 53
# Enable DNSSEC validation
auto-trust-anchor-file: "/var/lib/unbound/root.key"
# Hide version string from external queries
hide-version: yes
hide-identity: yes
remote-control:
control-enable: yes
control-interface: 127.0.0.1
control-port: 8953
server-key-file: "/etc/unbound/unbound_server.key"
server-cert-file: "/etc/unbound/unbound_server.pem"
control-key-file: "/etc/unbound/unbound_control.key"
control-cert-file: "/etc/unbound/unbound_control.pem"
Restart Unbound:
systemctl restart unbound
Verify unbound-control can connect:
unbound-control status
Expected output:
version: 1.19.3
verbosity: 1
threads: 4
modules: 3 [ validator iterator ]
uptime: 42 seconds
options: reuseport
unbound (pid 12345) is running...
Part 2: Expose a health endpoint for external monitoring
Unbound does not have a built-in HTTP server. The standard pattern is a small HTTP health server that wraps unbound-control status and returns 200/503 based on its exit code.
Python health server
#!/usr/bin/env python3
# unbound_health.py
import subprocess
from http.server import HTTPServer, BaseHTTPRequestHandler
def check_unbound():
result = subprocess.run(
["unbound-control", "status"],
capture_output=True,
text=True,
timeout=5
)
is_running = result.returncode == 0 and "is running" in result.stdout
return is_running, result.stdout.strip()
class HealthHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/health":
running, status_output = check_unbound()
if running:
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(b'{"status":"ok","resolver":"running"}')
else:
self.send_response(503)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(b'{"status":"error","resolver":"stopped"}')
else:
self.send_response(404)
self.end_headers()
def log_message(self, format, *args):
pass # suppress default access log
if __name__ == "__main__":
server = HTTPServer(("0.0.0.0", 8095), HealthHandler)
print("Unbound health server on :8095")
server.serve_forever()
Run it as a systemd service:
# /etc/systemd/system/unbound-health.service
[Unit]
Description=Unbound health check HTTP server
After=unbound.service
Requires=unbound.service
[Service]
Type=simple
ExecStart=/usr/bin/python3 /opt/unbound-health/unbound_health.py
Restart=always
RestartSec=5
User=root
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now unbound-health
Verify the health endpoint
curl http://localhost:8095/health
# {"status":"ok","resolver":"running"}
Part 3: Expose the health endpoint via nginx for external monitoring
# /etc/nginx/sites-available/unbound-health
server {
listen 443 ssl;
server_name dns-health.example.com;
ssl_certificate /etc/letsencrypt/live/dns-health.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/dns-health.example.com/privkey.pem;
# Allow only the health endpoint externally
location /health {
proxy_pass http://127.0.0.1:8095/health;
proxy_set_header Host $host;
access_log off;
}
# Block everything else
location / {
return 403;
}
}
nginx -t && systemctl reload nginx
Verify:
curl https://dns-health.example.com/health
# {"status":"ok","resolver":"running"}
Part 4: Set up monitoring in Vigilmon
Monitor Unbound process health
- Log in to vigilmon.online and click Add Monitor.
- Choose HTTP(S) monitor.
- Enter:
https://dns-health.example.com/health - Set interval to 1 minute.
- Add a keyword check: must contain
"status":"ok". - Add your alert channel.
- Click Save.
Monitor DNS port availability (TCP monitor)
The HTTP health check confirms the Unbound process is running. A TCP port 53 monitor confirms DNS queries can actually be served — if a firewall rule blocks port 53 or Unbound is bound only to localhost, the health endpoint might return 200 while DNS resolution is broken for clients.
- In Vigilmon, click Add Monitor.
- Choose TCP monitor (or Port monitor).
- Enter: your Unbound server's public IP, port
53. - Set interval to 1 minute.
- Add your alert channel.
- Click Save.
Use both monitors together:
- HTTP health (
:8095/health) = "Is the Unbound process alive?" - TCP port 53 = "Can DNS clients actually reach it?"
Part 5: DNS resolution heartbeat
Confirming that DNS port 53 is open does not verify that Unbound is resolving queries correctly. DNSSEC validation failures, forwarder misconfiguration, or a corrupted root hints file can cause Unbound to reject all queries while appearing to listen on port 53.
Add a heartbeat cron job that runs a real DNS query and PINGs Vigilmon on success:
#!/bin/bash
# /opt/unbound-health/check-resolution.sh
set -euo pipefail
RESOLVER="127.0.0.1"
TEST_DOMAIN="cloudflare.com"
VIGILMON_HEARTBEAT_URL="https://vigilmon.online/api/heartbeat/your-unbound-heartbeat-id"
# Perform a real DNS query via Unbound
RESULT=$(dig +short @${RESOLVER} ${TEST_DOMAIN} A 2>&1)
if echo "$RESULT" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then
# Query returned a valid IP — ping Vigilmon heartbeat
curl -s -o /dev/null "$VIGILMON_HEARTBEAT_URL"
echo "$(date): OK - resolved ${TEST_DOMAIN} to ${RESULT}"
else
echo "$(date): FAIL - unexpected result: ${RESULT}" >&2
exit 1
fi
chmod +x /opt/unbound-health/check-resolution.sh
Add to cron:
# /etc/cron.d/unbound-resolution-check
* * * * * root /opt/unbound-health/check-resolution.sh >> /var/log/unbound-health.log 2>&1
In Vigilmon, configure the heartbeat monitor with a 3-minute window — if Vigilmon does not receive a ping within 3 minutes of the expected 1-minute interval, it sends a DOWN alert.
Part 6: Enhanced health check with DNSSEC validation
For environments where DNSSEC validation is critical, extend the resolution check to verify that DNSSEC-signed zones validate correctly:
#!/bin/bash
# check-dnssec.sh
RESOLVER="127.0.0.1"
DNSSEC_DOMAIN="dnssec.works" # A domain known to require DNSSEC validation
VIGILMON_HEARTBEAT="https://vigilmon.online/api/heartbeat/your-dnssec-heartbeat-id"
# Check a domain that must pass DNSSEC validation
RESULT=$(dig +dnssec +short @${RESOLVER} ${DNSSEC_DOMAIN} A 2>&1)
STATUS=$?
# Check a domain with a deliberately broken DNSSEC signature
BROKEN_RESULT=$(dig +short @${RESOLVER} dnssec-failed.org A 2>&1)
# Successful validation: DNSSEC_DOMAIN resolves, broken domain does not
if [ $STATUS -eq 0 ] && echo "$RESULT" | grep -qE '[0-9]+\.[0-9]+' && [ -z "$BROKEN_RESULT" ]; then
curl -s -o /dev/null "$VIGILMON_HEARTBEAT"
echo "$(date): DNSSEC validation OK"
else
echo "$(date): DNSSEC validation FAIL - valid: ${RESULT}, broken: ${BROKEN_RESULT}" >&2
exit 1
fi
Part 7: Docker deployment
# docker-compose.yml
version: "3.8"
services:
unbound:
image: mvance/unbound:latest
ports:
- "53:53/udp"
- "53:53/tcp"
volumes:
- ./unbound.conf:/opt/unbound/etc/unbound/unbound.conf:ro
restart: unless-stopped
unbound-health:
image: python:3.12-slim
command: python /app/unbound_health.py
ports:
- "8095:8095"
volumes:
- ./unbound_health.py:/app/unbound_health.py:ro
# Override health check to use the process-level approach for Docker
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:8095/health"]
interval: 30s
timeout: 5s
retries: 3
depends_on:
- unbound
restart: unless-stopped
Kubernetes deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: unbound
namespace: dns
spec:
replicas: 2
selector:
matchLabels:
app: unbound
template:
metadata:
labels:
app: unbound
spec:
containers:
- name: unbound
image: mvance/unbound:latest
ports:
- containerPort: 53
protocol: UDP
- containerPort: 53
protocol: TCP
- name: health
image: python:3.12-slim
command: ["python", "/app/unbound_health.py"]
ports:
- containerPort: 8095
livenessProbe:
httpGet:
path: /health
port: 8095
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /health
port: 8095
initialDelaySeconds: 5
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: unbound-health
namespace: dns
spec:
selector:
app: unbound
ports:
- name: health
port: 8095
targetPort: 8095
type: ClusterIP
Part 8: SSL certificate monitoring
If your health endpoint is behind TLS (recommended for production):
- In Vigilmon, click Add Monitor.
- Choose SSL monitor.
- Enter:
dns-health.example.com - Set alert threshold to 14 days before expiry.
- Add your alert channel.
Part 9: Webhook alerts
// webhook-receiver.ts
import express from 'express';
const app = express();
app.use(express.json());
app.post('/webhook/vigilmon', (req, res) => {
const { monitor_name, status, url, response_code, checked_at } = req.body;
if (status === 'down') {
console.error('[VIGILMON] Unbound monitor DOWN', {
monitor: monitor_name,
url,
code: response_code,
at: checked_at,
});
// DNS failures are P1 — page on-call immediately
pageOnCall({
title: `Unbound DNS resolver is DOWN`,
severity: 'critical',
details: `Monitor: ${monitor_name}\nURL: ${url}\nHTTP: ${response_code}`,
});
} else if (status === 'up') {
console.info('[VIGILMON] Unbound recovered', { monitor: monitor_name });
resolveIncident({ monitor: monitor_name });
}
res.sendStatus(204);
});
app.listen(3000);
Vigilmon sends this payload on DOWN and UP transitions:
{
"monitor_id": "mon_abc123",
"monitor_name": "Unbound DNS Health",
"status": "down",
"url": "https://dns-health.example.com/health",
"checked_at": "2026-06-30T09:00:00Z",
"response_code": 503,
"response_time_ms": 5003
}
Summary
Your Unbound deployment now has four layers of monitoring:
- Process health endpoint — a sidecar HTTP server wraps
unbound-control statusand returns 200/503; Vigilmon polls it every 60 seconds to confirm the resolver is alive. - TCP port 53 monitor — confirms DNS clients can actually reach Unbound on port 53, independent of the health sidecar.
- DNS resolution heartbeat — a cron job runs a real DNS query every minute and pings Vigilmon's heartbeat endpoint on success; a missed ping triggers a DOWN alert.
- DNSSEC validation check — an extended heartbeat verifies that DNSSEC validation is working correctly, not just that port 53 is open.
Vigilmon handles check scheduling, multi-region polling, alert routing, and uptime history — so you know within 60 seconds when Unbound fails, before your services do.
Monitor your Unbound DNS resolver free at vigilmon.online
#unbound #dns #dnssec #devops #monitoring #infrastructure