SSL 证书到期告警:已经没人会发邮件提醒你了

Let's Encrypt 停发到期提醒邮件,证书有效期已降到 200 天。一段实测过的 openssl 检查脚本、适配短周期证书的阈值,以及真正能叫醒你的告警。

更新于

目录

有两件事在所有人的证书体系底下悄悄变了,而大多数团队两件都没有跟上。

第一,安全网被拆掉了。 Let's Encrypt 在 2025 年 6 月 4 日关停了到期提醒服务——就是那些在自动化悄悄坏掉时,救过成千上万个站点的邮件。停掉的理由都站得住脚(绝大多数用户已经自动续期;保存数百万个邮箱地址是隐私负担;这项服务每年要花「数万美元」),而那篇公告的结尾是:请自己去找第三方监控(Let's Encrypt)。很多人读到这里点头称是,然后就没有做后半句。

第二,容错余量塌了。2026 年 3 月 15 日起,公共 TLS 证书的最长有效期为 200 天;按 CA/Browser Forum SC-081v3 议案,2027 年 3 月 15 日降到 100 天,2029 年 3 月 15 日降到 47 天。Let's Encrypt 走得比上限更快:6 天期证书已于 2026 年 1 月 15 日全量开放,其路线图是 2027 年 2 月把默认配置降到 64 天、2028 年 2 月降到 45 天(Let's Encrypt)。

这两件事的方向是一致的:续期更频繁,意味着出错的机会更多;而出错的时候,没有任何人会给你发邮件。本文就是用来堵这个口子的——一段实测过的 shell 脚本、一套适配短周期证书的阈值,以及如何让最后一道防线用 Echobell 直接打响你的手机。

证书过期造成的故障到底有多严重?

严重到去年有超过三分之一的组织真的遇到过。 DigiCert 的《2026 全球证书管理展望》——由 Propeller Insights 于 2026 年 5 月执行,覆盖美、英、澳三国 1,001 位 IT 与网络安全决策者——显示,过去一年中超过三分之一的组织因证书过期而发生服务中断。接近四分之三的组织累计有至少 5 小时的证书相关停机,五分之一达到 25 小时以上,接近四分之一表示其最严重的一次证书事故损失超过 25 万美元DigiCert)。

这些数字里最值得琢磨的是时长。5 小时并不是续期需要的时间——续期只要几秒。5 小时是有人发现它所花的时间。

自动续期为什么还不够?

因为续期自动化是静默失败的,而一个已经不再运行的定时任务不会产生任何输出。 下面每一种都是真实且常见的故障,而且都不发出任何声音:

  • **定时器早就没在跑了。**一次系统升级、一次容器重建,或者三个月前某次没人记得的 systemctl disablecertbot.timer 没触发,看起来和成功触发一模一样。
  • **续期成功了,但服务从没重载。**新证书已经落到磁盘上,而 nginx、HAProxy 或 Postfix 内存里握着的还是旧的。这是「全自动」配置最常见的过期方式。
  • **验证路径坏了。**有人在 /.well-known/acme-challenge/ 前面加了跳转、WAF 规则或一条 Deny,HTTP-01 就失败了;或者 DNS-01 用的那个 DNS 服务商 API token 过期了。
  • **只有一个节点续了期。**两台负载均衡器,一个定时任务。第二台会一直提供旧证书,直到它不能再提供为止。
  • **那张证书根本不在 Web 服务器上。**内部 mTLS 客户端、Kafka broker、LDAP 服务器、VPN 网关、设备管理推送证书。公网上什么都看不到,也没有任何 ACME 客户端在管它。
  • **续期间隔被写死成了错的数值。**Let's Encrypt 对这一条说得很直接:当默认配置降到 64 天、再到 45 天之后,「把续期间隔硬编码成 60 天将不再够用」(Let's Encrypt)。

最后一条值得加重语气,因为它是日历正把所有人推进去的那个坑。如果你的续期节奏是某人在 2022 年敲进去的一个数字,那么证书有效期现在正朝着这个数字迎面走来。

