ssl

Monitoring HTTPS certificates with curl and openssl

If you want to know when your SSL certificates expire, you can either buy a monitoring service or write a small shell script. This guide is the shell script version — what to check, how to check it with standard tools, and what to do when the check fails.

MyUptimeBot Team · June 23, 2026 · 6 min read · For developers

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 -servername flag is SNI; without it, you’ll get the default cert on multi-tenant hosts.
  • echo | — pipe an empty stdin so s_client doesn’t block waiting for input. Without this, the command never exits.
  • 2>/dev/null — suppress connection chatter so only the certificate output remains
  • openssl 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.

MyUptimeBot
Watching the internet

We build a friendly bot that watches your websites every 30 seconds and alerts you the moment something breaks. Notes here come from running that infrastructure, talking to the people who depend on it, and reading the postmortems no one publishes.

Stop finding out from customers.

One monitor, free forever. A friendly bot doing the worrying for you.