Tips & Tricks: Notifica della Scadenza dei Certificati TLS con OpenSSL
Continuando l’argomento OpenSSL, utilizzando quando già descritto nell’articolo Tips & Tricks: Verifica della Catena di Certificati SSL/TLS con OpenSSL descriveremo come automatizzare la verifica di validità dei certificati TLS applicata ai siti WEB.
Da chi possiede un singolo sito web fino alle aziende che ne gestiscono molti ricorre la difficoltà di tenere traccia della scadenza dei certificati TLS (https) per evitare che il sito web mostri all’utente la pagina di sito “non sicuro”, cosa che non giova alla credibilità di ciò che si vuole condividere.
Questo script, eseguito a mezzo crontab, verifica periodicamente che i certificati dei siti web, indicati negli array (chiamati GroupA, GroupB…GroupN) rientrino nel periodo di validità ed eventualmente notifichi a mezzo email quali si approssimino alla scadenza. Si dimostra essere utile quando ci si “dimentica” della data di scadenza del certificato oppure quando, per qualsiasi motivo la procedura di rinnovo “automatizzata” (che sarà descritta in un prossimo articolo) fallisca il compito.
# ------------------------------------------------------------------------------
# - File Name : check-web-certs-expiry.sh
# - Author : Nicola Montemurro
# - Administrator : Nicola Montemurro - Mobile: 335 -
# - Create : 30/04/2026
# - Last Update : 01/05/2026
# - Description : script ...
# - Position : /usr/local/scripts
# - Note : NON modificare senza AUTORIZZAZIONE
# -----------------------------------------------------------------------------
#!/usr/bin/env bash
set -euo pipefail
# Configuration
DAYBEFOREALERT=5
MAILTO="sysadmin@mydomain.it" ### set to email address to send report (requires mailx)
SMTP="smtp.myotherdomain.com" ### unused in this script; kept for compatibility
CONNECT_TIMEOUT=5 ### seconds for openssl TCP connect (uses timeout)
OPENSSL_TIMEOUT=5 ### seconds to limit openssl time (uses timeout)
TODAYEPOCH=$(date -u +%s)
### Hosts groups
declare -a groupA=("www.solutiontech.it")
declare -a groupB=("solutioncafe.it")
declare -a groupC=("www.nicolamontemurro.it" "blog.nicolamontemurro.it")
declare -a groupD=("www.nicolamontemurro.com" "blog.nicolamontemurro.com")
declare -a groups=("groupA" "groupB" "groupC" "groupD")
ALERTS=()
WARNS=()
### helper: fetch certificate once, return text or empty on failure
fetch_cert_text() {
local host=$1
### Use timeout to avoid hangs; capture only the leaf certificate text via openssl x509
### We start a TCP connection and pipe into openssl x509 to get parsed cert.
if CERT_TEXT=$(timeout "${CONNECT_TIMEOUT}" openssl s_client -connect "${host}:443" -servername "${host}" </dev/null 2>/dev/null | timeout "${OPENSSL_TIMEOUT}" openssl x509 -noout -text 2>/dev/null)
then
printf '%s' "$CERT_TEXT"
return 0
else
return 1
fi
}
for group in "${groups[@]}"
do
lst="${group}[@]"
for host in "${!lst}"
do
CERT_TEXT=""
if ! CERT_TEXT=$(fetch_cert_text "$host")
then
WARNS+=("WARN: cannot retrieve certificate for ${host}")
continue
fi
### Issuer: prefer openssl -noout -ISSUER but we already have parsed text
ISSUER=$(printf '%s\n' "$CERT_TEXT" | awk -F'ISSUER=' '/ISSUER=/{print $2; exit}' || true)
ISSUER=${ISSUER:-unknown}
### Not After: try to extract 'Not After' line from parsed cert text
NOTAFTER_RAW=$(printf '%s\n' "$CERT_TEXT" | awk -F'Not After :' '/Not After/{gsub(/^[ \t]+/,""); print $2; exit}' || true)
### Fallback to enddate format
if [[ -z "$NOTAFTER_RAW" ]]
then
NOTAFTER_RAW=$(openssl x509 -noout -enddate <<<"$CERT_TEXT" 2>/dev/null | sed 's/notAfter=//g' || true)
fi
if [[ -z "$NOTAFTER_RAW" ]]
then
WARNS+=("WARN: cannot extract NotAfter for ${host}")
continue
fi
echo $NOTAFTER_RAW $host
### SAN extraction: collect DNS entries into a comma-separated list
ALTNAME=$(printf '%s\n' "$CERT_TEXT" | awk '/Subject Alternative Name:/ {flag=1; next} /^X509v3/ { if(flag){flag=0} } flag {print}' | tr -d ' ' | sed 's/DNS:/\nDNS:/g' | sed -n 's/^DNS://p' | paste -sd, -)
ALTNAME=${ALTNAME:-unknown}
### Convert NotAfter to epoch (GNU date required)
if ! NOTAFTER_EPOCH=$(date -u -d "$NOTAFTER_RAW" +%s 2>/dev/null)
then
WARNS+=("WARN: cannot parse NotAfter date for ${host}: '${NOTAFTER_RAW}'")
continue
fi
DELTA=$(( NOTAFTER_EPOCH - TODAYEPOCH ))
DAYLEFT=$(( DELTA / 86400 ))
if (( DAYLEFT < 0 ))
then
ALERTS+=("EXPIRED: ${host} expired $((-DAYLEFT)) day(s) ago on ${NOTAFTER_RAW} (Issuer: ${ISSUER}, SAN: ${ALTNAME})")
elif (( DAYLEFT <= DAYBEFOREALERT ))
then
ALERTS+=("ALERT: ${host} expires in ${DAYLEFT} day(s) on ${NOTAFTER_RAW} (Issuer: ${ISSUER}, SAN: ${ALTNAME})")
fi
done
done
### Output print warnings and alerts
if ((${#WARNS[@]}))
then
printf '%s\n' "${WARNS[@]}" >&2
fi
### Send warning email
if ((${#WARNS[@]}))
then
report=$(printf '%s\n' "${WARNS[@]}")
printf '%s\n' "$report"
### Send email if configured and mailx is available
if [[ -n "$MAILTO" ]]
then
if command -v mailx >/dev/null 2>&1
then
printf '%s\n' "$report" | mailx -s "Certificate WARNS" "$MAILTO"
else
printf 'WARN: MAILTO set but mailx not found; skipping email\n' >&2
fi
fi
fi
### send alert email
if ((${#ALERTS[@]}))
then
report=$(printf '%s\n' "${ALERTS[@]}")
printf '%s\n' "$report"
### Send email if configured and mailx is available
if [[ -n "$MAILTO" ]]
then
if command -v mailx >/dev/null 2>&1
then
printf '%s\n' "$report" | mailx -s "Certificate ALERTS" "$MAILTO"
else
printf 'WARN: MAILTO set but mailx not found; skipping email\n' >&2
fi
fi
exit 2
else
printf 'OK: no certificates expiring within %s days\n' "$DAYBEFOREALERT"
exit 0
fi