How-to guide · Operate and monitor

How to check when a certificate expires, and monitor it

In one sentence

You will have read expiry dates in files, live servers and certificate stores, and set up a check that warns before any certificate in a chain runs out.

Task
Operate and monitor
Platforms
  • Linux
  • macOS
  • Windows
  • Kubernetes
  • Java
Who it is for
Level
Practitioner · 6 steps
On this page
  1. Before you start
  2. Steps
  3. 1. Read the end date of a certificate file
  4. 2. Test a threshold with -checkend
  5. 3. Check what a server really sends
  6. 4. Check certificates in stores
  7. 5. Monitor the whole chain with a script
  8. 6. Choose when to alert
  9. Check it worked
  10. If something goes wrong
  11. Why this works
  12. Go deeper

Before you start

  • OpenSSL (Linux, macOS with Homebrew, or Git Bash on Windows).
  • GNU date for the script in step 5. Linux and Git Bash have it; on macOS, install GNU coreutils and use gdate.
  • A list of what to check: host names and ports, and where certificates are stored (files, Kubernetes secrets, Java keystores, Windows stores).

Expired certificates are among the most common self-inflicted outages, and the time between renewals keeps shrinking. Public TLS certificates may last at most 200 days from 15 March 2026, 100 days from 15 March 2027 and 47 days from 15 March 2029. And Let's Encrypt no longer sends expiration reminder emails (since 4 June 2025), so nobody else will warn you.

Steps

1. Read the end date of a certificate file

openssl x509 -in old.crt -noout -startdate -enddate
notBefore=Sep  1 00:00:00 2026 GMT
notAfter=Oct 24 00:00:00 2026 GMT

The times are in GMT. A certificate is valid from notBefore through notAfter, inclusive (RFC 5280, section 4.1.2.5). Both dates sit inside the signed part of the certificate, so nobody can extend them: renewing always means a new certificate.

2. Test a threshold with -checkend

-checkend takes a number of seconds and answers with an exit code: 0 if the certificate is still valid at that moment, 1 if it will have expired. The test certificate above, checked on 20 October 2026, seven days ahead and then one day ahead:

openssl x509 -in old.crt -noout -checkend 604800; echo "exit=$?"
openssl x509 -in old.crt -noout -checkend 86400; echo "exit=$?"
Certificate will expire
exit=1
Certificate will not expire
exit=0

Useful values: 86400 is one day, 604800 is seven days and 2592000 is 30 days.

3. Check what a server really sends

The file on disk is not always what clients get. Ask the live endpoint:

openssl s_client -connect www.rfc-editor.org:443 -servername www.rfc-editor.org </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -enddate
subject=CN=rfc-editor.org
notAfter=Jan  8 12:18:06 2027 GMT

Always pass -servername: a server that hosts many names may otherwise send its default certificate. curl built with OpenSSL (Linux, macOS with Homebrew) prints the same date in verbose mode:

curl -sv -o /dev/null https://www.rfc-editor.org/ 2>&1 | grep -E 'start date|expire date'
*  start date: Oct 10 11:18:19 2026 GMT
*  expire date: Jan  8 12:18:06 2027 GMT

The curl.exe that ships with Windows uses Schannel and does not print these lines.

4. Check certificates in stores

Kubernetes keeps TLS certificates in secrets of type kubernetes.io/tls:

kubectl get secrets -A --field-selector type=kubernetes.io/tls
kubectl get secret web-tls -n shop -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -subject -enddate
subject=C=BE, O=Example Shop, CN=www.example.com
notAfter=Jan 18 01:42:24 2027 GMT

Java keystores, with the JDK’s keytool (the first line is the server certificate, the next its CA):

keytool -list -v -keystore www.p12 | grep -E 'Alias name|until:'
Alias name: www
Valid from: Tue Oct 20 03:42:24 CEST 2026 until: Mon Jan 18 02:42:24 CET 2027
Valid from: Tue Oct 20 03:42:23 CEST 2026 until: Sun Oct 19 03:42:23 CEST 2031

keytool shows local time, not GMT.

Windows certificate stores, in PowerShell:

# Still valid, but expiring within 30 days
Get-ChildItem -Path Cert:\LocalMachine\My -ExpiringInDays 30 | Select-Object Subject, NotAfter, Thumbprint

# Already expired
Get-ChildItem -Path Cert:\LocalMachine\My -ExpiringInDays 0 | Select-Object Subject, NotAfter, Thumbprint

-ExpiringInDays is a parameter of the certificate provider. With a positive number it returns only certificates that are still valid, so run both lines, or filter on NotAfter yourself.

5. Monitor the whole chain with a script

Checking only the server’s own certificate misses expired intermediates, which break every certificate below them. This script checks every certificate each server sends and warns when one has less than a sixth of its lifetime left:

#!/usr/bin/env bash
# check-expiry.sh: check every certificate each server sends.
# Warns when a certificate has less than 1/FRACTION of its lifetime left.
# Needs OpenSSL and GNU date (Linux, Git Bash). Usage: check-expiry.sh host[:port] ...
FRACTION=${FRACTION:-6}
status=0

