How to monitor SSL certificate expiration
Check SSL expiry from the command line, set useful alert thresholds, and automate certificate expiry monitoring so you never wake up to a broken site.
By the Opservo team at Softuvo Solutions · Updated
An expired SSL certificate breaks your site for every visitor, kills API calls, and can take a Laravel app or e-commerce checkout completely offline in seconds. It happens to careful teams too, because certificates quietly tick down and the renewal reminder arrives in an inbox nobody checks. This guide shows you how to check SSL certificate expiration from the command line, what thresholds to alert on, and how to automate ssl expiry monitoring so the expiry date never sneaks past you.
How to check an SSL certificate expiration date manually
The fastest check is a one-liner with openssl. It connects to the server on port 443 and prints the certificate's notAfter field.
# Check expiry for a live domain
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| openssl x509 -noout -dates
# Expected output:
# notBefore=Apr 1 00:00:00 2024 GMT
# notAfter=Apr 1 23:59:59 2025 GMT
# Check days remaining (prints a number; negative means already expired)
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| openssl x509 -noout -enddate \
| awk -F= '{print $2}' \
| xargs -I{} date -d "{}" +%s \
| xargs -I{} sh -c 'echo $(( ({} - $(date +%s)) / 86400 )) days remaining'
# Check a certificate file on disk instead
openssl x509 -noout -dates -in /etc/letsencrypt/live/example.com/cert.pemThe -servername flag matters. Without it, openssl sends no SNI header and a server hosting multiple domains may return the wrong certificate. Always include it when you are checking a shared host or a CDN origin.
If the command hangs, the host is likely blocking port 443 from your machine or the DNS does not resolve. Check with curl -v https://example.com first and confirm the IP is reachable.
What to alert on: concrete thresholds for ssl certificate expiry monitoring
There is no single right threshold, but these three levels cover most teams:
- 30 days: send a Slack or email notice. This is plenty of time for a manual renewal or to investigate why auto-renew did not run.
- 14 days: escalate the alert. If certbot or acme.sh has been failing silently, you still have time to fix it before users notice.
- 3 days: wake someone up. At this point auto-renewal has clearly failed and you need a human to act immediately.
- 0 days (or negative): the certificate is already expired. Treat this as an outage, not a warning.
For internal APIs and staging environments the same thresholds apply. A staging cert expiring mid-sprint blocks every developer on the team, which is just as disruptive as a production outage.
If you manage multiple sites, the manual check above does not scale. The next section covers how to run it across a fleet.
How to script ssl certificate monitoring for multiple domains
A simple shell script lets you check every domain in a list and print any that expire within your chosen threshold. Save the list in a plain text file, one domain per line, and run the script from cron.
#!/usr/bin/env bash
# ssl-check.sh — alert when a cert expires within WARN_DAYS
WARN_DAYS=30
DOMAINS_FILE="/etc/ssl-monitor/domains.txt"
while IFS= read -r domain; do
[ -z "$domain" ] && continue
expiry=$(echo | openssl s_client -servername "$domain" \
-connect "${domain}:443" 2>/dev/null \
| openssl x509 -noout -enddate 2>/dev/null \
| cut -d= -f2)
if [ -z "$expiry" ]; then
echo "WARN: could not retrieve cert for $domain"
continue
fi
expiry_epoch=$(date -d "$expiry" +%s 2>/dev/null)
now_epoch=$(date +%s)
days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
if [ "$days_left" -le 0 ]; then
echo "CRITICAL: $domain cert EXPIRED ($days_left days)"
elif [ "$days_left" -le "$WARN_DAYS" ]; then
echo "WARN: $domain expires in $days_left days ($expiry)"
else
echo "OK: $domain expires in $days_left days"
fi
done < "$DOMAINS_FILE"
# Add to cron to run daily at 08:00 and email output:
# 0 8 * * * /etc/ssl-monitor/ssl-check.sh | mail -s "SSL expiry report" ops@example.comThis works well for a handful of domains. Once you are managing ten or more servers, each potentially hosting dozens of vhosts, keeping the domains list accurate becomes its own maintenance burden. That is the point where a purpose-built ssl certificate expiry monitoring tool saves more time than it costs.
If you are already thinking about broader server visibility, monitoring multiple Linux servers from one place is a natural next step, especially when each server hosts its own set of vhosts and certificates.
Certificate expiry monitoring for Laravel and other app stacks
Laravel apps often run behind nginx with multiple vhosts: one for the main application, one for an API subdomain, sometimes one for a separate admin panel. Each vhost has its own certificate, and they may renew on different schedules if you added subdomains at different times. An expired cert on the API subdomain will not affect the main site in a browser, but it will silently break every mobile client and third-party integration.
The openssl command above works for any subdomain. Include every vhost hostname in your domains list, not just the apex domain. The same applies to Django apps on subdomains, Node services behind a reverse proxy, and any FastAPI or PHP app served through nginx or Apache.
Laravel-specific monitoring covers more than just SSL, from queue workers to Horizon health. Monitoring Laravel in production goes into the full picture if you want those checks alongside certificate expiry.
Common reasons ssl certificate expiration sneaks past you
- Certbot or acme.sh runs on a cron job that silently fails after a server rebuild or OS upgrade.
- The renewal email goes to a shared inbox or a former employee's address.
- A wildcard certificate renews correctly but a specific subdomain cert does not, because they are managed separately.
- A CDN or load balancer terminates TLS, so the origin cert expires unnoticed while the CDN cert stays valid.
- A staging or internal domain is excluded from monitoring because it "does not matter", until it does.
For the CDN case, you need to check the cert at the CDN edge as well as the origin. Use the -connect flag to point openssl directly at the origin IP if the CDN masks it, or check both hostnames explicitly.
Always check the certificate your the client actually sees, not the one on disk. A renewal can succeed locally but the web server may still be serving the old cached cert until nginx or Apache is reloaded.
This is also why checking the live connection with openssl s_client matters more than checking the file in /etc/letsencrypt. If you forget to run systemctl reload nginx after certbot renew, the live cert stays expired even though the file is fresh.
Automating ssl expiry monitoring without managing scripts
The script above works, but it adds something to maintain: you have to keep the domain list current, check the cron logs, and remember to add new vhosts when you deploy them. For teams without a dedicated SRE, that maintenance often slips. If you are thinking about how to cover all of this with less manual overhead, monitoring a Linux server without an SRE covers the broader pattern.
Opservo auto-discovers nginx and Apache vhosts from the server's config and tracks SSL expiry for each one without any domain list to maintain. It sends alerts at 30, 14 and 3 days, and you can add external HTTP or domain checks for domains hosted elsewhere. Every alert tells you the domain, the days remaining and which server it belongs to, so you know exactly what to renew and where.
Invite the reader to add their servers to Opservo's free 2-server plan to get auto-discovered vhost SSL alerts without maintaining a domain list. — one command installs a read-only agent and your first findings arrive in five minutes: start free. Related: guides monitor laravel in production.