Clair is an open-source static analysis tool for detecting vulnerabilities in container images, originally developed at CoreOS and widely adopted in enterprise container registries including Quay.io. Clair ingests CVE data from upstream vulnerability databases, indexes container image layers, and exposes an HTTP API that CI/CD pipelines and registries query to block vulnerable images before deployment. When Clair's database becomes unavailable, when its vulnerability updater job silently fails to fetch new CVE data, or when the REST API becomes unresponsive, security teams lose visibility into container vulnerabilities without any alert. Vigilmon gives you external visibility into Clair's HTTP API, its PostgreSQL backend, TLS certificate health, and the automated scanning pipelines that depend on it.
What You'll Build
- HTTP monitors on Clair's health and metrics endpoints
- TCP monitors on the Clair API and PostgreSQL database ports
- SSL certificate monitors for Clair's HTTPS endpoints
- Heartbeat monitors for automated image scanning pipelines
- Alerting runbook mapping Clair failure modes to monitoring signals
Prerequisites
- Clair deployed (standalone binary, Docker Compose, or Kubernetes)
- PostgreSQL database backing Clair
- A free account at vigilmon.online
Step 1: Understand the Clair Health Surface
A production Clair deployment exposes several endpoints that reflect its operational state:
# Clair health endpoint (HTTP API mode)
curl -s http://clair.example.com:6060/healthz
# Expected: 200 OK, body: "OK"
# Clair introspection endpoint (gRPC-Gateway)
curl -s http://clair.example.com:8089/api/v1/health
# Expected: 200 OK
# Metrics endpoint (Prometheus scrape target)
curl -s http://clair.example.com:6061/metrics | head -20
# Expected: Prometheus metric lines
# Check the updater status via the matcher API
curl -s http://clair.example.com:6060/matcher/api/v1/updateoperations
# Expected: JSON list of recent update operations
# Check PostgreSQL connectivity used by Clair
psql "postgres://clair:password@db.example.com:5432/clair" \
-c "SELECT count(*) FROM uo_envelopes;" 2>&1
# Expected: row count for update operations stored
The key failure signals to monitor:
- Clair HTTP API availability (the primary scanning interface)
- PostgreSQL database connectivity (Clair stores all vulnerability data here)
- TLS certificate validity on Clair's HTTPS endpoint
- Updater pipeline heartbeats (CVE data freshness)
- Scan pipeline heartbeats (automated image analysis jobs)
Step 2: Monitor the Clair HTTP API
The Clair health endpoint is the primary signal that the service is running and able to process requests:
# Standalone Clair health check (default port 6060)
curl -v http://clair.example.com:6060/healthz
# Expected: HTTP 200, body "OK"
# Clair on Kubernetes via ClusterIP service
curl -v http://clair-service.clair-system.svc.cluster.local:6060/healthz
# Test the indexer API endpoint directly
curl -s -X POST http://clair.example.com:6060/indexer/api/v1/index_report \
-H "Content-Type: application/json" \
-d '{"manifest": {"hash": "sha256:test", "layers": []}}' 2>&1 | head -5
# Expected: 400 (bad request) or 200 — confirms API is processing requests
Create a Vigilmon monitor for the Clair health endpoint:
- Log in to Vigilmon → Add Monitor → HTTP.
- URL:
http://clair.example.com:6060/healthz(or your Clair host). - Check interval: 60 seconds.
- Response timeout: 10 seconds.
- Expected status codes:
200. - Expected body contains:
OK. - Label:
Clair API health. - Click Save.
If Clair exposes metrics, add a second monitor for the metrics endpoint as a secondary signal:
- Add Monitor → HTTP.
- URL:
http://clair.example.com:6061/metrics. - Expected status codes:
200. - Label:
Clair metrics endpoint. - Click Save.
Step 3: Monitor the Clair API TCP Port
A TCP monitor detects network-layer failures that the HTTP monitor alone can miss (load balancer routing issues, Kubernetes service misconfiguration):
# Test TCP connectivity to Clair API
nc -zv clair.example.com 6060
# Expected: Connection succeeded
# Test from inside the Kubernetes cluster
kubectl exec -n clair-system \
$(kubectl get pod -n clair-system -l app=clair -o name | head -1) \
-- nc -zv clair-service 6060
- Add Monitor → TCP.
- Host:
clair.example.com. - Port:
6060. - Check interval: 60 seconds.
- Label:
Clair API TCP (6060). - Click Save.
If Clair's gRPC introspection API runs on a separate port (default 8089):
- Add Monitor → TCP.
- Host:
clair.example.com. - Port:
8089. - Label:
Clair introspection TCP (8089). - Click Save.
Step 4: Monitor the PostgreSQL Database Port
Clair stores all indexed image data, vulnerability records, and update operation history in PostgreSQL. A database connectivity failure stops all indexing and matching:
# Test PostgreSQL port reachability from the Clair host
nc -zv db.example.com 5432
# Expected: Connection succeeded
# Validate the Clair database exists and is accessible
psql "postgres://clair:password@db.example.com:5432/clair" \
-c "\dt" 2>&1 | grep "update_operation\|indexer_manifest" | head -5
# Expected: table names from Clair's schema
# Check active Clair connections to the database
psql "postgres://postgres:adminpass@db.example.com:5432/postgres" \
-c "SELECT count(*) FROM pg_stat_activity WHERE datname='clair';"
# Expected: count > 0 while Clair is running
- Add Monitor → TCP.
- Host:
db.example.com(your PostgreSQL host). - Port:
5432. - Check interval: 60 seconds.
- Label:
Clair PostgreSQL DB TCP (5432). - Click Save.
A TCP failure on port 5432 while the Clair API HTTP monitor returns a non-200 response confirms the failure is at the database layer rather than the application layer.
Step 5: Monitor TLS Certificates
If Clair is exposed behind a TLS-terminating proxy (Nginx, Traefik, or a Kubernetes Ingress), an expired certificate breaks all client connections — including CI/CD pipelines that scan images before deployment:
# Check TLS certificate on Clair's HTTPS endpoint
echo | openssl s_client -connect clair.example.com:443 2>/dev/null | \
openssl x509 -noout -dates
# notAfter=Sep 30 00:00:00 2026 GMT
# Check with curl verbose TLS output
curl -v https://clair.example.com/healthz 2>&1 | grep -E "expire|SSL|TLS|certificate"
# List all Clair TLS certificates in Kubernetes
kubectl get certificate -n clair-system
# Expected: certificate resources with READY=True
- Add Monitor → SSL Certificate.
- Domain:
clair.example.com. - Alert when expiry is within: 30 days.
- Alert again: 14 days, 7 days, 3 days, 1 day.
- Click Save.
Add SSL monitors for any other domains used to reach Clair (e.g., quay.example.com if running alongside Quay).
Step 6: Heartbeat Monitoring for Clair Pipelines
Clair's value depends on two automated processes running continuously: the updater that fetches new CVE data from upstream sources, and the scanning pipeline that indexes new container images. Neither has an externally queryable health signal — Vigilmon heartbeats fill this gap.
CVE Database Updater Pipeline
Clair's updater fetches vulnerability data from NVD, Red Hat, Debian, Ubuntu, and Alpine security databases on a regular schedule. If it stops running silently, scans continue against stale CVE data with no alert:
#!/bin/bash
# check-clair-updater.sh — run via cron every 6 hours
CLAIR_API="http://clair.example.com:6060"
VIGILMON_HB="https://vigilmon.online/heartbeat/abc123"
# Query the latest update operation timestamp
LATEST_UPDATE=$(curl -sf "${CLAIR_API}/matcher/api/v1/updateoperations" | \
python3 -c "
import sys, json
ops = json.load(sys.stdin)
if ops and len(ops) > 0:
# Find the most recent update operation date
dates = [op.get('date', '') for op in ops.values() if isinstance(op, dict)]
print(max(dates) if dates else '')
" 2>/dev/null)
if [ -z "${LATEST_UPDATE}" ]; then
echo "ERROR: Could not retrieve update operations from Clair"
exit 1
fi
echo "Latest Clair CVE update: ${LATEST_UPDATE}"
# Signal that the updater check ran successfully
curl -s "${VIGILMON_HB}"
Set up the heartbeat monitor:
- Add Monitor → Cron Heartbeat.
- Expected interval:
420minutes (Clair default updater interval is 6 hours, with a 60-minute grace period). - Copy the heartbeat URL:
https://vigilmon.online/heartbeat/abc123. - Schedule the check script:
0 */6 * * * /opt/scripts/check-clair-updater.sh. - Label:
Clair CVE updater pipeline. - Click Save.
Image Scanning Pipeline (CI/CD Integration)
A GitHub Actions workflow that scans container images with Clair before pushing to the production registry:
# .github/workflows/image-scan.yml
name: Scan image with Clair
on:
push:
branches: [main]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Submit image to Clair for indexing
run: |
# Push to staging registry so Clair can index it
docker tag myapp:${{ github.sha }} registry.example.com/staging/myapp:${{ github.sha }}
docker push registry.example.com/staging/myapp:${{ github.sha }}
# Trigger Clair index via the API
MANIFEST=$(docker inspect --format='{{index .RepoDigests 0}}' \
registry.example.com/staging/myapp:${{ github.sha }})
curl -sf -X POST http://clair.example.com:6060/indexer/api/v1/index_report \
-H "Content-Type: application/json" \
-d "{\"manifest\": {\"hash\": \"${MANIFEST}\", \"layers\": []}}"
- name: Retrieve and check vulnerability report
run: |
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' \
registry.example.com/staging/myapp:${{ github.sha }})
REPORT=$(curl -sf \
"http://clair.example.com:6060/matcher/api/v1/vulnerability_report/${DIGEST}")
CRITICAL=$(echo "${REPORT}" | python3 -c \
"import sys, json; d=json.load(sys.stdin); print(sum(1 for v in d.get('vulnerabilities', {}).values() if v.get('normalized_severity') == 'Critical'))")
echo "Critical vulnerabilities: ${CRITICAL}"
if [ "${CRITICAL}" -gt 0 ]; then
echo "FAIL: ${CRITICAL} critical CVEs found"
exit 1
fi
- name: Ping Vigilmon heartbeat
if: success()
run: curl -s https://vigilmon.online/heartbeat/def456
- Add Monitor → Cron Heartbeat.
- Expected interval:
1500minutes (expected daily scans with grace period). - Label:
Clair CI/CD image scan pipeline. - Click Save.
Scheduled Retrospective Scan Job
A cron job that re-scans all images in the registry when new CVE data arrives:
#!/bin/bash
# retrospective-scan.sh — triggered after Clair updater completes
REGISTRY="registry.example.com"
CLAIR_API="http://clair.example.com:6060"
VIGILMON_HB="https://vigilmon.online/heartbeat/ghi789"
FAILED=0
# Enumerate all images in the registry (using registry API)
IMAGES=$(curl -sf "https://${REGISTRY}/v2/_catalog" | \
python3 -c "import sys, json; [print(r) for r in json.load(sys.stdin)['repositories']]")
for IMAGE in ${IMAGES}; do
DIGEST=$(crane digest "${REGISTRY}/${IMAGE}:latest" 2>/dev/null)
if [ -z "${DIGEST}" ]; then
echo "SKIP: Cannot get digest for ${REGISTRY}/${IMAGE}:latest"
continue
fi
REPORT=$(curl -sf \
"${CLAIR_API}/matcher/api/v1/vulnerability_report/${DIGEST}" 2>/dev/null)
if [ $? -ne 0 ]; then
echo "FAIL: Could not retrieve report for ${IMAGE}"
FAILED=1
else
CRITICAL=$(echo "${REPORT}" | python3 -c \
"import sys, json; d=json.load(sys.stdin); print(sum(1 for v in d.get('vulnerabilities', {}).values() if v.get('normalized_severity') == 'Critical'))" 2>/dev/null)
echo "OK: ${IMAGE} — ${CRITICAL} critical CVEs"
fi
done
if [ "${FAILED}" -eq 0 ]; then
curl -s "${VIGILMON_HB}"
fi
- Add Monitor → Cron Heartbeat.
- Expected interval:
720minutes (expected after every 6-hour updater cycle, with grace period). - Label:
Clair retrospective scan job. - Click Save.
Step 7: Configure Alerting
In Vigilmon under Settings → Notifications, configure alert channels and response runbooks:
| Monitor | Trigger | Immediate action |
|---|---|---|
| Clair API health | Non-200 or timeout | Check Clair process; run curl http://clair:6060/healthz from inside the cluster; check Clair logs for DB errors |
| Clair metrics endpoint | Non-200 | Secondary signal; confirm with API health monitor; check Prometheus scrape config |
| Clair API TCP | Connection refused | Check Kubernetes service and pod status; kubectl get pods -n clair-system |
| PostgreSQL TCP | Connection refused | Database connectivity failure; all Clair indexing and matching will fail; check DB host |
| TLS certificate | < 30 days | Rotate certificate on reverse proxy; verify with openssl s_client after renewal |
| CVE updater heartbeat | Missed ping | Check Clair updater logs; verify network access to NVD, Red Hat, and Debian advisory feeds |
| CI/CD scan heartbeat | Missed ping | Check GitHub Actions logs; verify Clair API is reachable from the CI runner |
| Retrospective scan heartbeat | Missed ping | Check cron job logs; verify Clair is accepting index requests and returning reports |
Common Clair Failure Modes and What Vigilmon Catches
| Scenario | Vigilmon signal |
|---|---|
| Clair API process crashed | HTTP health monitor fires; all image scans blocked |
| PostgreSQL database down | TCP monitor on port 5432 fires; Clair logs DB connection errors; API returns 503 |
| Clair network namespace issue in Kubernetes | TCP monitor fires; HTTP monitor fires; kubectl describe pod shows network policy block |
| TLS certificate expired on Clair reverse proxy | SSL monitor fires at 30-day threshold; CI pipelines get TLS errors on scan API calls |
| CVE updater stopped fetching new data | Updater heartbeat goes silent; scans continue against stale CVE database |
| NVD or upstream feed outage blocks updater | Updater heartbeat fires; check Clair logs for upstream HTTP errors |
| CI scan pipeline misconfigured | CI scan heartbeat goes silent; images ship without vulnerability gating |
| Clair indexer queue backlogged | API returns 200 but reports are delayed; heartbeat latency increases |
| Database disk full stops writes | PostgreSQL available on TCP but Clair logs write errors; health endpoint may return 500 |
| Clair update to new major version breaks API | HTTP monitor fires after upgrade; revert or fix the deployment |
Clair makes container vulnerability scanning possible — but only when its API, database, and updater pipeline are all operating correctly. Vigilmon gives you the external visibility layer that alerts you when Clair's own health endpoint is unreachable, when the PostgreSQL database drops off the network, or when the updater pipeline silently stops refreshing CVE data. When a CVE database updater fails silently and images continue scanning against stale data, Vigilmon's heartbeat monitor is the only thing standing between your team and a missed critical vulnerability.
Start monitoring your Clair deployment in under 5 minutes — register free at vigilmon.online.