|
@@ -0,0 +1,160 @@
|
|
|
|
|
+#!/bin/sh
|
|
|
|
|
+# ============================================================
|
|
|
|
|
+# nginx 证书装载自愈 —— 47.94.88.244(payment-platform)
|
|
|
|
|
+#
|
|
|
|
|
+# 【为什么需要它】
|
|
|
|
|
+# NPM(jc21/nginx-proxy-manager)的定时续期路径
|
|
|
|
|
+# processExpiringHosts -> renew -> renewLetsEncryptSsl*
|
|
|
|
|
+# 只跑 certbot,从头到尾没有 reload nginx。
|
|
|
|
|
+# 证书文件和 NPM 数据库都更新了,nginx 内存里还是旧证书,
|
|
|
|
|
+# 直到下一次改配置(动 NPM 界面)才顺带重载。
|
|
|
|
|
+#
|
|
|
|
|
+# 2026-08-25 三张证书静默续期成功,nginx 一直发 06-25 那张,
|
|
|
|
|
+# 09-23 过期,三个域名 HTTPS 中断三天才被发现。
|
|
|
|
|
+# 下一次静默续期约 10/23,不处理则 11/22 重演。
|
|
|
|
|
+#
|
|
|
|
|
+# 【做什么】
|
|
|
|
|
+# 比对「nginx 实际发出的证书」与「磁盘当前证书」:
|
|
|
|
|
+# 不一致 -> nginx -t 通过后 reload,再复验一次
|
|
|
|
|
+# 一致 -> 只在日志里记一行,不动 nginx
|
|
|
|
|
+# 另有兜底:实际发出的证书剩余不足 WARN_DAYS 天,无论比对结果都告警。
|
|
|
|
|
+#
|
|
|
|
|
+# 【部署】
|
|
|
|
|
+# scp devops/check-cert-loaded.sh root@47.94.88.244:/root/payments/deploy/
|
|
|
|
|
+# chmod +x /root/payments/deploy/check-cert-loaded.sh
|
|
|
|
|
+# /root/payments/deploy/check-cert-loaded.sh # 手工跑一次,应三个域名全 OK
|
|
|
|
|
+# crontab -e 加一行(输出由脚本自己写日志,cron 行保持短,防粘贴折行):
|
|
|
|
|
+# 17 4 * * * /root/payments/deploy/check-cert-loaded.sh
|
|
|
|
|
+#
|
|
|
|
|
+# 钉钉 webhook 从 $ENV_FILE 的 DINGTALK_WEBHOOK= 读取,未配置则只写日志。
|
|
|
|
|
+# 注意:钉钉机器人配了 IP 白名单,先确认本机公网出口 IP 已加入,
|
|
|
|
|
+# 否则返回 {"errcode":310000,...}(见运维手册告警章节)。
|
|
|
|
|
+# ============================================================
|
|
|
|
|
+
|
|
|
|
|
+PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
|
|
|
|
+export PATH
|
|
|
|
|
+
|
|
|
|
|
+# cron 下输出落到日志;手工执行时(stdout 是终端)直接打屏,方便验证
|
|
|
|
|
+LOG_FILE=/var/log/cert-loaded.log
|
|
|
|
|
+if [ ! -t 1 ]; then
|
|
|
|
|
+ exec >> "$LOG_FILE" 2>&1
|
|
|
|
|
+fi
|
|
|
|
|
+
|
|
|
|
|
+CONTAINER=nginx-proxy-manager
|
|
|
|
|
+DOMAINS="qcsj88888.com api.qcsj88888.com doc.qcsj88888.com"
|
|
|
|
|
+
|
|
|
|
|
+# 兜底阈值。NPM 在到期前 30 天续期,正常运行时实际发出的证书
|
|
|
|
|
+# 不会低于约 30 天;低于此值说明续期或重载至少有一环没走通。
|
|
|
|
|
+WARN_DAYS=15
|
|
|
|
|
+
|
|
|
|
|
+ENV_FILE=/root/payments/deploy/.env
|
|
|
|
|
+
|
|
|
|
|
+TAG='[cert-loaded]'
|
|
|
|
|
+NOW=$(date +%s)
|
|
|
|
|
+ALERT=''
|
|
|
|
|
+STALE=''
|
|
|
|
|
+
|
|
|
|
|
+log() { echo "$(date '+%F %T') $TAG $*"; }
|
|
|
|
|
+
|
|
|
|
|
+WEBHOOK=$(grep -E '^DINGTALK_WEBHOOK=' "$ENV_FILE" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '"')
|
|
|
|
|
+
|
|
|
|
|
+notify() {
|
|
|
|
|
+ if [ -z "$WEBHOOK" ]; then
|
|
|
|
|
+ log "未配置 DINGTALK_WEBHOOK($ENV_FILE),仅写日志"
|
|
|
|
|
+ return 0
|
|
|
|
|
+ fi
|
|
|
|
|
+ _p=$(mktemp)
|
|
|
|
|
+ printf '{"msgtype":"markdown","markdown":{"title":"%s","text":"%s"},"at":{"isAtAll":true}}' "$1" "$2" > "$_p"
|
|
|
|
|
+ _resp=$(curl -s -m 10 -w '\n%{http_code}' -X POST "$WEBHOOK" -H "Content-Type: application/json" -d @"$_p")
|
|
|
|
|
+ rm -f "$_p"
|
|
|
|
|
+ _code=$(echo "$_resp" | tail -1)
|
|
|
|
|
+ _body=$(echo "$_resp" | sed '$d')
|
|
|
|
|
+ # 钉钉业务错误同样返回 HTTP 200,必须查 body 里的 errcode
|
|
|
|
|
+ if [ "$_code" = "200" ] && echo "$_body" | grep -q '"errcode":0'; then
|
|
|
|
|
+ log '告警已发送'
|
|
|
|
|
+ else
|
|
|
|
|
+ log "告警发送失败 (HTTP $_code): $_body"
|
|
|
|
|
+ fi
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+# 探服务实际发出的证书(127.0.0.1 + SNI,绕开 DNS 缓存)
|
|
|
|
|
+served_cert() {
|
|
|
|
|
+ echo | openssl s_client -connect 127.0.0.1:443 -servername "$1" 2>/dev/null | openssl x509 -noout -fingerprint -sha256 -enddate 2>/dev/null
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fp_of() {
|
|
|
|
|
+ echo "$1" | grep -i 'Fingerprint=' | head -1 | cut -d= -f2 | tr -d ':' | tr 'A-F' 'a-f'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+# ── 磁盘上「当前」证书的指纹集合(live/ 指向的就是当前的)──
|
|
|
|
|
+DISK_FPS=$(docker exec "$CONTAINER" sh -c 'for f in /etc/letsencrypt/live/npm-*/fullchain.pem; do openssl x509 -in "$f" -noout -fingerprint -sha256 2>/dev/null; done' 2>/dev/null | cut -d= -f2 | tr -d ':' | tr 'A-F' 'a-f')
|
|
|
|
|
+
|
|
|
|
|
+if [ -z "$DISK_FPS" ]; then
|
|
|
|
|
+ log "读不到容器内的磁盘证书指纹(容器没跑?openssl 缺失?)"
|
|
|
|
|
+ notify '🔧 cert-loaded 脚本异常' "## 🔧 cert-loaded 脚本异常\n\n- **主机**:$(hostname)\n- 无法从容器 \`${CONTAINER}\` 读取磁盘证书指纹\n\n请人工检查。"
|
|
|
|
|
+ exit 1
|
|
|
|
|
+fi
|
|
|
|
|
+
|
|
|
|
|
+# ── 逐个域名比对 ──
|
|
|
|
|
+for d in $DOMAINS; do
|
|
|
|
|
+ s_raw=$(served_cert "$d")
|
|
|
|
|
+ s_fp=$(fp_of "$s_raw")
|
|
|
|
|
+ s_end=$(echo "$s_raw" | grep -i 'notAfter=' | head -1 | cut -d= -f2-)
|
|
|
|
|
+ s_days=''
|
|
|
|
|
+ if [ -n "$s_end" ]; then
|
|
|
|
|
+ s_epoch=$(date -d "$s_end" +%s 2>/dev/null)
|
|
|
|
|
+ if [ -n "$s_epoch" ]; then
|
|
|
|
|
+ s_days=$(( (s_epoch - NOW) / 86400 ))
|
|
|
|
|
+ fi
|
|
|
|
|
+ fi
|
|
|
|
|
+
|
|
|
|
|
+ if [ -z "$s_fp" ]; then
|
|
|
|
|
+ log "探测失败 $d"
|
|
|
|
|
+ ALERT="${ALERT}- $d:443 探测失败(服务在跑吗?)\n"
|
|
|
|
|
+ continue
|
|
|
|
|
+ fi
|
|
|
|
|
+
|
|
|
|
|
+ if echo "$DISK_FPS" | grep -qx "$s_fp"; then
|
|
|
|
|
+ log "OK $d 剩余 ${s_days:-?} 天"
|
|
|
|
|
+ else
|
|
|
|
|
+ log "不匹配 $d 发出的证书不是磁盘当前证书,剩余 ${s_days:-?} 天"
|
|
|
|
|
+ STALE="$STALE$d "
|
|
|
|
|
+ fi
|
|
|
|
|
+
|
|
|
|
|
+ if [ -n "$s_days" ] && [ "$s_days" -lt "$WARN_DAYS" ]; then
|
|
|
|
|
+ ALERT="${ALERT}- $d:实际发出的证书仅剩 ${s_days} 天(到期 $s_end)\n"
|
|
|
|
|
+ fi
|
|
|
|
|
+done
|
|
|
|
|
+
|
|
|
|
|
+# ── 发现未装载 -> 验配置 -> reload -> 复验 ──
|
|
|
|
|
+if [ -n "$STALE" ]; then
|
|
|
|
|
+ log "检测到未装载:$STALE"
|
|
|
|
|
+ if docker exec "$CONTAINER" nginx -t >/dev/null 2>&1; then
|
|
|
|
|
+ docker exec "$CONTAINER" nginx -s reload >/dev/null 2>&1
|
|
|
|
|
+ sleep 2
|
|
|
|
|
+ STILL=''
|
|
|
|
|
+ for d in $STALE; do
|
|
|
|
|
+ r_fp=$(served_cert "$d" | grep -i 'Fingerprint=' | head -1 | cut -d= -f2 | tr -d ':' | tr 'A-F' 'a-f')
|
|
|
|
|
+ echo "$DISK_FPS" | grep -qx "$r_fp" || STILL="$STILL$d "
|
|
|
|
|
+ done
|
|
|
|
|
+ if [ -z "$STILL" ]; then
|
|
|
|
|
+ log "已 reload,复验通过:$STALE"
|
|
|
|
|
+ ALERT="${ALERT}已自动 reload,复验通过:$STALE\n"
|
|
|
|
|
+ else
|
|
|
|
|
+ log "已 reload,但复验仍不匹配:$STILL"
|
|
|
|
|
+ ALERT="${ALERT}已 reload,但复验仍不匹配:$STILL\n"
|
|
|
|
|
+ fi
|
|
|
|
|
+ else
|
|
|
|
|
+ log "nginx -t 失败,未执行 reload"
|
|
|
|
|
+ ALERT="${ALERT}**nginx -t 失败,未执行 reload**:$STALE\n"
|
|
|
|
|
+ fi
|
|
|
|
|
+fi
|
|
|
|
|
+
|
|
|
|
|
+# ── 无异常静默退出 ──
|
|
|
|
|
+if [ -z "$ALERT" ]; then
|
|
|
|
|
+ log '全部正常'
|
|
|
|
|
+ exit 0
|
|
|
|
|
+fi
|
|
|
|
|
+
|
|
|
|
|
+notify '🔧 nginx 证书未装载' "## 🔧 nginx 证书未装载\n\n- **主机**:$(hostname)\n${ALERT}\n**根因**:NPM 续期路径不 reload nginx(见运维手册 SSL 章节)\n\n请确认是否需要跟进。"
|
|
|
|
|
+exit 0
|