怎么用命令行查证书的到期时间?

一条 openssl 管道,不需要任何依赖。 查线上主机:

openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -issuer -startdate -enddate

查磁盘上的文件:

openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -enddate

在共享 IP 上,-servername 不是可选项——不带 SNI 拿到的是服务器认定的默认证书,未必是你担心的那一张。

如果只要一个是非判断,可以完全跳过日期解析。openssl x509 -checkend <秒数> 在证书能挺过这个窗口时退出码为 0,在窗口内到期(包括已经过期)时退出码为 1

openssl x509 -in fullchain.pem -noout -checkend $((14 * 86400)) \
  || echo "14 天内到期"

这个退出码约定就是整个监控的原语,下面的一切都只是围着它接管道而已。

抓住「续了期但没重载」这种情况

把磁盘上的那张和实际在提供的那张比一比。 这是几乎没人做的检查,而它恰好能抓到续期自动化自己看不见的那种故障:

served=$(openssl s_client -connect 127.0.0.1:443 -servername example.com </dev/null 2>/dev/null \
         | openssl x509 -noout -fingerprint -sha256)
ondisk=$(openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -fingerprint -sha256)

[ "$served" = "$ondisk" ] || echo "服务仍在提供旧证书 —— 需要重载"

两条命令输出的格式完全相同(sha256 Fingerprint=AB:CD:...),所以直接做字符串比较就够。在续期定时器的时间窗之后几分钟跑它。

证书告警该用什么阈值?

用证书有效期的比例,而不是固定天数。 「提前 30 天提醒」这条规则对 90 天证书是合理的;套到 47 天证书上,它会对一张完全健康的证书报警;套到 6 天证书上,它会永远在响。

应该把这套梯度锚定在续期点上。Let's Encrypt 建议在「当前证书有效期约三分之二处」续期——也就是说,剩余三分之一有效期正是续期本应已经发生的时刻。这个点之后的一切,都是它没有发生的证据:

剩余有效期含义通知类型
1/3续期窗口已打开不通知——这很正常
1/6续期窗口错过了一次普通推送
1/12续期是在失败,不只是迟到时效性
< 1/24、已过期或不可达你离故障只剩几个小时来电

换成具体数字,对一张 47 天的证书大致是:7.8 天之前保持安静,3.9 天推送,2 天时效性,不到 1 天打电话。对 90 天证书是 15 天、7.5 天、3.75 天。对 6 天证书则以小时计,这时候人工梯度已经不是合适的工具了——应当依靠 ACME Renewal Information(ARI,已作为 RFC 9773 发布),让 CA 直接告诉你的客户端何时续期,而你只在客户端反复失败时才告警。

算这个比例只要四行,而且它能让同一个脚本对你名下每一张证书都成立,不管签发方是谁:

to_epoch() {
  date -u -d "$1" +%s 2>/dev/null || date -u -j -f '%b %d %T %Y %Z' "$1" +%s 2>/dev/null
}

pct_left() {  # 从标准输入读 PEM,打印剩余有效期占总有效期的百分比
  local pem nb na
  pem=$(cat)
  nb=$(to_epoch "$(printf '%s' "$pem" | openssl x509 -noout -startdate | cut -d= -f2)")
  na=$(to_epoch "$(printf '%s' "$pem" | openssl x509 -noout -enddate | cut -d= -f2)")
  echo $(( (na - $(date -u +%s)) * 100 / (na - nb) ))
}

第一种 date 写法是 GNU 的,第二种是 BSD/macOS 的;|| 会挑中你机器上有的那个。

怎么把它变成一条真的能触达我的告警?

Echobell 把 Webhook 或邮件变成普通推送、时效性通知,或者一通真正响铃、能穿透专注模式与勿扰模式的电话(见绕过 iOS 专注模式)。对证书来说这一点很重要,因为上面梯度的最后一档,往往是你在周日凌晨三点撞上的那一档。

第一步 —— 建两个频道,而不是一个

