Add server ops certificate expiry check
This commit is contained in:
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.log
|
||||
*.tmp
|
||||
*.bak
|
||||
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
61
README.md
Normal file
61
README.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Silver Server Ops
|
||||
|
||||
Server-wide maintenance scripts and operational notes for the Silver Cloud Hetzner host.
|
||||
|
||||
Suggested repository name:
|
||||
|
||||
```text
|
||||
silver-server-ops
|
||||
```
|
||||
|
||||
## SSL Certificate Expiry Alerts
|
||||
|
||||
`scripts/check-cert-expiry.sh` checks every Let's Encrypt certificate under `/etc/letsencrypt/live` and sends an ntfy alert when any certificate expires in less than 14 days.
|
||||
|
||||
Install on the server:
|
||||
|
||||
```bash
|
||||
sudo install -m 0755 scripts/check-cert-expiry.sh /opt/scripts/check-cert-expiry.sh
|
||||
```
|
||||
|
||||
Recommended root cron:
|
||||
|
||||
```cron
|
||||
15 8 * * * /opt/scripts/check-cert-expiry.sh >/var/log/cert-expiry-check.log 2>&1
|
||||
```
|
||||
|
||||
Defaults:
|
||||
|
||||
```bash
|
||||
CERT_DIR=/etc/letsencrypt/live
|
||||
EXPIRY_DAYS=14
|
||||
NTFY_URL=https://ntfy.silverwal.com/silver
|
||||
```
|
||||
|
||||
Optional overrides:
|
||||
|
||||
```bash
|
||||
EXPIRY_DAYS=21 NTFY_URL=https://ntfy.silverwal.com/silver /opt/scripts/check-cert-expiry.sh
|
||||
```
|
||||
|
||||
If your ntfy topic is protected with an access token:
|
||||
|
||||
```bash
|
||||
NTFY_TOKEN=your-token /opt/scripts/check-cert-expiry.sh
|
||||
```
|
||||
|
||||
Manual test:
|
||||
|
||||
```bash
|
||||
sudo /opt/scripts/check-cert-expiry.sh
|
||||
```
|
||||
|
||||
The script exits with `0` when all certificates are healthy and `1` when it sends an alert or cannot run the check correctly.
|
||||
|
||||
## Notification Notes
|
||||
|
||||
ntfy is a good default for this server because it is already self-hosted, simple to call from shell scripts, and supports useful alert metadata such as title, priority, and tags.
|
||||
|
||||
For jobs where silence is also a failure, pair ntfy with a dead man's switch such as Healthchecks. ntfy tells you what the script found; Healthchecks tells you when the script did not run at all.
|
||||
|
||||
If you later want one notification config that can fan out to ntfy, email, Slack, Discord, Telegram, and similar services, use Apprise as the wrapper and keep ntfy as one destination.
|
||||
103
scripts/check-cert-expiry.sh
Normal file
103
scripts/check-cert-expiry.sh
Normal file
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
CERT_DIR="${CERT_DIR:-/etc/letsencrypt/live}"
|
||||
EXPIRY_DAYS="${EXPIRY_DAYS:-14}"
|
||||
NTFY_URL="${NTFY_URL:-https://ntfy.silverwal.com/silver}"
|
||||
NTFY_TITLE="${NTFY_TITLE:-SSL certificate warning}"
|
||||
NTFY_PRIORITY="${NTFY_PRIORITY:-high}"
|
||||
NTFY_TAGS="${NTFY_TAGS:-warning,lock}"
|
||||
NTFY_TOKEN="${NTFY_TOKEN:-}"
|
||||
ALERT_ON_NO_CERTS="${ALERT_ON_NO_CERTS:-true}"
|
||||
|
||||
host="$(hostname -f 2>/dev/null || hostname 2>/dev/null || echo "unknown-host")"
|
||||
now_epoch="$(date +%s)"
|
||||
threshold_seconds=$((EXPIRY_DAYS * 86400))
|
||||
|
||||
send_alert() {
|
||||
local message="$1"
|
||||
local curl_args=(
|
||||
-fsS
|
||||
-H "Title: ${NTFY_TITLE}"
|
||||
-H "Priority: ${NTFY_PRIORITY}"
|
||||
-H "Tags: ${NTFY_TAGS}"
|
||||
)
|
||||
|
||||
if [[ -n "${NTFY_TOKEN}" ]]; then
|
||||
curl_args+=(-H "Authorization: Bearer ${NTFY_TOKEN}")
|
||||
fi
|
||||
|
||||
curl "${curl_args[@]}" --data-binary "${message}" "${NTFY_URL}" >/dev/null
|
||||
}
|
||||
|
||||
fail() {
|
||||
local message="$1"
|
||||
send_alert "${message}" || echo "Failed to send ntfy alert to ${NTFY_URL}" >&2
|
||||
echo "${message}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ! command -v openssl >/dev/null 2>&1; then
|
||||
fail "SSL certificate check failed on ${host}: openssl is not installed."
|
||||
fi
|
||||
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
echo "SSL certificate check failed on ${host}: curl is not installed." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -d "${CERT_DIR}" ]]; then
|
||||
fail "SSL certificate check failed on ${host}: certificate directory ${CERT_DIR} does not exist."
|
||||
fi
|
||||
|
||||
warnings=()
|
||||
cert_count=0
|
||||
|
||||
while IFS= read -r -d '' cert_path; do
|
||||
cert_count=$((cert_count + 1))
|
||||
cert_name="$(basename "$(dirname "${cert_path}")")"
|
||||
|
||||
enddate_line="$(openssl x509 -in "${cert_path}" -noout -enddate 2>/dev/null)"
|
||||
if [[ -z "${enddate_line}" ]]; then
|
||||
warnings+=("${cert_name}: could not read expiry date from ${cert_path}")
|
||||
continue
|
||||
fi
|
||||
|
||||
not_after="${enddate_line#notAfter=}"
|
||||
expiry_epoch="$(date -d "${not_after}" +%s 2>/dev/null)"
|
||||
if [[ -z "${expiry_epoch}" ]]; then
|
||||
warnings+=("${cert_name}: could not parse expiry date '${not_after}'")
|
||||
continue
|
||||
fi
|
||||
|
||||
seconds_left=$((expiry_epoch - now_epoch))
|
||||
days_left=$((seconds_left / 86400))
|
||||
|
||||
if (( seconds_left < 0 )); then
|
||||
warnings+=("${cert_name}: EXPIRED on ${not_after}")
|
||||
elif ! openssl x509 -checkend "${threshold_seconds}" -noout -in "${cert_path}" >/dev/null 2>&1; then
|
||||
warnings+=("${cert_name}: expires in ${days_left} day(s), on ${not_after}")
|
||||
fi
|
||||
done < <(find "${CERT_DIR}" -mindepth 2 -maxdepth 2 \( -type f -o -type l \) -name cert.pem -print0)
|
||||
|
||||
if (( cert_count == 0 )); then
|
||||
if [[ "${ALERT_ON_NO_CERTS}" == "true" ]]; then
|
||||
fail "SSL certificate check found no cert.pem files under ${CERT_DIR} on ${host}."
|
||||
fi
|
||||
|
||||
echo "No certificates found under ${CERT_DIR}; no alert sent."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if (( ${#warnings[@]} > 0 )); then
|
||||
message="SSL certificate expiry warning on ${host}. Threshold: ${EXPIRY_DAYS} day(s).
|
||||
|
||||
$(printf '%s\n' "${warnings[@]}")"
|
||||
|
||||
send_alert "${message}" || echo "Failed to send ntfy alert to ${NTFY_URL}" >&2
|
||||
echo "${message}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All ${cert_count} certificate(s) are valid for more than ${EXPIRY_DAYS} day(s)."
|
||||
Reference in New Issue
Block a user