What “monitor an SSL certificate” actually means
The job is to read the certificate that a server presents during a TLS handshake, extract its expiry date, compare it to today, and decide whether to alert.
That’s it. The entire workflow runs on tools that already exist on any Linux/macOS system: openssl, curl, optionally date and mail.
The simplest check
Here’s the one-liner that tells you when a certificate expires:
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -noout -enddate
Output:
notAfter=Aug 12 12:00:00 2026 GMT
That’s the expiry timestamp from the certificate the server is currently serving. Compare it to today and you have your check.
Breaking down what that command does:
openssl s_client -servername example.com -connect example.com:443— open a TLS connection. The-servernameflag is SNI; without it, you’ll get the default cert on multi-tenant hosts.echo |— pipe an empty stdin sos_clientdoesn’t block waiting for input. Without this, the command never exits.2>/dev/null— suppress connection chatter so only the certificate output remainsopenssl x509 -noout -enddate— parse the cert and print just the expiry date
Converting to “days until expiry”
The raw timestamp is hard to alert on. We want days remaining.
expiry=$(echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| openssl x509 -noout -enddate \
| cut -d= -f2)
expiry_epoch=$(date -d "$expiry" +%s) # GNU date
# For macOS / BSD date:
# expiry_epoch=$(date -j -f "%b %d %H:%M:%S %Y %Z" "$expiry" +%s)
now_epoch=$(date +%s)
days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
echo "Days until expiry: $days_left"
Save that as check_cert.sh, make it executable, and you can run it as a cron job.
Multi-domain monitoring
For more than one domain, loop over them:
#!/bin/bash
DOMAINS=(
"example.com"
"api.example.com"
"blog.example.com"
)
THRESHOLD_DAYS=30
for domain in "${DOMAINS[@]}"; do
expiry=$(echo | openssl s_client -servername "$domain" -connect "$domain":443 2>/dev/null \
| openssl x509 -noout -enddate \
| cut -d= -f2)
if [ -z "$expiry" ]; then
echo "WARNING: Could not fetch cert for $domain"
continue
fi
expiry_epoch=$(date -d "$expiry" +%s)
now_epoch=$(date +%s)
days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
if [ "$days_left" -lt "$THRESHOLD_DAYS" ]; then
echo "ALERT: $domain expires in $days_left days ($expiry)"
else
echo "OK: $domain expires in $days_left days"
fi
done
Run via cron daily:
0 8 * * * /opt/scripts/check_cert.sh | mail -s "Cert check" [email protected]
That’s a working SSL monitor. Total infrastructure: a cron job and mail.
What else to check (beyond expiry)
Expiry is the most important check, but a few others are worth automating:
Chain validation. A certificate is part of a chain back to a trusted root CA. If the server is misconfigured and presents only the leaf cert without the intermediate, modern browsers will work (they fetch intermediates themselves), but some older clients fail. Check with:
openssl s_client -showcerts -servername example.com -connect example.com:443 2>/dev/null </dev/null
You should see at least two BEGIN CERTIFICATE blocks (leaf + intermediate). If there’s only one, your chain is incomplete.
Common name / SAN matches. The certificate should be valid for the domain you’re checking. Mismatches cause NET::ERR_CERT_COMMON_NAME_INVALID in Chrome.
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| openssl x509 -noout -text \
| grep -A1 "Subject Alternative Name"
Issuer. Confirm the cert was issued by who you expect. If you set up Let’s Encrypt and the live cert is suddenly issued by some random CA you’ve never heard of, that’s worth investigating.
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| openssl x509 -noout -issuer
Protocol versions. Modern best practice is TLS 1.2 minimum, TLS 1.3 preferred. To verify:
openssl s_client -tls1_3 -servername example.com -connect example.com:443 2>/dev/null </dev/null | grep "Protocol"
If -tls1_3 fails, the server doesn’t support 1.3.
Using curl instead of openssl
If openssl is more verbose than you want, curl provides a higher-level interface:
curl -vI https://example.com 2>&1 | grep "expire date"
Output:
* expire date: Aug 12 12:00:00 2026 GMT
Curl’s output is more human-readable but harder to parse programmatically. Use openssl for scripts, curl for one-off debugging.
Edge cases to handle
A few things that trip up naive cert checkers:
Self-signed certificates. Internal services often use self-signed certs. You’ll want to skip chain validation (-k in curl, -noverify in openssl) but still check expiry.
SNI hosting. Multiple sites on one IP. Always pass -servername to openssl or use curl’s natural HTTPS handling.
Connection failures. If openssl can’t connect at all, your script needs to distinguish “site is down” from “cert is expired” — they’re different problems with different responses. Set a meaningful timeout (-connect_timeout 5 or wrap with timeout 10).
Time zones. Cert expiry is always in GMT. If your alerting system uses local time, double-check the math.
Certificate transparency. If your provider rotates certs frequently (common with Let’s Encrypt), the “expiry” check still works fine — you just see a recently-issued cert each time.
When to use a service instead
The above works fine for small numbers of domains you control. The reasons to use a hosted SSL monitor instead:
- More than ~20 domains — managing the cron jobs and alerting at scale becomes its own problem
- Notification routing — you want SSL alerts to go to specific channels (Slack, PagerDuty), not just email
- Historical tracking — knowing “this cert was issued by X on date Y” matters for compliance
- Integration with other checks — pairing SSL with uptime, response time, content checks in one pane
If you’re already using a monitoring service for uptime, SSL expiry is usually free or bundled. MyUptimeBot’s free plan includes SSL expiry monitoring on the one monitor you can run. Paid plans extend that to all monitors.
For purely self-hosted setups, Uptime Kuma does SSL checks alongside uptime and is genuinely good — install it on a Raspberry Pi and you have a self-hosted monitor for free.
A robust standalone script
Putting it all together — a script that handles the edge cases and produces useful output:
#!/bin/bash
set -uo pipefail
DOMAINS_FILE="${DOMAINS_FILE:-/etc/cert_check_domains.txt}"
THRESHOLD_WARN="${THRESHOLD_WARN:-30}"
THRESHOLD_CRIT="${THRESHOLD_CRIT:-7}"
check_domain() {
local domain="$1"
local cert_data
cert_data=$(timeout 10 sh -c "echo | openssl s_client -servername '$domain' -connect '$domain':443 2>/dev/null | openssl x509 -noout -enddate -issuer 2>/dev/null")
if [ -z "$cert_data" ]; then
echo "ERROR: $domain — cannot fetch certificate"
return 1
fi
local expiry
expiry=$(echo "$cert_data" | grep notAfter | cut -d= -f2)
local expiry_epoch
expiry_epoch=$(date -d "$expiry" +%s 2>/dev/null) || \
expiry_epoch=$(date -j -f "%b %d %H:%M:%S %Y %Z" "$expiry" +%s 2>/dev/null)
if [ -z "$expiry_epoch" ]; then
echo "ERROR: $domain — could not parse expiry: $expiry"
return 1
fi
local now_epoch=$(date +%s)
local days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
if [ "$days_left" -lt "$THRESHOLD_CRIT" ]; then
echo "CRITICAL: $domain expires in $days_left days"
elif [ "$days_left" -lt "$THRESHOLD_WARN" ]; then
echo "WARNING: $domain expires in $days_left days"
else
echo "OK: $domain expires in $days_left days"
fi
}
while read -r domain; do
[ -z "$domain" ] && continue
[ "${domain:0:1}" = "#" ] && continue
check_domain "$domain"
done < "$DOMAINS_FILE"
Save your domains one per line in /etc/cert_check_domains.txt, schedule the script via cron, pipe the output to email or your alerting webhook. Total code: ~50 lines.
The principle
SSL certificate monitoring is one of those problems where the basic version is easy enough that it’s hard to justify not doing it. A few hours of shell scripting, a cron job, and a domain list gives you protection against an entire category of preventable outage.
Buying a service is the right answer when scale or integration matters. Building it is the right answer when you have a handful of domains and an afternoon. Either way, the cost of no SSL monitoring is the day a cert expires unnoticed and your traffic vanishes.