在 App 里建一个频道,通知类型设为时效性,命名为「证书即将到期」。再建第二个,类型设为来电,命名为「证书即将失效」。分别从频道详情里复制 Webhook URL,形如 https://hook.echobell.one/t/<channel-token>。把它们当密钥对待——拿到那个「来电」URL 的人就能让你的手机响(Webhook 指南)。

把模板写成在锁屏上不解锁就能行动的样子:

标题:TLS {{state}}:{{host}}
正文:还剩 {{daysLeft}} 天,{{notAfter}} 到期 —— 签发者 {{issuer}}

你 POST 过去的任何 JSON 键都会变成变量(模板)。

第二步 —— 跑检查

下面是把前面各节拼起来、端到端实测过的脚本。存成 /usr/local/bin/cert-watch

#!/usr/bin/env bash
# cert-watch —— 当 TLS 证书临近到期时,向 Echobell 发送通知。
set -uo pipefail

HOOK="${ECHOBELL_CERT_HOOK:?请把 ECHOBELL_CERT_HOOK 设为你的频道 Webhook URL}"
WARN_DAYS="${WARN_DAYS:-14}"

post() {
  curl -sS -m 10 -X POST "$HOOK" \
    -H 'content-type: application/json' \
    -d "{\"host\":\"$1\",\"daysLeft\":$2,\"notAfter\":\"$3\",\"state\":\"$4\"}" \
    >/dev/null
}

days_left() {
  local end
  end=$(date -u -d "$1" +%s 2>/dev/null) ||
    end=$(date -u -j -f '%b %d %T %Y %Z' "$1" +%s 2>/dev/null) || return 1
  echo $(( (end - $(date -u +%s)) / 86400 ))
}

check() {
  local host="$1" port="$2" pem state not_after days

  pem=$(openssl s_client -connect "$host:$port" -servername "$host" \
        </dev/null 2>/dev/null | openssl x509 2>/dev/null)

  if [ -z "$pem" ]; then
    post "$host" 0 "" "unreachable"
    return
  fi

  if printf '%s' "$pem" | openssl x509 -noout -checkend 0 >/dev/null 2>&1; then
    printf '%s' "$pem" | openssl x509 -noout -checkend $((WARN_DAYS * 86400)) >/dev/null 2>&1 && return 0
    state="expiring"
  else
    state="expired"
  fi

  not_after=$(printf '%s' "$pem" | openssl x509 -noout -enddate | cut -d= -f2)
  days=$(days_left "$not_after") || days=-999
  post "$host" "$days" "$not_after" "$state"
}

for target in "$@"; do
  case "$target" in
    *:*) check "${target%:*}" "${target##*:}" ;;
    *) check "$target" 443 ;;
  esac
done

它会上报三种状态——expiringexpiredunreachable——一切正常时保持沉默。目标写成 hosthost:port,所以非 Web 的证书也照样覆盖:

ECHOBELL_CERT_HOOK="https://hook.echobell.one/t/<token>" \
  cert-watch example.com api.example.com mail.example.com:993 ldap.internal:636

unreachable 被特意设计成一条告警,而不是静默跳过。一个把「我没看成」当作「一切正常」的监控,正是证书会过期的根本原因。

第三步 —— 给它排期,并在排期本身失败时告警

对 45 天及更长的证书,一天一次就够;如果你在用 6 天期证书,一天两次。systemd timer:

# /etc/systemd/system/cert-watch.service
[Unit]
Description=TLS certificate expiry check

[Service]
Type=oneshot
EnvironmentFile=/etc/echobell.env
ExecStart=/usr/local/bin/cert-watch example.com api.example.com mail.example.com:993
# /etc/systemd/system/cert-watch.timer
[Unit]
Description=Daily TLS certificate expiry check

[Timer]
OnCalendar=daily
RandomizedDelaySec=1h
Persistent=true

[Install]
WantedBy=timers.target

Persistent=true 很关键:没有它,机器在预定时刻处于关机状态,那一次就直接跳过了。

然后把检查自身也闭环。Type=oneshot 的单元非零退出会触发 OnFailure=,于是一个 drop-in 就能让坏掉的续期自己开口:

