Monitoring TLS Certificate Expiry with Bash: openssl Scripts
AI generated
$_
#!/
Bash · TLS · Security · Monitoring
Monitoring TLS Certificate Expiry with Bash
From openssl s_client to an alerting script

An expired TLS certificate is one of the few failures that can take a shop completely offline without a single code bug behind it. Monitoring TLS certificate expiry with Bash and openssl catches looming outages weeks in advance and prevents the midnight emergency call.

17 min read openssl s_client · x509 · cron Bash 4.x/5.x · Linux · Nagios/Icinga

1. Why monitoring TLS certificate expiry is standard practice

An expired TLS certificate differs from most other production failures in that there is no early warning through error messages in the application code. The browser suddenly shows a security warning, APIs refuse the connection, and the failure occurs at exactly a predictable point in time, namely the expiry date. Anyone who establishes monitoring TLS certificate expiry as a fixed part of operations turns this predictable but easily overlooked failure into a plannable maintenance event.

This is especially critical for automated certificates via Let's Encrypt with a short 90 day lifetime: a failed renewal cron job often only becomes apparent once the certificate has already expired. A separate monitoring script that monitors TLS certificate expiry, independent of the renewal process itself, catches exactly this case, because it checks the actual, externally visible validity, not just whether the renewal command completed without error.

Monitoring is also important for manually managed certificates with longer lifetimes (one year or more), because it is precisely the rare repetition that causes the renewal process to be forgotten. A script that monitors TLS certificate expiry for all relevant domains and warns in time replaces relying on individual employees' calendar reminders with a reliable, automated process.

2. Fetching an endpoint's certificate with openssl s_client

The core of any script that monitors TLS certificate expiry is the command openssl s_client -connect host:443 -servername host. The -servername option is mandatory for servers using Server Name Indication (SNI), because without it a default certificate is often returned instead of the one actually configured for the domain, leading to incorrect results.

Since s_client is interactive and waits for input, you redirect /dev/null as stdin and explicitly close the connection with the -quiet option or via timeout. The output is piped to openssl x509 -noout -enddate, which returns only the expiry date in the format notAfter=Mon DD HH:MM:SS YYYY GMT.


#!/usr/bin/env bash
# fetch-cert-expiry.sh — fetch a certificate's expiry date via openssl
set -euo pipefail

HOST="${1:?Usage: fetch-cert-expiry.sh <host> [port]}"
PORT="${2:-443}"

expiry_raw=$(echo | timeout 5 openssl s_client -connect "$HOST:$PORT" -servername "$HOST" 2>/dev/null \
  | openssl x509 -noout -enddate 2>/dev/null) || {
  echo "[ERROR] Could not retrieve certificate for $HOST:$PORT" >&2
  exit 1
}

# expiry_raw looks like: notAfter=Aug 15 12:00:00 2026 GMT
expiry_date="${expiry_raw#notAfter=}"
echo "[INFO] Certificate for $HOST expires on: $expiry_date"

3. Extracting the expiry date and converting it to days

An expiry date as text is of little help to a monitoring script, what matters is the number of remaining days. Bash itself has no built-in date arithmetic, but date -d "$expiry_date" +%s converts the date into a Unix timestamp, which you then compare to the current timestamp. Important: this syntax works with GNU date (Linux); on macOS with BSD date the option syntax differs (date -j -f).

For a script that monitors TLS certificate expiry and needs to run portably between Linux and macOS, it is worth adding a small detection logic that picks the right date syntax. In most production environments with Linux servers, though, the GNU date variant is entirely sufficient.


#!/usr/bin/env bash
# days-until-expiry.sh — convert a certificate expiry date to remaining days
set -euo pipefail

HOST="${1:?Usage: days-until-expiry.sh <host>}"

expiry_raw=$(echo | timeout 5 openssl s_client -connect "$HOST:443" -servername "$HOST" 2>/dev/null \
  | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)

expiry_epoch=$(date -d "$expiry_raw" +%s)
now_epoch=$(date +%s)
days_remaining=$(( (expiry_epoch - now_epoch) / 86400 ))

echo "[INFO] $HOST: certificate expires in $days_remaining day(s) ($expiry_raw)"

if (( days_remaining < 0 )); then
  echo "[CRITICAL] Certificate for $HOST has already expired!" >&2
  exit 2
fi

4. Checking multiple domains at once

