Check if certificate file expires in n days

Summary

If you just want to know whether the certificate has expired (or will do so within the next N seconds), the -checkend <seconds> option to openssl x509 will tell you:

# 1 day in seconds
let CertificateExpirationWarningTrigger=60*60*24

if openssl x509 -checkend ${CertificateExpirationWarningTrigger} -noout -in mycert.pem
then
  echo "Certificate is good for another day!"
else
  echo "Certificate has expired or will do so within 24 hours!"
  echo "(or is invalid/not found)"
fi

This saves having to do date/time comparisons yourself.

openssl will return an exit code of 0 (zero) if the certificate has not expired and will not do so for the next 86400 seconds, in the example above. If the certificate will have expired or has already done so - or some other error like an invalid/nonexistent file - the return code is 1.

(Of course, it assumes the time/date is set correctly)

You can also query the end date of a certificate like this:

$ openssl x509 -enddate -noout -in mycert.pem
notAfter=May 22 06:53:50 2021 GMT

# Convert it to ISO date
$ date --date="$(openssl x509 -enddate -noout -in mycert.pem |cut -d= -f 2)" --iso-8601
2021-05-22

Here’s my bash command line to list multiple certificates in order of their expiration, most recently expiring first.

for pem in /etc/ssl/certs/*.pem; do
  printf '%s: %s\n' \
    "$(date --date="$(openssl x509 -enddate -noout -in "$pem"|cut -d= -f 2)" --iso-8601)" \
    "$pem"
done | sort

Sample output:

2015-12-16: /etc/ssl/certs/Staat_der_Nederlanden_Root_CA.pem
2016-03-22: /etc/ssl/certs/CA_Disig.pem
2016-08-14: /etc/ssl/certs/EBG_Elektronik_Sertifika_Hizmet_S.pem

Source:

Docker implementation

The following example shows how this is implemented in our docker images. If a certificate is inside the expiration range the following message will be logged by our container entrypoint script which in turn will be picked up by our log collector so that we can take action:

{
  "level": "warning",
  "msg": "Certificate will expire soon",
  "certificate": "mongodb-prod.pfx",
  "enddate": "May 5 13:41:49 2020 GMT"
}

/app/certs/extract-pfx.sh

#!/bin/sh

help="
Usage: $(basename $0) <pfx-path> <passphrase> [env-var-prefix]

"

check_errs()
{
  # Function. Parameter 1 is the return code
  # Para. 2 is text to display on failure.
  if [ "${1}" -ne "0" ]; then
    echo "ERROR: ${2}, exiting"
    # as a bonus, make our script exit with the right error code.
    exit ${1}
  fi
}

PFX_FILE_PATH="${1:?"${help} <pfx-path> missing - Provide path to pfx file"}"
PFX_PASSWORD="${2:?"${help} <passphrase> missing - Specify password to decrypt pfx"}"
ENV_VAR_PREFIX="${3}"

# ${variable%%pattern}
# Trim the longest match from the end
PFX_BASE_NAME="${PFX_FILE_PATH%%.*}"

EXTRACT_CERT_PATH="${PFX_BASE_NAME}-cert.pem"
EXTRACT_KEY_PATH="${PFX_BASE_NAME}-key.pem"
EXTRACT_CA_PATH="${PFX_BASE_NAME}-ca-chain.pem"

echo "
Exporting:

Cert:     ${EXTRACT_CERT_PATH}
Key:      ${EXTRACT_KEY_PATH}
CA Chain: ${EXTRACT_CA_PATH}
"

openssl pkcs12 -in "${PFX_FILE_PATH}" -nocerts -nodes -passin pass:"${PFX_PASSWORD}" | openssl rsa -out "${EXTRACT_KEY_PATH}"
check_errs $? "Private key export failed"

openssl pkcs12 -in "${PFX_FILE_PATH}" -nokeys -clcerts -passin pass:"${PFX_PASSWORD}" | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > "${EXTRACT_CERT_PATH}"
check_errs $? "Cert export failed"

openssl pkcs12 -in "${PFX_FILE_PATH}" -cacerts -nokeys -chain -passin pass:"${PFX_PASSWORD}" | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > "${EXTRACT_CA_PATH}"
check_errs $? "CA chain export failed"

# 30 days in seconds
let CertificateExpirationWarningThreshold=60*60*24*30

if ! openssl x509 -checkend ${CertificateExpirationWarningThreshold} -noout -in ${EXTRACT_CERT_PATH} > /dev/null
then
  ENDDATE=$(openssl x509 -enddate -noout -in ${EXTRACT_CERT_PATH} | cut -d= -f 2)
  echo "{'level':'warning','msg':'Certificate will expire soon','certificate':'${PFX_FILE_PATH}','enddate':'${ENDDATE}'}" | tr "'" "\""
fi

if [ ! -z "${ENV_VAR_PREFIX}" ]
then
  export "${ENV_VAR_PREFIX}_CERT_FILE"=${EXTRACT_CERT_PATH}
  export "${ENV_VAR_PREFIX}_KEY_FILE"=${EXTRACT_KEY_PATH}
  export "${ENV_VAR_PREFIX}_CA_FILE"=${EXTRACT_CA_PATH}
fi

/app/docker-entrypoint.sh

#!/bin/sh

set -e

echo "--- Start pre-flight tasks ---"

if [ -z "${MONGODB_PFX_FILE}" ] || [ -z "${MONGODB_PFX_PASSWORD}" ]; then
  echo ' [MONGODB] one or more variables for PFX extraction are undefined, skipping certificate dump ...'

  else
    cd ./certs
    source ./extract-pfx.sh "${MONGODB_PFX_FILE}" "${MONGODB_PFX_PASSWORD}" "MONGODB"
    cd ..
fi

if [ -z "${SOMEOTHER_PFX_FILE}" ] || [ -z "${SOMEOTHER_PFX_PASSWORD}" ]; then
  echo ' [SOMEOTHER] one or more variables for PFX extraction are undefined, skipping certificate dump ...'

  else
    cd ./certs
    source ./extract-pfx.sh "${SOMEOTHER_PFX_FILE}" "${SOMEOTHER_PFX_PASSWORD}" "SOMEOTHER"
    cd ..
fi

# some more code
# [...]

echo "--- End pre-flight tasks ---"

exec dockerize "$@"