Skip to content
Commitpedia
Log in Join & edit

Check a site’s TLS expiry and security headers from the terminal

A bash script that connects to a domain, prints the certificate's issuer and expiry with days remaining, and reports which recommended security headers are present or missing. Read-only; needs only curl and openssl.

Drafted with AI from official documentation. Scripts were run in a test environment and repository links verified. Awaiting a human check. Did it work for you? Last edited Outdated?

Edit history of Check a site’s TLS expiry and security headers from the terminal: 1 recorded edit
Edit history · 1 edit
Type
Script
Edits
1
Editors
1
First issued
15 Oct 2026
Last edit
15 Oct 2026
Language
bash
Licence
CC0-1.0

What this changes

Nothing on your machine or the server. It makes one TLS connection and one HTTPS HEAD request to the site and prints the results.

How to undo it

Nothing to undo.

After setting up a reverse proxy, or when a certificate is about to expire somewhere, you want a quick answer: when does the certificate run out, who issued it, and does the site send the headers it should? This script answers in a few seconds and exits non-zero if the certificate expires within two weeks, so it can also run from cron or CI.

The script

#!/usr/bin/env bash
set -euo pipefail

usage() { echo "Usage: $(basename "$0") <hostname> [warn-days]" >&2; exit 2; }

host=${1:-}
warn_days=${2:-14}
[[ -n "$host" ]] || usage
host=${host#https://}
host=${host#http://}
host=${host%%/*}

echo "== TLS certificate for $host"
cert=$(echo | openssl s_client -servername "$host" -connect "$host:443" 2>/dev/null | openssl x509 -noout -subject -issuer -enddate) \
  || { echo "Could not fetch a certificate from $host:443" >&2; exit 1; }
printf '%s\n' "$cert"

end=$(printf '%s\n' "$cert" | sed -n 's/^notAfter=//p')
if end_epoch=$(date -d "$end" +%s 2>/dev/null); then
  :
else
  end_epoch=$(date -j -f '%b %d %T %Y %Z' "$end" +%s)
fi
now=$(date +%s)
days=$(( (end_epoch - now) / 86400 ))
echo "Days until expiry: $days"

status=0
if (( days < warn_days )); then
  echo "WARNING: certificate expires in less than $warn_days days"
  status=1
fi

echo
echo "== Security headers"
headers=$(curl -sSI -L --max-time 15 "https://$host/" | tr -d '\r')
for name in strict-transport-security content-security-policy x-content-type-options \
            x-frame-options referrer-policy permissions-policy; do
  if line=$(printf '%s\n' "$headers" | grep -i -m1 "^$name:"); then
    printf 'OK       %s\n' "$line"
  else
    printf 'MISSING  %s\n' "$name"
  fi
done

echo
echo "== Headers that reveal software"
printf '%s\n' "$headers" | grep -iE '^(server|x-powered-by):' || echo "none"

exit "$status"

How to run it

chmod +x check-site.sh
./check-site.sh example.com
./check-site.sh https://www.example.com/some/page    # scheme and path are stripped
./check-site.sh example.com 30                       # warn if fewer than 30 days remain
== TLS certificate for example.com
subject=CN=example.com
issuer=C=US, O=Let's Encrypt, CN=R11
notAfter=Jan  8 09:14:22 2027 GMT
Days until expiry: 85

== Security headers
OK       strict-transport-security: max-age=31536000; includeSubDomains
MISSING  content-security-policy
OK       x-content-type-options: nosniff
MISSING  x-frame-options
OK       referrer-policy: strict-origin-when-cross-origin
MISSING  permissions-policy

== Headers that reveal software
server: nginx

How it works

  • openssl s_client -servername opens a TLS connection with SNI, so the right certificate is returned on a server that hosts many names. The leading echo | closes the connection immediately once the handshake is done.
  • openssl x509 -noout -subject -issuer -enddate prints the fields from the certificate it received.
  • The expiry date is parsed with GNU date -d on Linux and, if that fails, with BSD date -j -f on macOS, so the same script runs on both.
  • curl -sSI -L fetches only the headers, following redirects, and tr -d '\r' removes the carriage returns HTTP lines end with. The loop greps for each recommended header, case-insensitively.
  • The exit code is 1 when the certificate is close to expiry, which lets a cron job or CI step fail loudly.

Variations

# Several sites from a list, one per line
while read -r site; do ./check-site.sh "$site" || echo "ATTENTION: $site"; done < sites.txt

# Just the expiry check, for cron (exits 1 if fewer than 14 days remain)
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
  | openssl x509 -noout -checkend $((14*24*3600)) || echo "example.com certificate expires soon"

# Full certificate chain details
echo | openssl s_client -servername example.com -connect example.com:443 -showcerts 2>/dev/null | head -n 40

The script checks the site the way a browser would see it, so it also catches a proxy that is up but serving the wrong certificate or missing headers after a configuration change. The openssl x509 -checkend option in the variations is the simplest building block if you only want expiry monitoring; see the openssl-x509 manual for its other options.

Did this work for you?

No votes yet. Be the first.

If it did not work, the report form below tells the editors what went wrong.

See also

  • 0019 fetch with timeout, retries and proper error handling A reusable fetchJson helper for Node 20+ and browsers that adds a timeout, retries with backoff for temporary failures, and clear errors… Intermediate · Step by step · Any system Script Async code · Making requests 15 Oct 2026
  • 0081 How an HTTP request works: methods, headers, and body An HTTP request is a short text message a client sends to a server: a method and path, some headers, and sometimes… Beginner · Explains an idea · Any system Article Making requests 15 Oct 2026
  • 0083 Test a JSON API with curl A runnable bash script showing the curl commands you need most when testing an API: GET, POST JSON, custom headers, Bearer tokens,… Beginner · Step by step · Linux, macOS, Windows Script Authentication · Making requests 15 Oct 2026
  • 0084 Tiny JSON echo server for testing HTTP clients A standard-library Python server that echoes back the method, path, query, headers, and body of every request, and can return any status… Intermediate · Step by step · Any system Script Files & automation · Making requests 15 Oct 2026
  • 0085 fetch with timeout, retries, and exponential backoff A drop-in fetch wrapper that times out slow requests, retries network errors and 429/5xx responses with jittered exponential backoff, and honors Retry-After. Advanced · Step by step · Any system, Browser Script Async code · Making requests 15 Oct 2026
  • 0087 CORS explained: why the browser blocks your request CORS is the browser rule that decides whether JavaScript on one origin may read a response from another. The server opts in… Intermediate · Explains an idea · Browser Article Async code · Making requests 15 Oct 2026

Repos in the wild

  • Requests: HTTP for humans psf/requests Requests is the classic Python library for calling web APIs and downloading pages, with a simple API for GET and POST requests, headers, JSON and sessions. GitHub
  • Axios axios/axios A promise-based HTTP client for browsers and Node.js with interceptors, automatic JSON handling, request cancellation and a consistent API in both environments. GitHub
  • curl and libcurl (curl/curl) curl/curl The source of curl, the command-line tool for transferring data with URLs, and libcurl, the C library behind it that many other programs use for HTTP. GitHub
  • Caddy: the web server with automatic HTTPS caddyserver/caddy Caddy is an open-source web server and reverse proxy written in Go that obtains and renews TLS certificates on its own, with a configuration file short enough to memorize and HTTP/1, 2 and 3 support out of the box. GitHub

History

Every saved edit is kept. The plot on the sleeve draws one line per edit. Open any edit to see exactly what changed.

  1. Dominik +4,596 chars

Report a problem

Something wrong, unsafe or copied? Tell the editors.
What is wrong?