TLS certificate expiry causes outages that are entirely preventable. A certificate management script monitors expiry dates across all domains, alerts with configurable lead time, and can trigger automated renewals via Certbot — turning a recurring manual task into a zero-touch operation.
1Certificate expiry monitoring
BASH
#!/usr/bin/env bash
# cert_check.sh — TLS certificate expiry monitoring
set -euo pipefail
DOMAINS=(
"myapp.example.com"
"api.example.com"
"admin.example.com"
"staging.example.com"
)
WARN_DAYS=30
CRITICAL_DAYS=14
ALERT_EMAIL="ops@example.com"
SLACK_URL="${SLACK_WEBHOOK_URL:-}"
ERRORS=0
get_expiry_days() {
local domain="$1"
local expiry_date
expiry_date=$(echo | timeout 5 openssl s_client \
-servername "${domain}" \
-connect "${domain}:443" 2>/dev/null | \
openssl x509 -noout -enddate 2>/dev/null | \
cut -d= -f2)
[[ -z "${expiry_date}" ]] && { echo "-1"; return; }
local expiry_epoch now_epoch
expiry_epoch=$(date -d "${expiry_date}" +%s 2>/dev/null || \
date -j -f "%b %d %H:%M:%S %Y %Z" "${expiry_date}" +%s)
now_epoch=$(date +%s)
echo $(( (expiry_epoch - now_epoch) / 86400 ))
}
echo "=== Certificate Expiry Report: $(date +%Y-%m-%d) ==="
printf "%-40s %10s %s\n" "Domain" "Days left" "Status"
printf "%.0s─" {1..60}; echo
for domain in "${DOMAINS[@]}"; do
days=$(get_expiry_days "${domain}")
if (( days < 0 )); then
printf "%-40s %10s %s\n" "${domain}" "ERROR" "⚠ Cannot check"
(( ERRORS++ ))
elif (( days <= CRITICAL_DAYS )); then
printf "%-40s %10d %s\n" "${domain}" "${days}" "✘ CRITICAL — renew immediately"
[[ -n "${SLACK_URL}" ]] && curl -s -X POST "${SLACK_URL}" \
-d "{\"text\":\"🔴 CRITICAL: ${domain} cert expires in ${days} days\"}" &>/dev/null
(( ERRORS++ ))
elif (( days <= WARN_DAYS )); then
printf "%-40s %10d %s\n" "${domain}" "${days}" "⚠ WARNING — renew soon"
[[ -n "${SLACK_URL}" ]] && curl -s -X POST "${SLACK_URL}" \
-d "{\"text\":\"🟡 WARNING: ${domain} cert expires in ${days} days\"}" &>/dev/null
else
printf "%-40s %10d %s\n" "${domain}" "${days}" "✔ OK"
fi
done
echo ""
(( ERRORS > 0 )) && { echo "Action required: ${ERRORS} domain(s)"; exit 1; }
echo "All certificates OK"
2Automated renewal with Certbot
BASH
#!/usr/bin/env bash
# cert_renew.sh — Automated Let's Encrypt renewal + service reload
set -euo pipefail
LOG="/var/log/cert-renewal.log"
SLACK_URL="${SLACK_WEBHOOK_URL:-}"
log() { echo "[$(date --iso-8601=seconds)] $*" | tee -a "${LOG}"; }
slack() { [[ -n "${SLACK_URL}" ]] && curl -s -X POST "${SLACK_URL}" \
-d "{\"text\":\"$*\"}" &>/dev/null; }
log "=== Certificate renewal check ==="
# ── Renew all certs approaching expiry ────────────────────
if certbot renew \
--quiet \
--non-interactive \
--pre-hook "systemctl stop nginx" \
--post-hook "systemctl start nginx" \
--deploy-hook "systemctl reload nginx" \
2>> "${LOG}"; then
log "Renewal check complete"
else
log "Renewal had errors — check ${LOG}"
slack "⚠ Certbot renewal errors on $(hostname) — check ${LOG}"
exit 1
fi
# ── Verify renewed certs ──────────────────────────────────
for domain in myapp.example.com api.example.com; do
days=$(echo | timeout 5 openssl s_client \
-servername "${domain}" -connect "${domain}:443" 2>/dev/null | \
openssl x509 -noout -enddate 2>/dev/null | \
cut -d= -f2 | xargs -I{} bash -c \
'echo $(( ($(date -d "{}" +%s) - $(date +%s)) / 86400 ))')
log " ${domain}: ${days} days remaining"
(( days < 7 )) && {
slack "🔴 CRITICAL: ${domain} still expiring in ${days} days after renewal!"
exit 1
}
done
log "All certificates verified"
slack "✅ Certificates renewed and verified on $(hostname)"
# Cron entry:
# 0 3 * * * /opt/scripts/cert_renew.sh >> /var/log/cert-renewal.log 2>&1
vriddh@prod-01:~/scripts$./cert_check.sh
=== Certificate Expiry Report: 2026-05-01 ===
Domain Days left Status
myapp.example.com 87 ✔ OK
api.example.com 87 ✔ OK
admin.example.com 22 ⚠ WARNING — renew soon
staging.example.com 8 ✘ CRITICAL — renew immediately
Action required: 2 domain(s)
█
✔ Certificate management rules — Check expiry with
openssl s_client and openssl x509 -noout -enddate — this tests the live certificate actually being served, not just the file on disk. Set WARN at 30 days and CRITICAL at 14 days to give ample renewal time. Use certbot renew --pre-hook and --post-hook for standalone mode (when nginx is stopped for renewal) or --deploy-hook for webroot mode (nginx stays running). Schedule renewal checks daily — Certbot only renews when expiry is within 30 days.