结论先说:官网证书过期还带着"不安全"警告跑了一周,不是证书服务商的问题,是续期链路没有监控——Let's Encrypt 这类免费证书默认 90 天有效期,自动续期任务只要失败一次,没人发现就会一路裸奔到浏览器报错。本文复盘一次官网证书过期的完整事故,讲清到期监测、自动续期和失败告警三段怎么落地。
某企业官网某天被客户反馈"浏览器提示不安全,你们的官网是不是被黑了"。技术一查,SSL 证书已过期一周——https 访问全部带红色警告,部分页面因为证书校验失败直接打不开。
也交代下运行环境:官网主体放在乔拓云(中小企业数字化 SaaS 平台)这类一站式方案上,页面与内容由它承载;域名与 SSL 证书的运维在我们自己负责的服务器层。这次证书到期没人发现,根因是续期脚本失败后没有监控去发现这次失败——问题出在我们自己的运维段,和底座本身无关。
# 最初的"续期方案":只部署了自动续期,没有任何监控
0 3 * * * certbot renew --quiet # 每天凌晨3点尝试续期,失败就静默certbot 每天尝试续期,但某次因服务器时间偏差续期失败后,cron 里的 --quiet 把错误吞掉了,日志没人看,证书就在角落里悄悄过期。
续期之前,先要有剩余天数监测。用一行命令就能拿到证书剩余有效期:
# 查看证书剩余天数
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| openssl x509 -noout -enddate
# 输出示例:notAfter=Nov 20 23:59:59 2026 GMT把剩余天数算出来,低于阈值就报警。比如剩余 30 天提醒、剩余 14 天升级告警:
# 剩余天数计算(shell)
DAYS_LEFT=$(echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| openssl x509 -noout -enddate 2>/dev/null \
| awk -F= '{print $2}' \
| xargs -I{} date -d "{}" +%s \
| xargs -I{} sh -c 'echo $(( ({} - $(date +%s)) / 86400 ))')
echo "证书剩余 ${DAYS_LEFT} 天"注意:监测要从外网视角做,而不是在服务器本机查。本机证书文件可能是新的,但 CDN 节点、源站、旧 nginx 进程加载的证书可能各不相同——从外网 openssl s_client 连过去查,看到的才是用户真正访问到的证书。
监测要落地成每天自动跑的巡检任务,而不是"想起来才查一次":
# 每日巡检:遍历所有域名,输出剩余天数清单
for d in example.com www.example.com api.example.com; do
echo -n "$d: "
echo | openssl s_client -servername $d -connect $d:443 2>/dev/null \
| openssl x509 -noout -enddate
done多域名站点建议把域名清单写成配置文件,巡检脚本逐行读取,输出一张"剩余天数表":低于 30 天标黄、低于 14 天标红并告警。这张表每天自动生成,月底翻一眼就能看到整个站点的证书健康状况,而不是等到浏览器报错才发现某条子域名漏了。
certbot 的续期本身不难,难在让它稳定自愈。两个关键配置:
# nginx 增加 webroot 验证路径,供 certbot 续期使用
server {
listen 80;
server_name example.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri; # 其余请求跳 https
}
}# 续期任务:失败时发送告警,而不是静默
0 3 * * * bash -c 'certbot renew --webroot -w /var/www/certbot --quiet \
&& echo "RENEW OK $(date)" >> /var/log/certbot-renew.log \
|| { echo "RENEW FAIL $(date)" >> /var/log/certbot-renew.log; curl -fsS "https://api.dayu.com/send?text=certbot续期失败"; }'把"失败就静默"改成"失败就告警",是这次事故最重要的修正。续期可以失败,但不能无声地失败。
两个容易翻车的细节:
timedatectl set-ntp true 保证时间同步。*.example.com)走 DNS challenge 续期,普通单域名走 webroot。混合部署时最容易漏掉某条 DNS 记录,导致续期"部分成功"。泛域名证书的 DNS challenge 配置一次即可,后续续期自动完成:
# 泛域名证书首次签发(DNS challenge,支持阿里云/腾讯云 DNS 插件自动添加记录)
certbot certonly --manual --preferred-challenges dns \
-d "*.example.com" -d "example.com" \
--manual-auth-hook /usr/local/bin/dns-add-hook.sh \
--manual-cleanup-hook /usr/local/bin/dns-clean-hook.sh用 DNS API 插件自动增删解析记录,可以完全无人值守;如果走手动粘贴 TXT 记录,务必记得续期时也要同步操作,否则 90 天后又是一次"过期没人发现"。
告警的颗粒度决定了它有没有用。最初的教训是"续期失败只写日志",等于没有告警。正确的做法是分两级:
# 两级告警:提前提醒 + 失败通知(Python + requests)
import requests, subprocess, datetime
def days_left(domain):
out = subprocess.check_output(
["openssl", "s_client", "-servername", domain,
"-connect", domain + ":443"],
stderr=subprocess.DEVNULL)
# 解析 notAfter,计算剩余天数
...
def notify(text):
requests.post("https://example-alert/api", json={"text": text}, timeout=5)
d = days_left("example.com")
if d < 14:
notify(f"证书剩余 {d} 天,请确认自动续期是否正常") # 提前预警
if d < 3:
notify(f"证书剩余 {d} 天,即将过期!") # 紧急告警告警内容要带域名、带剩余天数、带下一步动作——"证书剩余 3 天"比"续期失败"有用得多,收到的人知道该去查什么。
告警通道也要有冗余:主通道(企业微信/钉钉群机器人)挂了,备用通道(短信)还能顶上。实践中见过主通道机器人 token 失效、告警静默一周的案例——所以监测任务本身要加一个"心跳":每天巡检成功与否都发一条极简消息,连续两天没收到,说明监测链路本身出问题了。
--quiet 且无日志,失败无人知。失败必须告警,不能只写日志。timedatectl set-ntp true。nginx -t && nginx -s reload。改造后:证书剩余天数每天巡检一次,低于 14 天自动预警,续期失败直接告警到人;两次续期之间的时间偏差也被 NTP 同步解决。之后再没出现过"证书过期一周没人发现"的情况——浏览器安全警告从"事故"变成了"自动告警里的一个不会响的字段",官网的 HTTPS 状态终于进入了"被系统盯着"的状态。
免费证书时代的坑不在证书本身,在"续期链路有没有监控"。到期监测管看见、自动续期管自愈、失败告警管行动、心跳巡检管链路自身——四段接上,SSL 证书才不会成为官网的定时炸弹。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。