Let’s Encrypt 续期监控:证书到期检测与自动告警脚本

网站突然打不开,浏览器提示”您的连接不是私密连接”,十有八九是 SSL(安全传输协议)证书过期了。Let’s Encrypt 证书有效期只有 90 天,虽然 certbot 会通过定时任务自动续期,但续期失败往往悄无声息——cron 日志没人看、邮件通知被忽略,直到用户投诉才发现问题。这篇文章教你如何搭建一套完整的续期监控方案:用脚本检测所有域名的证书剩余天数,在到期前 30 天、14 天、7 天分级告警,即使 certbot 静默失败也能提前收到通知。

Let's Encrypt 证书续期监控封面:浏览器地址栏挂锁与时钟的关联示意图

为什么证书会”自动续期却仍然过期”

很多人以为装了 certbot 就万事大吉,实际上自动续期存在多个失效点。我们处理过的真实案例中,最常见的原因有三个:

一是 80 端口被占用导致 HTTP-01 验证失败。某客户在服务器上新增了一个占用 80 端口的服务后,certbot 的 renew 验证全部失败,持续了 60 天无人发现。二是 DNS(域名解析系统)记录变更后未同步,比如网站迁移到 VPS(虚拟专用服务器)后只迁移了网页文件,没检查证书配置,旧的续期配置指向已失效的验证路径。三是 Python 运行环境损坏,certbot 依赖的系统包升级后与现有环境冲突,续期命令直接报错退出。

这些问题共同的特点是:certbot 的 cron 日志里其实写了错误,但没有人主动去查。所以监控的重点不是”检查证书是否过期”,而是”在过期前足够早的时间点发现续期异常”。

检测脚本:读取证书剩余天数

核心思路是用 openssl 读取证书的实际到期时间,而不是信任 certbot 自己的记录。以下是完整的检测脚本,保存为 /usr/local/bin/check_cert_expiry.sh:

#!/bin/bash
# 检查指定域名证书的剩余有效天数
DOMAINS="www.example.com shop.example.com"
ALERT_EMAIL="admin@example.com"
WARN_DAYS=30

for domain in $DOMAINS; do
  END_DATE=$(echo | openssl s_client -servername "$domain" \
    -connect "$domain":443 2>/dev/null \
    | openssl x509 -noout -enddate \
    | cut -d= -f2)
  if [ -z "$END_DATE" ]; then
    echo "$(date '+%F %T') [ERROR] $domain 无法读取证书" >> /var/log/cert-check.log
    continue
  fi
  END_TS=$(date -d "$END_DATE" +%s)
  NOW_TS=$(date +%s)
  DAYS_LEFT=$(( (END_TS - NOW_TS) / 86400 ))
  echo "$(date '+%F %T') [INFO] $domain 剩余 ${DAYS_LEFT} 天" >> /var/log/cert-check.log
  if [ "$DAYS_LEFT" -lt "$WARN_DAYS" ]; then
    echo "$domain 证书剩余 ${DAYS_DAYS} 天,请检查续期状态" \
      | mail -s "证书告警: $domain" "$ALERT_EMAIL"
  fi
done

这个脚本有两个设计要点。第一,它走真实的 443 端口握手读取线上证书,能发现”本地证书续期成功但 nginx 没有重新加载”这类配置不同步问题。第二,日志写入 /var/log/cert-check.log,每次运行都留下时间戳记录,方便事后回溯证书是在哪一天开始没有续上的。

部署 cron 定时检测

脚本写好后,用 cron 让它每天自动运行一次。执行 crontab -e 添加:

0 8 * * * /usr/local/bin/check_cert_expiry.sh

每天早上 8 点检查一遍。如果你想更稳妥,可以在续期窗口(Let’s Encrypt 建议每 60 天续期一次)前后各加一次检查,改为每 12 小时运行一次:0 8,20 * * *。

部署后先手动跑一次验证:bash /usr/local/bin/check_cert_expiry.sh && tail /var/log/cert-check.log。确认日志中每个域名都有正常的剩余天数记录,再交给 cron。很多服务器没配置 mail 命令,告警会静默丢失,这一步可以用 echo test | mail -s test admin@example.com 验证邮件通道是否真的可用。