In practice, a script that monitors TLS certificate expiry rarely runs for just a single domain. A typical setup includes the main domain, several subdomains for staging and API, and domains from partner services. A domain list in a file combined with a Bash loop processes all endpoints in one pass and collects the results in a structured overview.

For faster execution with many domains, parallelization with background processes pays off, similar to other Bash automation tasks: each check is independent of the others, which is why several openssl s_client calls can easily run simultaneously without the results interfering with each other.


#!/usr/bin/env bash
# check-multiple-domains.sh — monitor certificate expiry for a domain list
set -euo pipefail

DOMAIN_FILE="${1:?Usage: check-multiple-domains.sh <domain-list-file>}"

check_domain() {
  local host="$1"
  local expiry_raw expiry_epoch now_epoch days_remaining

  expiry_raw=$(echo | timeout 5 openssl s_client -connect "$host:443" -servername "$host" 2>/dev/null \
    | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2) || {
    printf '%-30s ERROR (unreachable or no cert)\n' "$host"
    return 1
  }

  expiry_epoch=$(date -d "$expiry_raw" +%s)
  now_epoch=$(date +%s)
  days_remaining=$(( (expiry_epoch - now_epoch) / 86400 ))
  printf '%-30s %4d days remaining (%s)\n' "$host" "$days_remaining" "$expiry_raw"
}

