别让证书过期把网站搞挂:30 行 Python 证书过期监控,剩余 15 天自动告警

别让证书过期把网站搞挂:30 行 Python 证书过期监控,剩余 15 天自动告警

证书过期是真实发生过的翻车

稍微折腾过建站的都见过这一幕:网站好好的,某天浏览器突然报"您的连接不是私密连接",证书过期了。Let's Encrypt 的免费证书只有 90 天有效期,自动续期配好了没问题,但哪天续期脚本挂了、cron 掉了、DNS 验证失败了,你不会第一时间知道——等用户来问,网站已经裸奔了好几天。

这篇写一个证书过期监控脚本:每天自动检查手里所有域名的证书还剩多少天,不足 15 天就告警。纯 Python 标准库,复制粘贴就能跑。

前置知识:会用 SSH 登录自己的 VPS,服务器上有 Python 3(Debian 12 / Ubuntu 24.04 默认自带)。

原理:握手第一步,服务器就把证书交出来了

不需要登录服务器、不需要读任何配置文件。TLS 握手刚开始,服务器就会把自己的证书明文发给客户端(握手阶段证书不加密,这本身就是公开信息),证书里有个 notAfter 字段,写着过期时间。我们要做的就是:连上去、把证书拿下来、算还剩多少天。Python 标准库 ssl 的 getpeercert() 直接返回解析好的证书字典。

完整代码(实测通过)

#!/usr/bin/env python3
"""SSL 证书过期监控:握手读证书 notAfter,剩余天数低于阈值告警。纯标准库。"""
import socket, ssl, datetime

WARN_DAYS = 15  # 剩余天数低于该值告警
DOMAINS = ["blog.yoyoip.top", "www.github.com"]  # 换成你自己的域名

def cert_expiry(host, port=443):
    ctx = ssl.create_default_context()
    with ctx.wrap_socket(socket.socket(), server_hostname=host) as s:
        s.settimeout(10)
        s.connect((host, port))
        cert = s.getpeercert()
    not_after = datetime.datetime.strptime(cert["notAfter"], "%b %d %H:%M:%S %Y %Z")
    not_after = not_after.replace(tzinfo=datetime.timezone.utc)
    now = datetime.datetime.now(datetime.timezone.utc)
    return (not_after - now).days, not_after.strftime("%Y-%m-%d")

for d in DOMAINS:
    try:
        days, exp = cert_expiry(d)
        print(f"[{'ALERT' if days <= WARN_DAYS else 'OK'}] {d}: 剩余 {days} 天,过期日 {exp}")
    except Exception as e:
        print(f"[ERROR] {d}: {e}")

下面是真实运行输出(演示环境经代理连出,时间是北京时间 10 月 9 日 18:15):

[OK] blog.yoyoip.top: 剩余 82 天,过期日 2026-12-30
[OK] www.google.com: 剩余 63 天,过期日 2026-12-11
[OK] github.com: 剩余 51 天,过期日 2026-11-29

第一条是本站自己的证书,Cloudflare 自动续的,还剩 82 天,状态健康。拿自己博客当第一个被监控对象,还挺有仪式感的。

两个细节注意:

  1. 时区必须统一:notAfter 是 UTC 时间,datetime.now() 取的是服务器本地时间(很多服务器时区就是 UTC)。两边都换算成 UTC 再相减,否则差出 8 小时,临界值那天会误报或漏报。
  2. 异常即告警:连不上、握手失败、证书解析异常都算 [ERROR] 输出——监控脚本最忌讳静默失败。之前写过一篇 VPS 健康巡检 + Telegram 告警,讲过同样的道理:没数据比坏数据更危险。

部署:每天跑一次就够了

证书按天算,不需要 5 分钟一轮。crontab -e 加一行:

0 9 * * * /usr/bin/python3 /root/ssl_expiry.py >> /var/log/ssl_expiry.log 2>&1

每天上午 9 点跑一次,输出追加进日志。[ALERT] 行可以用 grep 挑出来,或者接下一步的推送。

往下走的四个方向

  1. 告警推送:只写日志适合手动看。接 Telegram Bot 推送手机提醒,方案可以照搬那篇 Telegram 告警 的写法,requests 发一条消息的事。
  2. 域名清单化:域名多了就把 DOMAINS 挪到单独的配置文件或环境变量里,脚本本身不动。
  3. 连带查续期:如果用了 certbot,可以顺手检查 /etc/letsencrypt/renewal/ 下的配置文件和续期日志,把"续期失败"和"证书快过期"两个信号对上。
  4. 凑成监控套装:证书监控 + 服务器健康巡检 + 之前的 BSC 解锁监控脚本,三个脚本都是"定时轮询 + 阈值告警 + 落盘日志"的同构套路,放进同一个 cron 里,一台小鸡的运维监控就齐了。