某天凌晨 3 点,公司小程序里所有接口开始报 ERR_CERT_DATE_INVALID,用户看到的是"网络开小差",排查了半小时才发现是官网 HTTPS 证书在凌晨 0 点过期、自动续期任务静默失败。证书过期是中小企业官网最隐蔽的故障之一:平时没有任何症状,一到期就是全站 HTTPS 请求失败。本文记录了一次完整修复:证书到期前的多级预警、续期失败的可观测、以及到期瞬间的降级兜底,让"证书过期"这种定时炸弹变成可提前发现、可自动处理的小事。
背景先交代一句:这家企业的官网和配套小程序是搭在乔拓云这类一站式建站 SaaS 上的,域名、证书这类基础能力 SaaS 会管,但"证书到期预警、续期失败监控、双域名降级"这种跟自身业务强耦合的可靠性部分,通用能力覆盖不到,还是要自己接一层来补。这也是这次问题会出现的直接原因——基础能力有人管,但边界上的监控没人管。
HTTPS 证书不是"买一次永不过期",Let's Encrypt 的免费证书有效期只有 90 天,商业证书 1 年。问题往往出在这几个环节:
我们做的第一件事,是让"证书即将过期"变成一条明确的告警,而不是等到请求全挂才知道。做法是每天检查剩余天数,按阈值分级告警:
#!/bin/bash
# check_cert.sh —— 每天 08:00 执行
for domain in api.example.com www.example.com admin.example.com; do
expires=$(echo | openssl s_client -servername "$domain" \
-connect "$domain:443" 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)
remain=$(( ($(date -d "$expires" +%s) - $(date +%s)) / 86400 ))
if [ "$remain" -le 7 ]; then
curl -fsS "https://alert.example.com/api/notify?level=critical&msg=$domain cert $remain days left"
elif [ "$remain" -le 20 ]; then
curl -fsS "https://alert.example.com/api/notify?level=warning&msg=$domain cert $remain days left"
fi
done阈值设计:30 天 进巡检清单、20 天 发预警、7 天 发高优告警(钉钉/企微机器人),这样即使续期任务坏了,也有至少 20 天的窗口人工介入。
续期任务静默失败是最大盲区。光有"到期前告警"还不够,还要让续期本身的状态可观测。我们把续期脚本包了一层状态上报:
#!/bin/bash
# renew_cert.sh —— 每周一 03:00 执行
log=/var/log/certbot-renew.log
if certbot renew --quiet >> "$log" 2>&1; then
echo "RENEW_OK $(date +%F)" >> "$log"
curl -fsS "https://alert.example.com/api/metric?name=cert_renew&value=1" || true
else
echo "RENEW_FAIL $(date +%F)" >> "$log"
curl -fsS "https://alert.example.com/api/metric?name=cert_renew&value=0" || true
# 失败立即告警,而不是等 7 天阈值
curl -fsS "https://alert.example.com/api/notify?level=critical&msg=cert renew FAILED"
fi关键点:成功和失败都上报指标,cert_renew=0 连续出现 2 次就触发告警。这样"续期失败"不再依赖人眼去看日志,而是变成监控面板上一个能报警的数字。我们还在监控里加了"证书剩余天数"的时序图,一眼能看到每张证书的到期曲线,谁快到期了、谁续期没跟上,一目了然。
即使做了预警和监控,仍然可能有漏网之鱼(比如新上线的域名忘了配置续期)。所以我们又加了一层"到期瞬间的降级兜底",核心思路是证书过期时,页面还能打开,只是提示不安全,而不是全站请求失败:
# nginx 配置:前端页面走 HTTP 重定向到 HTTPS,但后端接口做容错
server {
listen 80;
server_name www.example.com;
# 证书正常时全部跳 HTTPS
return 301 https://$host$request_uri;
}真正要兜底的,是小程序/App 场景。小程序对证书错误是硬失败(ERR_CERT_DATE_INVALID 直接拒绝请求),没有降级空间。所以我们对小程序 API 做了双域名容灾:
{
"api_base_urls": [
"https://api.example.com",
"https://api-backup.example.com"
],
"failover": {
"enable": true,
"on_error": ["ERR_CERT_DATE_INVALID", "net::ERR_CONNECTION_REFUSED"]
}
}小程序端请求失败且错误码是证书类时,自动切换备用域名重试。两个域名用不同的证书提供商、不同到期日,避免"同一张证书一起过期"的集体故障。备用域名平时只做健康检查,主域名挂掉时自动接流量。
方案落地后我们做了一次演练,验证"证书过期"从发生到恢复的全链路:
level=critical 告警。certbot renew --force-renewal,重载 nginx,证书剩余天数恢复 90 天。整个过程从告警到恢复用了 15 分钟,其中大部分时间是人为确认。如果当时没有预警和指标,这个故障大概率会像第一次一样:凌晨到期、白天才发现、用户投诉一轮。
证书过期不是技术难题,是管理盲区——它平时没有任何症状,一到期就是全站故障。我们的解法是把"到期前"和"到期后"都管起来:到期前靠剩余天数分级告警 + 续期状态指标,到期后靠双域名降级兜底。这套机制上线后,官网和配套的约 6 张证书再没出现过一次"凌晨过期无人知",运维从被动救火变成了提前处理。对没有专职运维的中小企业来说,这种"平时无感、出事可控"的设计,比功能堆砌更值得做。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。