while IFS= read -r domain; do
  [[ -z "$domain" || "$domain" == \#* ]] && continue
  check_domain "$domain" &
done < "$DOMAIN_FILE"

wait

5. Thresholds and Nagios-compatible exit codes

For integration into monitoring systems like Nagios or Icinga, a clear exit code convention is decisive: 0 for OK, 1 for warning, 2 for critical, 3 for unknown. A script that monitors TLS certificate expiry therefore defines two thresholds, for example 30 days for warning and 7 days for critical, and returns the matching exit code along with meaningful text output that the monitoring system displays as a status message.

This convention makes the script immediately integrable into existing monitoring infrastructure without developing a special plugin. Nagios-compatible systems additionally expect performance data in the format | days_remaining=42 at the end of the output, which enables trend graphs over time.


#!/usr/bin/env bash
# check-cert-nagios.sh — Nagios-compatible certificate expiry check
set -euo pipefail

HOST="${1:?Usage: check-cert-nagios.sh <host>}"
WARN_DAYS="${2:-30}"
CRIT_DAYS="${3:-7}"

expiry_raw=$(echo | timeout 5 openssl s_client -connect "$HOST:443" -servername "$HOST" 2>/dev/null \
  | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2) || {
  echo "UNKNOWN: could not retrieve certificate for $HOST"
  exit 3
}

expiry_epoch=$(date -d "$expiry_raw" +%s)
now_epoch=$(date +%s)
days_remaining=$(( (expiry_epoch - now_epoch) / 86400 ))

if (( days_remaining < 0 )); then
  echo "CRITICAL: certificate for $HOST expired $(( -days_remaining )) day(s) ago | days_remaining=$days_remaining"
  exit 2
elif (( days_remaining < CRIT_DAYS )); then
  echo "CRITICAL: certificate for $HOST expires in $days_remaining day(s) | days_remaining=$days_remaining"
  exit 2
elif (( days_remaining < WARN_DAYS )); then
  echo "WARNING: certificate for $HOST expires in $days_remaining day(s) | days_remaining=$days_remaining"
  exit 1
else
  echo "OK: certificate for $HOST valid for $days_remaining more day(s) | days_remaining=$days_remaining"
  exit 0
fi

6. Notifications before an impending expiry

Not every team runs Nagios or Icinga, many prefer direct notifications via Slack, email, or Microsoft Teams. A script that monitors TLS certificate expiry and falls below a threshold then sends a structured message with domain, remaining days, and exact expiry date to a webhook, instead of just returning an exit code.

It is important to avoid repeated notifications when a script runs daily and a certificate has been in the warning range for days: a simple state file that stores the last day a notification was sent prevents the team from receiving the same message every day and eventually ignoring it.

Tool Setup Effort Chain Checking Recommendation
openssl s_client + x509 Minimal, available everywhere Yes, with -showcerts Default choice for Bash monitoring
curl -vI Minimal Limited Quick visual check
Specialized tools (e.g. check_ssl_cert) Extra installation Very extensive Large Nagios environments
SaaS monitoring (external) Contract/cost Yes When no own infrastructure is desired

Mironsoft

Shell automation, TLS monitoring and deployment infrastructure

No more surprise certificate outages?

We set up Bash-based TLS certificate monitoring for all your domains, integrate it into Nagios, Icinga, or Slack, and make sure renewal failures do not surface at the customer first.

Monitoring Scripts

openssl-based checks with thresholds for all domains

Integration

Wiring into Nagios, Icinga, Slack, or email notifications

Renewal Safety Net

Independent check of the actual certificate status, not just the cron job exit code

7. Checking certificate chains and intermediate certificates

An end certificate can be valid itself while an intermediate certificate in the chain has expired or is missing, which causes errors in some clients while others tolerate it. Anyone monitoring TLS certificate expiry should therefore not only check the end certificate but retrieve the full chain with openssl s_client -showcerts and examine each individual certificate for its own expiry date.

In practice this matters especially for self-hosted certificates where intermediate certificates need manual updates. Automated certificates via Let's Encrypt and most managed certificate services from major cloud providers update the entire chain automatically, which makes this problem rarer but not impossible.

8. Integration into cron and CI for daily checks

A script that monitors TLS certificate expiry belongs in a daily cron job, not a manual checklist. A run at 6 AM with output to a log file and notification on threshold breach is entirely sufficient for most setups. For teams with existing CI infrastructure, the same check can also run as a scheduled pipeline job (scheduled pipeline in GitLab CI, cron trigger in GitHub Actions), keeping monitoring configuration and deployment configuration in the same repository.

An additional benefit of CI integration: historical runs remain traceable in the pipeline logs without maintaining a separate logging infrastructure. For production-critical domains, redundant monitoring via two independent paths, for example cron and CI pipeline at the same time, is still recommended so that a failure of one system does not become a blind spot.

9. openssl vs. curl vs. specialized tools compared

openssl s_client remains the most flexible and widely used method to monitor TLS certificate expiry, because it is available on practically every system and offers complete control over the connection. curl -vI https://host also delivers certificate information, but in a format that is harder to parse robustly than the clear x509 -noout -enddate output from openssl.

Specialized tools like check_ssl_cert for Nagios offer additional checks such as key length, supported cipher suites, and revocation status, which can matter for security-critical environments. For pure expiry monitoring, the core of any script that monitors TLS certificate expiry, a self-written openssl-based script is often sufficient and considerably easier to maintain than an extra tool dependency.

10. Summary

Monitoring TLS certificate expiry with Bash means combining openssl s_client and x509 -noout -enddate, converting the result into remaining days, and checking it against defined thresholds. For multiple domains, a loop with optional parallelization scales, for monitoring systems Nagios-compatible exit codes provide the necessary integration, and for teams without Nagios, webhook notifications do the same job directly.

The decisive advantage of such a script lies in its independence from the actual renewal process: it checks the actually, externally visible validity, not just whether a cron job completed without error. Anyone who establishes monitoring TLS certificate expiry as a daily, automated routine prevents one of the most avoidable causes of production outages.

Monitoring TLS Certificate Expiry with Bash — The Essentials at a Glance

Fetching

openssl s_client -servername HOST followed by x509 -noout -enddate for the expiry date.

Conversion

date -d "$expiry" +%s minus the current timestamp, divided by 86400 for remaining days.

Thresholds

Nagios-compatible exit codes (0/1/2/3) for warning at 30 and critical at 7 days.

Automation

Daily cron job or CI scheduled pipeline, independent of the renewal process itself.

11. FAQ: Monitoring TLS Certificate Expiry with Bash

1Isn't monitoring the renewal job enough?
A job can run without error and still leave a bad certificate. Independent external checking catches that.
2Why is -servername important?
Without SNI the server often returns the wrong default certificate instead of the one configured for the domain.
3Convert expiry date to days?
Convert with date -d into a Unix timestamp, subtract current timestamp, divide by 86400.
4Which exit codes for Nagios?
0 OK, 1 warning, 2 critical, 3 unknown. Performance data with | days_remaining=N for trends.
5Check many domains performantly?
Parallelize with background processes, each check is independent, wait collects the results.
6Check intermediate certificates?
openssl s_client -showcerts retrieves the full chain, each certificate checkable individually.
7Avoid daily repeat alerts?
A state file with the last notified day prevents duplicate alerts.
8Cron job or also CI?
A cron job is usually enough. For critical domains, run redundantly via cron and CI pipeline at once.
9curl -vI instead of openssl?
Provides similar data but is harder to parse robustly than the clear openssl x509 enddate output.
10Difference to check_ssl_cert?
Specialized tools additionally check key length and revocation. For pure expiry checks a self-written script suffices.