手里域名和站点多了之后,SSL 证书过期是最容易被忽略的故障点。分享一个只用 Python 标准库实现的证书过期检查脚本:不依赖任何第三方包,直连 443 端口读取证书有效期,剩余天数不足阈值就推送告警,配合 cron 每天跑一次即可。

原理

用 ssl + socket 直连目标域名的 443 端口,完成 TLS 握手后读取对端证书的 notAfter 字段,换算成剩余天数,小于设定的阈值就触发推送。

代码

import ssl
import socket
import datetime
import json
import urllib.request
import urllib.parse

DOMAINS = ["example.com", "blog.example.com"]
WARN_DAYS = 14

def cert_expire_days(host, port=443):
    ctx = ssl.create_default_context()
    with socket.create_connection((host, port), timeout=10) as sock:
        with ctx.wrap_socket(sock, server_hostname=host) as ssock:
            cert = ssock.getpeercert()
    expire = datetime.datetime.strptime(cert["notAfter"], "%b %d %H:%M:%S %Y %Z")
    return (expire - datetime.datetime.utcnow()).days

def bark_push(key, title, body):
    url = "https://api.day.app/{}/{}/{}".format(key, urllib.parse.quote(title), urllib.parse.quote(body))
    urllib.request.urlopen(url, timeout=10)

def tg_push(token, chat_id, text):
    url = "https://api.telegram.org/bot{}/sendMessage".format(token)
    data = json.dumps({"chat_id": chat_id, "text": text}).encode()
    req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"})
    urllib.request.urlopen(req, timeout=10)

if __name__ == "__main__":
    warnings = []
    for d in DOMAINS:
        try:
            days = cert_expire_days(d)
            print("{}: 剩余 {} 天".format(d, days))
            if days < WARN_DAYS:
                warnings.append("{} 仅剩 {} 天".format(d, days))
        except Exception as e:
            warnings.append("{} 检查失败: {}".format(d, e))
    if warnings:
        msg = "\n".join(warnings)
        # bark_push("你的 Bark Key", "证书告警", msg)
        # tg_push("Bot Token", "Chat ID", "证书告警", msg)

定时运行

加一条 cron,每天早上 8 点检查一次:

0 8 * * * /usr/bin/python3 /root/ssl_check.py >> /var/log/ssl_check.log 2>&1

几点说明

  • 只用标准库,不用装 requests,任何有 Python3 的机器上直接能跑。
  • create_default_context 会校验服务端证书,遇到自签名或已过期证书会直接抛异常,本身也算一种告警。
  • Bark 适合 iOS 推送,Telegram Bot 适合多平台,按需二选一即可。
  • 域名多的话可以把 DOMAINS 改成从文本文件读取,方便维护。