How to monitor SSL certificate expiration across multiple domains
What you're building
You need three things:
1. One list of every hostname you're responsible for.
2. A scheduled check that reads each certificate's expiry date and works out the days left.
3. An alert that reaches a person while there's still time to fix the problem.
You can set this up in under an hour with openssl, cron and a Slack webhook or mail command.
Step 1: Build the domain list
Create domains.txt with one hostname per line:
`
client-a.com
www.client-a.com
shop.client-b.co.uk
mail.client-c.com:465
`
Put every hostname that serves HTTPS on the list, not just the main domain:
- both
wwwand the bare domain; - subdomains such as
shop.,staging.,portal.andapi.; - mail servers and other non-443 services (add the port);
- client sites you inherited and never set up yourself. These are the most likely to break.
To find hostnames you've forgotten, search Certificate Transparency logs at crt.sh (for example %.client-a.com). Every publicly trusted certificate gets logged there.
Step 2: The check script
`bash
#!/usr/bin/env bash
while read -r line; do
[ -z "$line" ] && continue
host=${line%%:*}; port=${line##*:}; [ "$port" = "$line" ] && port=443
end=$(echo | timeout 10 openssl s_client -connect "$host:$port" -servername "$host" 2>/dev/null \
| openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
if [ -z "$end" ]; then echo "$host ERROR no-certificate-or-unreachable"; continue; fi
days=$(( ( $(date -d "$end" +%s) - $(date +%s) ) / 86400 ))
echo "$host $days $end"
done < "${1:-domains.txt}" | sort -k2 -n
`
Run it with bash check-ssl.sh domains.txt. Example output:
`
old-client.org ERROR no-certificate-or-unreachable
shop.client-b.co.uk 9 Oct 25 08:14:02 2026 GMT
client-a.com 61 Dec 16 10:00:00 2026 GMT
`
Notes:
-servernamematters. Without SNI, shared hosting often returns the wrong certificate.date -dis GNU syntax. On macOS, installcoreutilsand usegdate, or run the script on a small Linux server.- An ERROR is an alert too. It can mean an expired or missing certificate, a DNS change or a dead site, and you want to hear about any of them.
Step 3: Alert only when something needs action
Keep the lines that are in trouble (errors, or fewer than 21 days left) and send them only if there are any:
`bash
#!/usr/bin/env bash
out=$(bash /opt/ssl/check-ssl.sh /opt/ssl/domains.txt | awk '$2=="ERROR" || $2+0 < 21')
[ -z "$out" ] && exit 0
curl -s -X POST -H 'Content-type: application/json' \
--data "{\"text\": \"SSL attention needed:\n$out\"}" "$SLACK_WEBHOOK_URL"
`
Schedule it every day:
`
0 8 * * * SLACK_WEBHOOK_URL=https://hooks.slack.com/services/XXX /opt/ssl/alert.sh
`
Choosing the warning window
Auto-renewing certificates usually renew well before they expire. Certbot, for example, renews when about 30 days are left, which is one third of a 90-day certificate's life. So a certificate showing fewer than about 21 days usually means auto-renewal has already failed. That leaves you three weeks to fix it. For certificates you renew by hand, alert earlier, at 30 and 14 days.
Mistakes that cause missed renewals anyway
- The monitor dies silently. If the cron box goes down, you'll just stop hearing anything, which looks the same as all being fine. Have the script send a short "all OK, N domains checked" message once a week as a heartbeat.
- Alerts go to one person. Send them to a shared channel or a role address, not the inbox of someone who might be on leave or no longer with you.
- CDN hides the origin. Behind Cloudflare and similar proxies you're checking the edge certificate. If the origin certificate matters (for example in Full (strict) mode), add an origin check:
openssl s_client -connect ORIGIN_IP:443 -servername client-a.com. - Multiple servers. If a hostname resolves to several IPs, each server can hold a different certificate. Check them individually.
- The list goes stale. Add "add to SSL list" to your new-client onboarding checklist, and "remove from list" to offboarding.
- Alert fatigue. If the same warning fires every day for a site you no longer manage, people start ignoring all of them. Prune the list.
Why this matters more each year
Under CA/Browser Forum ballot SC-081, the maximum lifetime of public certificates drops to 200 days (from March 15, 2026), then 100 days (2027), then 47 days (2029) (DNSimple). More renewals mean more chances for one to fail without anyone noticing. Let's Encrypt also ended its expiry reminder emails in 2025, so the warning many people relied on no longer arrives.
If you'd rather not run a script
The DIY setup works, but someone has to own the server, the cron job and the list. We're building Lapsewatch for agency ops teams. It will keep one list of client domains with days-to-expiry and send email or Slack alerts before anything lapses, with no server to maintain. It isn't available yet; the page below has the details.