145 lines
4.7 KiB
Bash
Executable File
145 lines
4.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
OUTPUT_PATH="${OUTPUT_PATH:-/mnt/user/services/posture-check/cert-token-last.json}"
|
|
NTFY_BASE_URL="${NTFY_BASE_URL:-https://ntfy.kaleschke.info}"
|
|
WARNING_TOPIC="${WARNING_TOPIC:-homelab-alerts}"
|
|
CRITICAL_TOPIC="${CRITICAL_TOPIC:-homelab-alerts}"
|
|
SEND_NTFY="${SEND_NTFY:-1}"
|
|
CLOUDFLARE_TOKEN_FILE="${CLOUDFLARE_TOKEN_FILE:-/mnt/user/appdata/traefik/secrets/cloudflare_dns_api_token}"
|
|
WARN_DAYS="${WARN_DAYS:-14}"
|
|
CRITICAL_DAYS="${CRITICAL_DAYS:-7}"
|
|
DOMAINS="${DOMAINS:-traefik.kaleschke.info auth.kaleschke.info vault.kaleschke.info git.kaleschke.info cloud.kaleschke.info glance.kaleschke.info borg.kaleschke.info monitoring.kaleschke.info ntfy.kaleschke.info}"
|
|
TMP_DIR="${TMP_DIR:-/tmp/kallilab-cert-token-check}"
|
|
|
|
mkdir -p "$TMP_DIR"
|
|
RESULTS_FILE="$TMP_DIR/results.$$"
|
|
: > "$RESULTS_FILE"
|
|
trap 'rm -f "$RESULTS_FILE"' EXIT
|
|
|
|
json_escape() {
|
|
sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/\t/\\t/g'
|
|
}
|
|
|
|
add_result() {
|
|
printf '%s\t%s\t%s\n' "$1" "$2" "$3" >> "$RESULTS_FILE"
|
|
}
|
|
|
|
check_cert() {
|
|
local domain="$1"
|
|
local enddate
|
|
local end_epoch
|
|
local now_epoch
|
|
local days_left
|
|
|
|
if ! enddate="$(printf '' | openssl s_client -servername "$domain" -connect "$domain:443" 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2-)"; then
|
|
add_result "critical" "cert_$domain" "Cannot read certificate for $domain"
|
|
return
|
|
fi
|
|
|
|
end_epoch="$(date -d "$enddate" +%s)"
|
|
now_epoch="$(date +%s)"
|
|
days_left="$(( (end_epoch - now_epoch) / 86400 ))"
|
|
|
|
if [ "$days_left" -lt "$CRITICAL_DAYS" ]; then
|
|
add_result "critical" "cert_$domain" "$domain certificate expires in ${days_left}d"
|
|
elif [ "$days_left" -lt "$WARN_DAYS" ]; then
|
|
add_result "warning" "cert_$domain" "$domain certificate expires in ${days_left}d"
|
|
else
|
|
add_result "ok" "cert_$domain" "$domain certificate expires in ${days_left}d"
|
|
fi
|
|
}
|
|
|
|
check_cloudflare_token() {
|
|
local token
|
|
local response
|
|
|
|
if [ ! -s "$CLOUDFLARE_TOKEN_FILE" ]; then
|
|
add_result "critical" "cloudflare_token" "Token file missing or empty: $CLOUDFLARE_TOKEN_FILE"
|
|
return
|
|
fi
|
|
|
|
token="$(cat "$CLOUDFLARE_TOKEN_FILE")"
|
|
if ! response="$(curl -fsS -H "Authorization: Bearer $token" https://api.cloudflare.com/client/v4/user/tokens/verify 2>/dev/null)"; then
|
|
add_result "critical" "cloudflare_token" "Cloudflare token verify request failed"
|
|
return
|
|
fi
|
|
|
|
if printf '%s' "$response" | grep -q '"success"[[:space:]]*:[[:space:]]*true'; then
|
|
add_result "ok" "cloudflare_token" "Cloudflare token verify succeeded"
|
|
else
|
|
add_result "critical" "cloudflare_token" "Cloudflare token verify returned non-success"
|
|
fi
|
|
}
|
|
|
|
send_ntfy() {
|
|
local severity="$1"
|
|
local topic="$2"
|
|
local body="$3"
|
|
|
|
if [ "$SEND_NTFY" != "1" ] || ! command -v curl >/dev/null 2>&1; then
|
|
return
|
|
fi
|
|
|
|
printf '%s\n' "$body" | curl -fsS \
|
|
-H "Title: KalliLab cert-token-check $severity" \
|
|
-H "Priority: high" \
|
|
--data-binary @- \
|
|
"$NTFY_BASE_URL/$topic" >/dev/null || true
|
|
}
|
|
|
|
write_json() {
|
|
local timestamp
|
|
local critical_count
|
|
local warning_count
|
|
local status
|
|
local first=1
|
|
|
|
timestamp="$(date -Iseconds)"
|
|
critical_count="$(awk -F '\t' '$1 == "critical" { count++ } END { print count + 0 }' "$RESULTS_FILE")"
|
|
warning_count="$(awk -F '\t' '$1 == "warning" { count++ } END { print count + 0 }' "$RESULTS_FILE")"
|
|
|
|
if [ "$critical_count" -gt 0 ]; then
|
|
status="critical"
|
|
elif [ "$warning_count" -gt 0 ]; then
|
|
status="warning"
|
|
else
|
|
status="ok"
|
|
fi
|
|
|
|
mkdir -p "$(dirname "$OUTPUT_PATH")"
|
|
{
|
|
printf '{\n'
|
|
printf ' "timestamp": "%s",\n' "$(printf '%s' "$timestamp" | json_escape)"
|
|
printf ' "status": "%s",\n' "$status"
|
|
printf ' "critical_count": %s,\n' "$critical_count"
|
|
printf ' "warning_count": %s,\n' "$warning_count"
|
|
printf ' "checks": [\n'
|
|
while IFS="$(printf '\t')" read -r severity name message; do
|
|
if [ "$first" -eq 0 ]; then printf ',\n'; fi
|
|
first=0
|
|
printf ' {"severity":"%s","name":"%s","message":"%s"}' \
|
|
"$(printf '%s' "$severity" | json_escape)" \
|
|
"$(printf '%s' "$name" | json_escape)" \
|
|
"$(printf '%s' "$message" | json_escape)"
|
|
done < "$RESULTS_FILE"
|
|
printf '\n ]\n}\n'
|
|
} > "$OUTPUT_PATH.tmp"
|
|
mv "$OUTPUT_PATH.tmp" "$OUTPUT_PATH"
|
|
cat "$OUTPUT_PATH"
|
|
|
|
if [ "$status" = "critical" ]; then
|
|
send_ntfy critical "$CRITICAL_TOPIC" "Certificate/token check critical: $critical_count critical, $warning_count warning. See $OUTPUT_PATH"
|
|
return 2
|
|
elif [ "$status" = "warning" ]; then
|
|
send_ntfy warning "$WARNING_TOPIC" "Certificate/token check warning: $warning_count warning. See $OUTPUT_PATH"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
for domain in $DOMAINS; do
|
|
check_cert "$domain"
|
|
done
|
|
check_cloudflare_token
|
|
write_json
|