证书检测流程示意图:服务器、检测盾牌与告警铃铛的连接关系

对于托管在虚拟主机上的网站,用户没有 root 权限也无法自行部署 cron,这类环境证书由服务商统一管理。如果你正在选型,Hostease 虚拟主机默认提供自动续期的 SSL(安全传输协议)证书,适合不想自己维护证书链路的中小站点。

分级告警:把”到期了”变成”快到期了”

单一阈值的告警有两个缺陷:设得太晚(比如 3 天)会来不及处理,设得太早(比如 45 天)会导致告警被反复触发、逐渐麻木。更好的做法是分级告警,在脚本中用阶梯逻辑实现:

if [ "$DAYS_LEFT" -le 7 ]; then
  LEVEL="CRITICAL"
elif [ "$DAYS_LEFT" -le 14 ]; then
  LEVEL="WARNING"
elif [ "$DAYS_LEFT" -le 30 ]; then
  LEVEL="NOTICE"
fi

三级阈值对应的处理节奏是:NOTICE 级别(剩余 30 天以内)记录日志并每日提醒,此时正常执行一次 certbot renew 就能解决;WARNING 级别(14 天以内)说明上次自动续期可能已经失败,需要登录服务器检查 certbot renew --dry-run 的输出,重点看 80/443 端口和 DNS 验证是否正常;CRITICAL 级别(7 天以内)必须立即人工介入,手动续期并检查 nginx/apache 是否重新加载了新证书。

实际部署时建议把 CRITICAL 告警同时发到两个渠道(比如邮件加即时通讯 webhook),因为证书过期造成的访客流失是实打实的——浏览器拦截页面会让超过 80% 的访客直接离开。

分级告警示意图:蓝、橙、红三档不同长度的进度条

进阶:用 certbot 钩子主动捕获续期失败

前面的方案是”事后检测”,还可以再往前走一步,在续期失败的第一时间就收到通知。certbot 提供了 --deploy-hook 和失败钩子机制,在 /etc/letsencrypt/renewal-hooks/ 目录下可以放置三类脚本:pre(续期前)、post(续期后)、deploy(新证书部署成功后)。

最实用的是 deploy hook,用来自动重载 web 服务器,避免”证书已续期但服务还在用旧证书”的问题:

# /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
#!/bin/bash
systemctl reload nginx

chmod +x 赋予执行权限后,每次 certbot 成功续期都会自动重载 nginx。而对于失败捕获,可以在 cron 中改为调用 certbot renew 2>&1 | mail -s "certbot 续期结果" admin@example.com,让每次续期无论成败都发一封邮件——成功时邮件是例行确认,没收到邮件或邮件里有报错就是需要关注的信号。

两种方案结合后的监控链路是:certbot 续期时钩子自动重载服务并邮件回报结果,独立的检测脚本每天核对线上证书剩余天数,服务器运维相关文章里还有更多 Web 服务器维护的细节可以参考。双层机制下,即使某一层失效,另一层仍能在证书过期前 30 天把问题暴露出来。

总结

Let’s Encrypt 续期监控的本质是给自动化流程加上人工可感知的安全网。核心动作有三步:用 openssl 脚本每天检测线上证书剩余天数,用分级阈值(30/14/7 天)区分告警优先级,用 certbot 钩子在续期失败的第一时间发出通知。整套方案只需要一个 shell 脚本加一条 cron 记录,10 分钟就能部署完成。

如果你需要现成的运维环境支持,可以考虑 Hostease 的 VPS 主机方案,root 权限完整,可以自由部署 certbot 和自定义监控脚本;管理多台机器时,这套检测脚本也可以统一跑在 独立服务器的运维流程里做集中告警;如果是企业站点,我们推荐把证书检查纳入每周例行巡检清单,与备份验证一起执行。建议现在就登录一台服务器,先手动跑一遍本文的检测脚本,看看你的域名证书还剩多少天。

发表评论