#!/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