Checking SSL certificate expiry from the command line is a core sysadmin and DevOps task. The tools are already installed on virtually every Linux and macOS system — you just need the right invocations. This guide covers the essential bash one-liners, multi-domain scripts, and cron job patterns for SSL expiry checking, along with an honest assessment of where shell-script-based monitoring falls short.
The canonical way to check a live certificate's expiry date from bash:
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -enddate
Output:
notAfter=Dec 14 23:59:59 2026 GMT
The -servername flag enables SNI (Server Name Indication), which is required for virtual-hosted servers. Without it, servers with multiple certs on the same IP may return the wrong certificate.
To get the number of days remaining:
expiry=$(echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)
echo $(( ($(date -d "$expiry" +%s) - $(date +%s)) / 86400 )) days remaining
On macOS, date -d is not available. Use this instead:
expiry=$(echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)
echo $(( ($(date -j -f "%b %e %T %Y %Z" "$expiry" +%s) - $(date +%s)) / 86400 )) days remaining
If you prefer curl or need to avoid openssl directly, curl exposes certificate information via the --head and verbose flags:
# Most portable curl approach — prints expiry to stderr
curl -v --head https://example.com 2>&1 | grep "expire date"
Output:
* expire date: Dec 14 23:59:59 2026 GMT
For scripting, the cleaner curl approach uses -w format strings if your curl version supports it, or pipes through grep:
curl -vI https://example.com 2>&1 \
| grep -i "expire date\|start date\|subject\|issuer" \
| head -10
-L or -I) and respects system CA bundles, making it convenient for quick interactive checks. openssl s_client gives you raw TLS handshake detail and is more reliable in scripts because it doesn't depend on curl version or platform-specific behavior. For scripting multi-domain checks, openssl s_client is the safer choice.
If you have the certificate file itself (e.g., from a Kubernetes secret, Certbot renewal, or cert store):
# From a PEM file
openssl x509 -noout -enddate -in /etc/ssl/certs/example.com.crt
# From a DER-encoded file
openssl x509 -inform DER -noout -enddate -in example.com.der
# Full details: subject, issuer, validity period
openssl x509 -noout -text -in /etc/ssl/certs/example.com.crt | grep -A2 "Validity"
A straightforward script for checking a list of domains and printing days remaining for each:
#!/bin/bash
# check-ssl-expiry.sh — check SSL expiry for a list of domains
DOMAINS=(
"example.com"
"api.example.com"
"staging.example.com"
"mail.example.com"
)
WARN_DAYS=30
for domain in "${DOMAINS[@]}"; do
expiry=$(echo | openssl s_client -connect "${domain}:443" \
-servername "$domain" 2>/dev/null \
| openssl x509 -noout -enddate 2>/dev/null \
| cut -d= -f2)
if [ -z "$expiry" ]; then
echo "FAIL $domain (could not retrieve certificate)"
continue
fi
days=$(( ($(date -d "$expiry" +%s) - $(date +%s)) / 86400 ))
if [ "$days" -le 0 ]; then
echo "EXPIRED $domain (expired $((days * -1)) days ago)"
elif [ "$days" -le "$WARN_DAYS" ]; then
echo "WARN $days days $domain"
else
echo "OK $days days $domain"
fi
done
Make it executable and run it:
chmod +x check-ssl-expiry.sh
./check-ssl-expiry.sh
Sample output:
OK 73 days example.com
WARN 18 days api.example.com
OK 61 days staging.example.com
FAIL mail.example.com (could not retrieve certificate)
Extend the script to send an email when a cert is approaching expiry:
#!/bin/bash
DOMAINS=("example.com" "api.example.com" "staging.example.com")
WARN_DAYS=30
ALERT_EMAIL="ops@example.com"
for domain in "${DOMAINS[@]}"; do
expiry=$(echo | openssl s_client -connect "${domain}:443" \
-servername "$domain" 2>/dev/null \
| openssl x509 -noout -enddate 2>/dev/null \
| cut -d= -f2)
[ -z "$expiry" ] && {
echo "SSL check failed for $domain" \
| mail -s "SSL Check Failed: $domain" "$ALERT_EMAIL"
continue
}
days=$(( ($(date -d "$expiry" +%s) - $(date +%s)) / 86400 ))
if [ "$days" -le "$WARN_DAYS" ]; then
echo "$domain SSL certificate expires in $days days ($expiry)" \
| mail -s "SSL Expiry Warning: $domain ($days days)" "$ALERT_EMAIL"
fi
done
Run the check daily. Add to crontab with crontab -e:
# Run SSL expiry check daily at 8:00 AM
0 8 * * * /usr/local/bin/check-ssl-expiry.sh >> /var/log/ssl-check.log 2>&1
Or using a more verbose format that logs timestamps:
0 8 * * * /usr/bin/bash -c 'echo "=== $(date) ===" >> /var/log/ssl-check.log && /usr/local/bin/check-ssl-expiry.sh >> /var/log/ssl-check.log 2>&1'
Verify the cron job runs by checking the log after the next scheduled time:
tail -f /var/log/ssl-check.log
The scripts above work. They also have failure modes that are difficult to close with more bash:
If the cron daemon stops, the server migrates, the disk fills up, or the mail server is unavailable, no alerts fire. More importantly, you get no notification that monitoring stopped. This is the classic monitoring gap: you only find out something was broken when you notice the expired cert, not when the monitoring broke.
A cron-based SSL check has no watchdog. You'd need a second cron job to verify the first one ran, and a third to monitor the second. External monitoring services sidestep this by running from infrastructure you don't control — if your server goes down, they still check your endpoint.
When a new domain goes live, someone has to remember to add it to the script. Teams regularly miss domains this way — especially subdomains, staging environments, and third-party services that use your TLS certs.
The script connects from your server. It doesn't check from multiple geographic locations, it doesn't test what users in different regions see, and it can't detect CDN-terminated TLS issues where the CDN is serving an expired cert from its own cache.
Use the free SSLWatch checker to immediately see how many days remain on any domain's certificate — no bash, no openssl, no local setup required:
Enter any domain and see days remaining, expiry date, issuer, and chain status in seconds. No signup required.
Check SSL expiry → Get expiry alerts →SSLWatch automates the SSL monitoring loop that bash scripts implement manually. It checks your HTTPS endpoints on a regular schedule, sends email alerts at 30, 14, 7, and 1 day before expiry, and runs from external infrastructure that isn't affected by your server's health.
No script to maintain. No cron job to watch. No domain list to update manually. The monitoring runs whether your server is healthy or not.
SSLWatch monitors your domains with scheduled external checks and sends expiry alerts before the cert becomes a problem.
Join the SSLWatch waitlist → Free expiry checker →