<!-- https://pkizerotohero.com/guides/check-certificate-expiry/ | Markdown mirror of the HTML page | PKI Zero to Hero, by Duyutech BV -->

> Check certificate expiry with openssl -enddate and -checkend on files, servers, Kubernetes, Java and Windows, plus a tested script that watches the chain.

Source: https://pkizerotohero.com/guides/check-certificate-expiry/ · Last reviewed: 2026-10-20

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](https://pkizerotohero.com/guides/#task-operate)

Platforms

- Linux
- macOS
- Windows
- Kubernetes
- Java

Who it is for

- [IT ops](https://pkizerotohero.com/start/#itops)
- [Developer](https://pkizerotohero.com/start/#developer)
- [Expert](https://pkizerotohero.com/start/#expert)

Level

Practitioner · 6 steps

## 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

```bash
openssl x509 -in old.crt -noout -startdate -enddate
```

```text
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](https://www.rfc-editor.org/rfc/rfc5280#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:

```bash
openssl x509 -in old.crt -noout -checkend 604800; echo "exit=$?"
openssl x509 -in old.crt -noout -checkend 86400; echo "exit=$?"
```

```text
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:

```bash
openssl s_client -connect www.rfc-editor.org:443 -servername www.rfc-editor.org </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -enddate
```

```text
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:

```bash
curl -sv -o /dev/null https://www.rfc-editor.org/ 2>&1 | grep -E 'start date|expire date'
```

```text
* 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`:

```bash
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
```

```text
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):

```bash
keytool -list -v -keystore www.p12 | grep -E 'Alias name|until:'
```

```text
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:

```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:

```bash
#!/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
bash check-expiry.sh www.rfc-editor.org letsencrypt.org 127.0.0.1:8445 nothing.invalid; echo "exit=$?"
```

```text
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:

```text
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](https://letsencrypt.org/docs/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:

*Interactive tool: Lifetime calendar (runs in the browser at [/tools/lifetime/](https://pkizerotohero.com/tools/lifetime/))*

## 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`:

```bash
FRACTION=1 bash check-expiry.sh www.rfc-editor.org; echo "exit=$?"
```

```text
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

- **`date: illegal option -- d` on macOS:** BSD `date` has different options. Install GNU coreutils and replace `date` with `gdate`.
- **The server shows an old certificate after renewal:** the service was not reloaded, or one node behind the load balancer still has the old file. Check each node by its address, with `-servername` set to the public name.
- **An intermediate is about to expire:** install the chain that came with your latest certificate ([How to build the full chain file](https://pkizerotohero.com/guides/build-fullchain/)). Clients report an expired link as [`certificate has expired`](https://pkizerotohero.com/errors/openssl-certificate-has-expired/), [`CERT_HAS_EXPIRED`](https://pkizerotohero.com/errors/node-cert-has-expired/), [Java’s `CertificateExpiredException`](https://pkizerotohero.com/errors/java-certificate-expired/) or [Go’s `certificate has expired or is not yet valid`](https://pkizerotohero.com/errors/go-x509-certificate-expired/).
- **Users see [`NET::ERR_CERT_DATE_INVALID`](https://pkizerotohero.com/errors/chrome-err-cert-date-invalid/) on a certificate that is in date:** check the clock on their device. A wrong clock makes a good certificate look expired or not yet valid.
- **`ERROR … no certificate received`:** DNS, a firewall, or a port that does not speak TLS directly. Test it with [How to check the certificate chain a server sends](https://pkizerotohero.com/guides/check-remote-certificate-chain/).

## 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](https://pkizerotohero.com/handbook/certificate-lifecycle/) explains how to find every certificate and renew without an outage, [Anatomy of an X.509 certificate](https://pkizerotohero.com/handbook/x509-certificates/) shows where the dates sit, and [ACME and the 47-day schedule](https://pkizerotohero.com/handbook/acme/) explains how to automate renewal so that monitoring rarely has to fire.

## Go deeper

- [RFC 5280, section 4.1.2.5](https://www.rfc-editor.org/rfc/rfc5280#section-4.1.2.5): the validity field and how dates are encoded.
- [OpenSSL `x509` manual](https://docs.openssl.org/master/man1/openssl-x509/): `-enddate`, `-checkend` and `-nameopt`.
- [Let’s Encrypt integration guide](https://letsencrypt.org/docs/integration-guide/): when to renew, and ACME Renewal Information.
- [Microsoft Learn: the Certificate provider](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.security/about/about_certificate_provider?view=powershell-5.1): `-ExpiringInDays` and other filters.
- [Kubernetes: TLS secrets](https://kubernetes.io/docs/concepts/configuration/secret/#tls-secrets): how certificates are stored in a cluster.

**Early edition.** Checked against the standards; an independent expert review is in progress. Found a mistake? [Tell us](https://pkizerotohero.com/contact/?topic=content-error&page=/guides/check-certificate-expiry/).

Last reviewed 20 October 2026Edition 0.9

## Keep going

### Learn why it works

- [Chapter 19: Inventory, expiry and renewal](https://pkizerotohero.com/handbook/certificate-lifecycle/)
- [Chapter 8: Anatomy of an X.509 certificate](https://pkizerotohero.com/handbook/x509-certificates/)
- [Chapter 20: ACME and the 47-day schedule](https://pkizerotohero.com/handbook/acme/)

### Error pages

- [verify error:num=10:certificate has expired](https://pkizerotohero.com/errors/openssl-certificate-has-expired/)
- [NET::ERR\_CERT\_DATE\_INVALID](https://pkizerotohero.com/errors/chrome-err-cert-date-invalid/)
- [CERT\_HAS\_EXPIRED](https://pkizerotohero.com/errors/node-cert-has-expired/)
- [CertPathValidatorException: validity check failed](https://pkizerotohero.com/errors/java-certificate-expired/)
- [x509: certificate has expired or is not yet valid](https://pkizerotohero.com/errors/go-x509-certificate-expired/)

### Tools

- [Lifetime calendar](https://pkizerotohero.com/tools/lifetime/): See how long a certificate may live on any date, and when to renew.
- [Certificate decoder](https://pkizerotohero.com/tools/decoder/): Paste a PEM certificate and read every field in plain words.

### Topics

- [Lifecycle and automation](https://pkizerotohero.com/topics/lifecycle-and-automation/)

## More guides: Operate and monitor

- [How to monitor Certificate Transparency for your domains](https://pkizerotohero.com/guides/monitor-certificate-transparency/)
- [How to prepare for short-lived certificates: a checklist](https://pkizerotohero.com/guides/prepare-47-day-certificates/)