# /etc/systemd/system/certbot.service.d/echobell.conf
[Unit]
OnFailure=echobell-alert@%n.service
# /etc/systemd/system/echobell-alert@.service
[Unit]
Description=Echobell alert for %i

[Service]
Type=oneshot
EnvironmentFile=/etc/echobell.env
ExecStart=/usr/local/bin/echobell-notify %i

其中 /usr/local/bin/echobell-notify 只有三行:

#!/usr/bin/env bash
curl -sS -m 10 -X POST "$ECHOBELL_CERT_HOOK" \
  -H 'content-type: application/json' \
  -d "{\"host\":\"$(hostname -f)\",\"state\":\"renewal-failed\",\"unit\":\"$1\",\"daysLeft\":-1}"

只要有任何一张证书续期失败,certbot renew 就会以非零码退出——这正是你想要的信号,也正是目前哪儿都没送到的信号。注意 certbot 的 --deploy-hook 只在成功时执行,所以它没法用来做这件事,失败路径必须从单元这一侧来。

第四步 —— 用条件把梯度分开

两个频道收到的是同一份 payload,由条件决定哪一个真的触发。时效性频道:

state == "expiring" && daysLeft > 3

来电频道:

state == "expired" || state == "unreachable" || state == "renewal-failed" || daysLeft <= 3

注意 <= 会用 Number() 强制转换两侧,所以即使 daysLeft 是以字符串发出去的,比较依然是数值比较。条件里没有「包含」运算符,这也正是脚本发一个显式的 state 字段、而不是一段需要你去做模式匹配的自由文本的原因。

第五步 —— 在信任它之前先测一次

把脚本指向一台证书已知有问题的主机,看告警是不是真的到了:

ECHOBELL_CERT_HOOK="https://hook.echobell.one/t/<token>" cert-watch expired.badssl.com

测试时请在真正会收到通知的那台手机上打开勿扰模式,并在 App 里启用重试失败来电,好让被专注模式挡掉的电话再拨一次。一条从没触发过的升级路径,只是一个猜测。

如果我的监控已经在查证书了呢?

那就把它现成的 Webhook 接到频道上,跳过脚本。 大多数监控本来就知道到期时间,它们通常缺的是一条能穿透「人在睡觉」的路径。

  • Uptime Kuma 自带证书到期通知,把它指向一个频道即可(Uptime Kuma 指南电话告警配置)。
  • Prometheus + Alertmanager 配 blackbox exporter 会给你 probe_ssl_earliest_cert_expiry,基于它写告警并路由到频道(Prometheus 指南Alertmanager 电话告警)。
  • Grafana 的告警规则可以直接 POST 到频道(Grafana 指南)。
  • UpptimeUptimeRobot 都能覆盖公开端点(UpptimeUptimeRobot)。
  • 只会发邮件的那些——CA 自己的控制台、云厂商的 ACM 提醒、内部 PKI——用一条转发规则就行。每个频道都有自己的邮箱地址,fromtosubjecttexthtml 都可以作为变量使用(邮件触发)。

上面这些都替代不了的一件事,是让检查从提供证书的那台机器之外发起。如果你的监控和被监控对象住在同一台主机上,那么一次让主机宕掉的故障,会顺手把告警一起带走。

Echobell 不做什么

这里有必要说清楚,因为证书管理这个品类里,能做的事比这多得多的产品到处都是。

**Echobell 会做:**把 Webhook 或邮件变成普通推送、时效性通知或响铃电话;用条件过滤;用模板格式化;把同一次触发送达共享频道的每一位订阅者,各自选择自己的紧急程度。