check_one() {  # $1 = target, $2 = position in chain, $3 = PEM certificate
  local start end now subject
  start=$(date -d "$(openssl x509 -noout -startdate <<< "$3" | cut -d= -f2)" +%s)
  end=$(date -d "$(openssl x509 -noout -enddate <<< "$3" | cut -d= -f2)" +%s)
  subject=$(openssl x509 -noout -subject -nameopt RFC2253 <<< "$3" | cut -d= -f2-)
  now=$(date +%s)
  local left=$(( (end - now) / 86400 )) life=$(( (end - start) / 86400 ))
  if [ $(( (end - now) * FRACTION )) -lt $(( end - start )) ]; then
    echo "WARN  $1 #$2 $subject: $left of $life days left"; return 1
  fi
  echo "OK    $1 #$2 $subject: $left of $life days left"
}

for target in "$@"; do
  host=${target%%:*}; port=${target##*:}; [ "$port" = "$target" ] && port=443
  chain=$(openssl s_client -connect "$host:$port" -servername "$host" -showcerts </dev/null 2>/dev/null \
          | sed -n '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/p')
  if [ -z "$chain" ]; then echo "ERROR $target: no certificate received"; status=2; continue; fi
  cert=""; i=0
  while IFS= read -r line; do
    cert+="$line"$'\n'
    if [ "$line" = "-----END CERTIFICATE-----" ]; then
      check_one "$target" "$i" "$cert" || { [ "$status" -eq 0 ] && status=1; }
      cert=""; i=$((i + 1))
    fi
  done <<< "$chain"
done
exit "$status"

Run against two public sites and a test server whose certificate ends on 24 October:

bash check-expiry.sh www.rfc-editor.org letsencrypt.org 127.0.0.1:8445 nothing.invalid; echo "exit=$?"
OK    www.rfc-editor.org #0 CN=rfc-editor.org: 80 of 90 days left
OK    www.rfc-editor.org #1 CN=WE1,O=Google Trust Services,C=US: 854 of 1896 days left
OK    www.rfc-editor.org #2 CN=GTS Root R4,O=Google Trust Services LLC,C=US: 464 of 1534 days left
OK    letsencrypt.org #0 CN=letsencrypt.org: 44 of 89 days left
...
WARN  127.0.0.1:8445 #0 CN=old.example.com: 3 of 53 days left
OK    127.0.0.1:8445 #1 CN=Keystone Demo Issuing CA 1,O=Keystone Demo,C=BE: 1824 of 1825 days left
ERROR nothing.invalid: no certificate received
exit=2

The exit code is 0 when all is well, 1 for a warning and 2 when a server could not be reached. Most monitoring systems can run a command and alert on its exit code; a daily cron job also works:

0 7 * * * /usr/local/bin/check-expiry.sh www.example.com api.example.com:8443 || echo "certificate check failed" | mail -s "TLS expiry" ops@example.com

6. Choose when to alert

Alert on a share of the lifetime, not a fixed number of days. A rule such as “30 days before expiry” fires almost as soon as a 47 days certificate is issued. Let’s Encrypt’s integration guide recommends renewing “when they have a third of their total lifetime left” (halfway for certificates shorter than ten days), and ACME clients that support ARI, RFC 9773, on the IETF standards track, ask the CA when to renew. The script’s default of a sixth leaves time to fix a failed renewal before users notice. The calendar shows how often each lifetime must be renewed:

Try it: Lifetime calendarOpen the full tool

Runs in your browser. Nothing you type or paste is sent anywhere.

Start from

The schedule for public TLS certificates

Issued fromLongest lifetimeDomain check reuseRenewals a year, at least
1 September 2020398 days398 days (from 1 October 2021)1
15 March 2026200 days200 days2
15 March 2027100 days100 days4
15 March 202947 days10 days8

From the CA/Browser Forum Baseline Requirements (sections 6.3.2 and 4.2.1), checked 20 October 2026. Renewals a year: 365 divided by the lifetime, rounded up; renewing earlier means more.

Check it worked

Prove the alert path, not just the script. FRACTION=1 makes every certificate “too close”, so the run must warn and exit with 1:

FRACTION=1 bash check-expiry.sh www.rfc-editor.org; echo "exit=$?"
WARN  www.rfc-editor.org #0 CN=rfc-editor.org: 80 of 90 days left
...
exit=1

Wire that into your monitoring once and confirm the alert reaches the team that owns the certificate.

If something goes wrong

Why this works

The validity dates are part of the data the CA signs, and every client compares them with its own clock for every certificate in the path. One certificate out of date anywhere in the chain fails the whole connection. Reading notAfter from the live endpoint and from every store tells you exactly when that will happen. Inventory, expiry and renewal explains how to find every certificate and renew without an outage, Anatomy of an X.509 certificate shows where the dates sit, and ACME and the 47-day schedule explains how to automate renewal so that monitoring rarely has to fire.

Go deeper

Early edition. Checked against the standards; an independent expert review is in progress. Found a mistake? Tell us.

Last reviewed Edition 0.9