Initial template: TLS cert expiry checker
This commit is contained in:
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# user secrets / local overrides
|
||||||
|
config/cert-expiry.conf
|
||||||
|
config/domains.txt
|
||||||
|
config/msmtprc
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# editor / os noise
|
||||||
|
.DS_Store
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
22
README.md
Normal file
22
README.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# TLS Cert Expiry Check (Template)
|
||||||
|
|
||||||
|
Minimal, POSIX-friendly TLS certificate expiry checker using `openssl s_client`.
|
||||||
|
|
||||||
|
## What this is
|
||||||
|
- `git clone && run` template repo
|
||||||
|
- No real domains/emails/credentials committed
|
||||||
|
- Example configs included
|
||||||
|
- Easy to integrate into cron / CI / monitoring
|
||||||
|
|
||||||
|
## Repo structure
|
||||||
|
|
||||||
|
```text
|
||||||
|
cert-expiry-check-template/
|
||||||
|
├── bin/
|
||||||
|
│ └── cert-expiry-check.sh
|
||||||
|
├── config/
|
||||||
|
│ ├── domains.example.txt
|
||||||
|
│ ├── cert-expiry.conf.example
|
||||||
|
│ └── msmtprc.example
|
||||||
|
├── .gitignore
|
||||||
|
└── README.md
|
||||||
214
bin/cert-expiry-check.sh
Executable file
214
bin/cert-expiry-check.sh
Executable file
@@ -0,0 +1,214 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# cert-expiry-check.sh - POSIX-friendly TLS cert expiry checker template
|
||||||
|
# Fetches certificate notAfter via openssl s_client, computes days left, prints status lines.
|
||||||
|
#
|
||||||
|
# Exit codes:
|
||||||
|
# 0 = all OK
|
||||||
|
# 1 = at least one WARN (expiring soon)
|
||||||
|
# 2 = at least one EXPIRED or ERROR
|
||||||
|
# 3 = usage/config error
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
# -------------------- defaults (override via config) --------------------
|
||||||
|
WARN_DAYS="${WARN_DAYS:-30}"
|
||||||
|
TIMEOUT="${TIMEOUT:-12}"
|
||||||
|
FORCE_SEND="${FORCE_SEND:-0}"
|
||||||
|
ENABLE_EMAIL="${ENABLE_EMAIL:-0}"
|
||||||
|
|
||||||
|
DOMAINS_FILE="${DOMAINS_FILE:-./config/domains.txt}"
|
||||||
|
CONF_FILE="${CONF_FILE:-./config/cert-expiry.conf}"
|
||||||
|
MSMTP_CONFIG="${MSMTP_CONFIG:-./config/msmtprc}"
|
||||||
|
LOG_FILE="${LOG_FILE:-}" # empty => no logfile
|
||||||
|
|
||||||
|
RECIPIENT="${RECIPIENT:-}"
|
||||||
|
SENDER="${SENDER:-}"
|
||||||
|
|
||||||
|
CHECK_ONLY=0
|
||||||
|
|
||||||
|
# -------------------- helpers --------------------
|
||||||
|
die() { echo "ERROR: $*" >&2; exit 3; }
|
||||||
|
|
||||||
|
have_cmd() { command -v "$1" >/dev/null 2>&1; }
|
||||||
|
|
||||||
|
log() {
|
||||||
|
ts="$(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
|
line="$ts $*"
|
||||||
|
echo "$line"
|
||||||
|
if [ -n "$LOG_FILE" ]; then
|
||||||
|
printf '%s\n' "$line" >>"$LOG_FILE" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'USAGE'
|
||||||
|
Usage: cert-expiry-check.sh [--check-only] [--domains FILE] [--config FILE]
|
||||||
|
--check-only Do not send email, just print results and exit with status policy
|
||||||
|
--domains FILE Use a custom domains file (default: ./config/domains.txt)
|
||||||
|
--config FILE Use a custom config file (default: ./config/cert-expiry.conf)
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------------------- args --------------------
|
||||||
|
while [ "${1:-}" != "" ]; do
|
||||||
|
case "$1" in
|
||||||
|
--check-only) CHECK_ONLY=1 ;;
|
||||||
|
--domains) shift; [ -n "${1:-}" ] || die "--domains requires a file"; DOMAINS_FILE="$1" ;;
|
||||||
|
--config) shift; [ -n "${1:-}" ] || die "--config requires a file"; CONF_FILE="$1" ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) die "Unknown arg: $1" ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
# -------------------- load config (optional) --------------------
|
||||||
|
if [ -f "$CONF_FILE" ]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$CONF_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -------------------- validate --------------------
|
||||||
|
have_cmd openssl || die "openssl not found"
|
||||||
|
have_cmd date || die "date not found"
|
||||||
|
[ -f "$DOMAINS_FILE" ] || die "Domains file not found: $DOMAINS_FILE"
|
||||||
|
|
||||||
|
# date string -> epoch (GNU date)
|
||||||
|
to_epoch() {
|
||||||
|
# input like: "Jul 5 12:00:00 2026 GMT"
|
||||||
|
date -u -d "$1" +%s 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_target() {
|
||||||
|
# host or host:port -> host:port (default 443)
|
||||||
|
case "$1" in
|
||||||
|
*:*) printf '%s\n' "$1" ;;
|
||||||
|
*) printf '%s:443\n' "$1" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
get_notafter() {
|
||||||
|
hp="$1"
|
||||||
|
host="${hp%:*}"
|
||||||
|
port="${hp##*:}"
|
||||||
|
|
||||||
|
# Use timeout if available, else best-effort.
|
||||||
|
if have_cmd timeout; then
|
||||||
|
timeout "$TIMEOUT" openssl s_client -servername "$host" -connect "$host:$port" </dev/null 2>/dev/null \
|
||||||
|
| openssl x509 -noout -enddate 2>/dev/null \
|
||||||
|
| sed 's/^notAfter=//'
|
||||||
|
else
|
||||||
|
openssl s_client -servername "$host" -connect "$host:$port" </dev/null 2>/dev/null \
|
||||||
|
| openssl x509 -noout -enddate 2>/dev/null \
|
||||||
|
| sed 's/^notAfter=//'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
send_email() {
|
||||||
|
# Returns 0 on success, non-zero on failure.
|
||||||
|
have_cmd msmtp || return 1
|
||||||
|
[ -n "$RECIPIENT" ] || return 1
|
||||||
|
[ -n "$SENDER" ] || return 1
|
||||||
|
[ -f "$MSMTP_CONFIG" ] || return 1
|
||||||
|
|
||||||
|
subject="[cert-expiry] warn=$warn_count expired=$expired_count error=$error_count"
|
||||||
|
{
|
||||||
|
printf 'From: %s\n' "$SENDER"
|
||||||
|
printf 'To: %s\n' "$RECIPIENT"
|
||||||
|
printf 'Subject: %s\n' "$subject"
|
||||||
|
printf '\n'
|
||||||
|
printf '%s\n' "$summary_line"
|
||||||
|
printf '\n'
|
||||||
|
# mail_body contains \n escapes
|
||||||
|
printf "%b" "$mail_body"
|
||||||
|
} | msmtp -C "$MSMTP_CONFIG" -t
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------------------- run --------------------
|
||||||
|
now_epoch="$(date -u +%s)"
|
||||||
|
|
||||||
|
ok_count=0
|
||||||
|
warn_count=0
|
||||||
|
expired_count=0
|
||||||
|
error_count=0
|
||||||
|
|
||||||
|
mail_body=""
|
||||||
|
|
||||||
|
log "Certificate expiry report ($(date -u))"
|
||||||
|
log "Domains file: $DOMAINS_FILE"
|
||||||
|
log "Threshold: ${WARN_DAYS} days"
|
||||||
|
|
||||||
|
while IFS= read -r raw || [ -n "$raw" ]; do
|
||||||
|
# trim trailing spaces
|
||||||
|
line="$(printf '%s' "$raw" | sed 's/[[:space:]]*$//')"
|
||||||
|
# skip comments/empty
|
||||||
|
case "$line" in
|
||||||
|
""|\#*) continue ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
target="$(normalize_target "$line")"
|
||||||
|
notafter="$(get_notafter "$target" || true)"
|
||||||
|
|
||||||
|
if [ -z "$notafter" ]; then
|
||||||
|
error_count=$((error_count + 1))
|
||||||
|
log "ERROR $target could_not_fetch_cert_enddate"
|
||||||
|
mail_body="${mail_body}ERROR $target could_not_fetch_cert_enddate\n"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
exp_epoch="$(to_epoch "$notafter" || true)"
|
||||||
|
if [ -z "${exp_epoch:-}" ]; then
|
||||||
|
error_count=$((error_count + 1))
|
||||||
|
log "ERROR $target could_not_parse_notAfter='$notafter'"
|
||||||
|
mail_body="${mail_body}ERROR $target could_not_parse_notAfter='$notafter'\n"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
seconds_left=$((exp_epoch - now_epoch))
|
||||||
|
days_left=$((seconds_left / 86400))
|
||||||
|
|
||||||
|
if [ "$seconds_left" -lt 0 ]; then
|
||||||
|
expired_count=$((expired_count + 1))
|
||||||
|
log "EXPIRED $target days_left=$days_left notAfter='$notafter'"
|
||||||
|
mail_body="${mail_body}EXPIRED $target days_left=$days_left notAfter='$notafter'\n"
|
||||||
|
elif [ "$days_left" -le "$WARN_DAYS" ]; then
|
||||||
|
warn_count=$((warn_count + 1))
|
||||||
|
log "WARN $target days_left=$days_left notAfter='$notafter'"
|
||||||
|
mail_body="${mail_body}WARN $target days_left=$days_left notAfter='$notafter'\n"
|
||||||
|
else
|
||||||
|
ok_count=$((ok_count + 1))
|
||||||
|
log "OK $target days_left=$days_left notAfter='$notafter'"
|
||||||
|
fi
|
||||||
|
done <"$DOMAINS_FILE"
|
||||||
|
|
||||||
|
summary_line="SUMMARY ok=$ok_count warn=$warn_count expired=$expired_count error=$error_count warn_days=$WARN_DAYS"
|
||||||
|
log "$summary_line"
|
||||||
|
|
||||||
|
# Email decision
|
||||||
|
should_send=0
|
||||||
|
if [ "$ENABLE_EMAIL" -eq 1 ] && [ "$CHECK_ONLY" -eq 0 ]; then
|
||||||
|
if [ "$FORCE_SEND" -eq 1 ]; then
|
||||||
|
should_send=1
|
||||||
|
elif [ "$warn_count" -gt 0 ] || [ "$expired_count" -gt 0 ] || [ "$error_count" -gt 0 ]; then
|
||||||
|
should_send=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$should_send" -eq 1 ]; then
|
||||||
|
if send_email; then
|
||||||
|
log "EMAIL sent_to=$RECIPIENT"
|
||||||
|
else
|
||||||
|
log "EMAIL failed (msmtp/config/recipient/sender missing)"
|
||||||
|
error_count=$((error_count + 1))
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "EMAIL skipped (enable_email=$ENABLE_EMAIL check_only=$CHECK_ONLY force_send=$FORCE_SEND)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -------------------- exit code policy (FIXED) --------------------
|
||||||
|
if [ "$expired_count" -gt 0 ] || [ "$error_count" -gt 0 ]; then
|
||||||
|
exit 2
|
||||||
|
elif [ "$warn_count" -gt 0 ]; then
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
18
config/cert-expiry.conf.example
Normal file
18
config/cert-expiry.conf.example
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Copy to config/cert-expiry.conf (gitignored)
|
||||||
|
|
||||||
|
# Notification email settings (examples)
|
||||||
|
RECIPIENT="alert@example.domain"
|
||||||
|
SENDER="noreply@example.domain"
|
||||||
|
|
||||||
|
# Check settings
|
||||||
|
WARN_DAYS=30
|
||||||
|
TIMEOUT=12
|
||||||
|
FORCE_SEND=0
|
||||||
|
|
||||||
|
# Files (defaults are repo-local, these are optional)
|
||||||
|
DOMAINS_FILE="./config/domains.txt"
|
||||||
|
LOG_FILE="./cert-expiry.log"
|
||||||
|
|
||||||
|
# Email via msmtp (optional)
|
||||||
|
ENABLE_EMAIL=0
|
||||||
|
MSMTP_CONFIG="./config/msmtprc"
|
||||||
13
config/msmtprc.example
Normal file
13
config/msmtprc.example
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Copy to config/msmtprc (gitignored) and chmod 600
|
||||||
|
|
||||||
|
defaults
|
||||||
|
auth on
|
||||||
|
tls on
|
||||||
|
tls_trust_file /etc/ssl/certs/ca-certificates.crt
|
||||||
|
|
||||||
|
account default
|
||||||
|
host smtp.example.domain
|
||||||
|
port 587
|
||||||
|
from noreply@example.domain
|
||||||
|
user noreply@example.domain
|
||||||
|
password CHANGE_ME
|
||||||
Reference in New Issue
Block a user