Echobell 不会做:

  • **发现或盘点你的证书。**它不会扫描你的网络,不会爬证书透明日志,也不会告诉你那张没人记得签发过的证书。上面的脚本只检查你列出来的主机。证书蔓延是个真问题,而这不是它的解法。
  • **续期。**它没有 ACME 客户端,也碰不到你的密钥。它只告诉你续期坏了,修还是你的事。
  • **按自己的节奏去检查证书。**没有托管探针。必须由你这边跑的某个东西——定时器、CI 任务、你现有的监控——去做那次查看。
  • **提供值班轮换、升级策略或签收确认。**没有「五分钟无人接听就打给下一个人」。如果你需要这些,你需要的是事件管理平台——可参考 Opsgenie 替代方案对比
  • **保证送达。**电话依赖推送基础设施、网络,以及一部有电的手机。它缩短的是「坏掉」与「知道」之间的距离,而不是一个可以绝对依赖的控制措施。

常见问题

Let's Encrypt 真的不再发到期邮件了吗?

是的。到期提醒服务已于 2025 年 6 月 4 日终止,Let's Encrypt 也删除了此前与签发记录关联保存的邮箱地址。公告建议改用第三方监控,并举了 Red Sift Certificates Lite 为例(250 张证书以内免费)。如果你一年多没收到过 Let's Encrypt 的到期提醒,原因就在这里——而不是因为从来没有东西快过期。

2026 年 TLS 证书的有效期是多久?

自 2026 年 3 月 15 日起,公开信任的 TLS 证书最长 200 天。按 SC-081v3 议案,2027 年 3 月 15 日降到 100 天,2029 年 3 月 15 日降到 47 天。各家 CA 出于安全会签在上限之下——例如 DigiCert 签发 199 天的证书,明确是「为了避免超出允许的最长有效期」。Let's Encrypt 按自己的节奏走得更远也更快,6 天期证书自 2026 年 1 月起已全量开放。

如果我用了 ARI,还需要到期告警吗?

需要,但告警的事件不一样了。ARI(RFC 9773)告诉你的 ACME 客户端什么时候续期,这彻底消灭了「硬编码间隔」这种故障——但它不保证续期成功,不保证服务重载,也不保证客户端还活着。请对「续期反复失败」和「在提供的证书与磁盘上的不一致」告警,而不是对一个你已经不需要自己管的倒计时告警。

不在 Web 服务器上的证书怎么办?

那些恰恰是最容易过期的,因为没有 ACME 客户端盯着它们,在出事之前也没有浏览器会抗议。脚本接受 host:port,所以 993 上的 IMAP、636 上的 LDAPS、9093 上的 Kafka broker、8443 上的内部 API,用法完全一样。至于那些根本不经过套接字的证书——代码签名、推送通知证书、设备群里的客户端证书——需要从它们所在的地方取出日期,再 POST 到同一个频道。

每天检查会不会变成噪音?

只要它在一切正常时保持沉默就不会,而这正是脚本在阈值以上什么都不发的原因。会变成噪音的是另一种设计:每天报一次「证书正常」——两周之后没人再看,而它停止到达的那一天也没人会注意到。如果你确实想要心跳,把它放在单独的普通频道上,永远别放在会响铃的那个上。这个论证的一般版本见消除告警疲劳

整个团队能一起收到证书告警吗?

可以。共享频道之后,每位订阅者都会收到触发,并各自选择通知类型。一种可行的分工:当班的人订阅来电频道,其他人订阅时效性频道,这样凌晨三点的到期只吵醒一个人,而不是六个。

在 macOS 上能用吗?

能,只有一个注意点:BSD 的 date 不接受 -d,这正是 days_leftto_epoch 先试 GNU 写法、再回落到 date -u -j -f 的原因。macOS 默认的 openssl 是 LibreSSL,-checkend-fingerprint 的行为完全一致。即使你用 Homebrew 装了 OpenSSL,也不需要改什么。

把证书信息放进通知里安全吗?

这里用到的字段——主机名、到期日期、签发者——都是公开的,任何人用同样的 openssl 命令都能从你的服务器上读到。不要往 payload 里加私钥、内部路径,或者非公开主机上证书除主机名以外的内容。Echobell 的通知内容与历史只保存在你的设备上,服务器只留账号、频道与订阅关系(隐私模型),这是个不错的默认设定,但不构成「可以多发一点」的理由。


相关阅读