| name | ssl-cert-monitor |
| description | Operate ssl-cert-monitor -- add hosts, configure alert rules, trigger checks, review history, and deploy the stack. |
| triggers | ["ssl-cert-monitor","cert monitor","certificate monitor","ssl expiry"] |
When to use this skill
Use this skill when working with ssl-cert-monitor: configuring hostnames to monitor, setting expiry thresholds, creating Slack webhook alert rules, reviewing certificate history, or deploying the stack.
Quick Start (development)
cd ssl-cert-monitor
pnpm install
cp .env.example .env
cd backend && pnpm dev
cd frontend && pnpm dev
Quick Start (Docker)
docker compose up -d
Environment Variables
| Variable | Default | Description |
|---|
PORT | 3000 | Express server port |
DB_PATH | ./data/ssl.db | SQLite database path |
CHECK_INTERVAL_MINUTES | 60 | Default check interval for new hosts |
WARN_DAYS | 30 | Default warning threshold |
CRIT_DAYS | 7 | Default critical threshold |
WEBHOOK_TIMEOUT_MS | 5000 | Webhook delivery timeout |
CORS_ORIGIN | http://localhost:5173 | Dashboard CORS origin |
VITE_API_URL | http://localhost:3000 | API base URL for frontend |
Adding Hosts
Via the dashboard: click "Add Host", enter the hostname and port, optionally set display name and per-host thresholds. A connection test runs before saving.
Via the API:
curl -X POST http://localhost:3000/api/hosts \
-H "Content-Type: application/json" \
-d '{"hostname":"api.example.com","port":443,"warnDaysBeforeExpiry":30,"critDaysBeforeExpiry":7}'
Triggering an Immediate Check
curl -X POST http://localhost:3000/api/hosts/1/check
The check runs synchronously and returns the new CheckResult.
API Reference
| Method | Path | Description |
|---|
GET | /api/health | { ok: true } |
GET | /api/hosts | List all hosts |
POST | /api/hosts | Add a host |
GET | /api/hosts/:id | Host with latest check |
PUT | /api/hosts/:id | Update host settings |
DELETE | /api/hosts/:id | Remove host and history |
POST | /api/hosts/:id/check | Trigger immediate check |
GET | /api/hosts/:id/checks | Check history (90 days) |
GET | /api/checks | All recent results (100) |
GET | /api/alerts | Hosts below threshold |
GET | /api/alert-rules | Alert rule list |
POST | /api/alert-rules | Create alert rule |
PUT | /api/alert-rules/:id | Update alert rule |
DELETE | /api/alert-rules/:id | Delete alert rule |
POST | /api/alert-rules/:id/test | Send test webhook |
GET | /api/settings | Current settings |
POST | /api/settings | Update settings |
Alert Rules
Alert rules fire when a certificate's daysUntilExpiry falls below the rule's thresholdDays. Rules match hostnames using glob patterns (e.g. *.example.com or *).
One webhook delivery per host per rule per 24 hours (deduplication in the scheduler).
Webhook payload (Slack-compatible):
{
"text": "SSL certificate for api.example.com expires in 6 days (2024-03-21)",
"attachments": [{
"color": "#dc2626",
"fields": [
{ "title": "Host", "value": "api.example.com:443" },
{ "title": "Expires", "value": "2024-03-21" },
{ "title": "Days Remaining", "value": "6" },
{ "title": "Severity", "value": "critical" }
]
}]
Check Schedule
The scheduler ticks every minute via node-cron. On each tick it queries for hosts where last_checked_at IS NULL OR last_checked_at <= datetime('now', '-N minutes'). Checks run concurrently with max 5 simultaneous connections.
Expiry Severity Thresholds
| State | Condition | Badge Color |
|---|
| ok | > warn_days | Green |
| warning | <= warn_days and > crit_days | Amber |
| critical | <= crit_days and > 0 | Red |
| expired | <= 0 | Red |
| error | Connection failed | Violet |
Troubleshooting
"Connection timed out"
- Confirm the host is reachable on port 443 from the server running ssl-cert-monitor
- Check firewall rules; the server needs outbound TCP to port 443
- For Docker: ensure the backend container has network access to external hosts
"No certificate returned"
- The host is listening on the port but not offering TLS (may be HTTP-only)
- Or the port accepts connections but drops them before the TLS handshake
"Webhook not receiving test"
- Verify the webhook URL is correct and the receiving service (Slack, Discord) is operational
- Check
webhook_deliveries table for error_message from failed attempts
- Increase
WEBHOOK_TIMEOUT_MS if the receiving endpoint is slow
Checks not running automatically
- Confirm the backend process is running:
GET /api/health
- Verify
node-cron is scheduling: check logs for "Scheduler started" on startup