证书这东西很安静。它快过期的时候,Nginx 不会弹提醒,只会默默把访客那条 HTTPS 请求挡在门外。等你发现,往往已经是用户先来报障了。与其靠回忆续证,不如让系统在到期前自己盯一遍。核心就是 OpenSSL 的两个命令:查本地文件的 x509,和从线上拉真实证书的 s_client。
证书快过期,Nginx 不会提前告诉你
证书的有效期写在 notAfter 字段,浏览器和 Nginx 都只认这个时间。更稳妥的做法,是把「还剩几天」变成定时检查,同时看本地和线上两处,因为这两边真不一定一致。
先看清楚:本地证书文件还剩几天
本地证书一般是服务器上的 PEM 文件,先读出有效期:
openssl x509 -in /etc/nginx/ssl/example.com/fullchain.pem -noout -dates
输出会给出 notBefore(生效)和 notAfter(到期),注意别指向错误目录里的旧证书。
checkend 的退出码:0 和 非0 分别是什么意思
脚本不能只读日期,x509 的 -checkend 可直接用退出码判断,详见OpenSSL 官方文档:指定秒数内到期返回非零,否则返回零。
openssl x509 -in /etc/nginx/ssl/example.com/fullchain.pem -checkend 604800 -noout; echo $?
也就是说 -checkend 返回 0 代表「安全」,返回非 0 代表「该动手了」,和「0 表示成功」的习惯相反,写脚本判断时要取反。这是新手最容易看走眼的点,建议先手动跑一次把退出码看清楚。
604800 从哪来:7 天阈值怎么定
为什么是 604800?因为检查以「秒」为单位,7 天就是 7 × 24 × 60 × 60 秒:
echo $((7 * 24 * 60 * 60)) # 604800
阈值 604800 代表「提前 7 天」,想更保守就改成 14 天 1209600。7 天能少打扰,又留出处理时间。
从线上看:s_client 拿到真实下发的证书
检查本地文件有个漏洞:线上真正生效的,未必就是你服务器上那份文件。域名前面挂了 CDN、负载均衡、WAF 时,真正下发给访客的是它们那份证书。所以还得「从访客的视角」连一次线上。这个用 s_client,参数见OpenSSL 官方 s_client 文档。
echo | openssl s_client -connect www.example.com:443 -servername www.example.com -showcerts 2>/dev/null \
| openssl x509 -noout -dates
-servername 会在 ClientHello 中带上 SNI,让服务器按域名选证书;少了它,多域名服务器可能返回默认站证书。注意用 echo | 或重定向收尾,避免 s_client 一直等输入。
把检查写成脚本:本地 + 线上一起跑
把两步合成脚本,同时检查本地文件和线上证书:
#!/usr/bin/env bash
HOST="www.example.com"; PORT="443"; THRESHOLD=604800
CERT="/etc/nginx/ssl/example.com/fullchain.pem"
# 1) 本地文件
if [ -f "$CERT" ]; then
if openssl x509 -in "$CERT" -checkend $THRESHOLD -noout 2>/dev/null; then
echo "OK 本地剩余有效期超过 7 天"
else
echo "WARN 本地将在 7 天内到期,需要续证"
fi
else
echo "WARN 找不到本地证书:$CERT"
fi
# 2) 线上真实下发
REMOTE=$(echo | openssl s_client -connect "$HOST:$PORT" -servername "$HOST" -showcerts 2>/dev/null \
| openssl x509 -noout -dates 2>/dev/null)
[ -n "$REMOTE" ] && echo "线上 $HOST 有效期:$REMOTE" \
|| echo "WARN 无法从线上取到证书(检查 SNI / 端口 / 网络)"
脚本里 if openssl ... -checkend; then 正是利用前面的退出码:只有剩余时间还够才进「OK」分支。重点是「文件不存在」「证书损坏」「网络取不到」这些异常都要当 WARN 处理,不能静默退出。
用 systemd 定时触发:service + timer 配对
接下来让它每天自动跑。systemd 用「service + timer」配对,timer 到点触发 service。OnCalendar、Persistent、RandomizedDelaySec 见官方手册:
# /etc/systemd/system/cert-check.service
[Unit]
Description=Check certificate expiry
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/check_cert.sh
# /etc/systemd/system/cert-check.timer
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=30min
[Install]
WantedBy=timers.target
OnCalendar=*-*-* 03:00:00 是每天凌晨 3 点。Persistent=true 会存上次触发时间,机器断电漏跑了下次开机补上。RandomizedDelaySec=30min 在 3 点基础上随机偏移最多 30 分钟,避免一堆定时任务整点挤一起。先 daemon-reload 再 enable:
sudo systemctl daemon-reload
sudo systemctl enable --now cert-check.timer
systemctl list-timers cert-check.timer # 看下一次触发时间
常见误判与验收清单
这套东西跑起来不难,容易踩的是「看似正常、其实没对」。整理成一个表:
| 你看到的 | 背后真实原因 | 怎么验 |
|---|---|---|
| 本地证书还早,线上却提示快过期 | CDN / 负载均衡 / WAF 用的是另一张证书 | 用 s_client -servername 对比域名和源站的 notAfter |
| 证书能打开,但某个子域名报错 | 单域名或 SAN 里没带这个域名 | 查证书 Subject Alternative Name 是否包含该域名 |
| 域名解析到 CDN,拿到的是回源证书 | 握手对象不对,SNI 指到了别处 | 指定 -servername,确认连的是对外那一侧 |
最后核对一遍,都满足才算真做对了:
- 本地证书能正常
openssl x509 -in ... -dates读出 notAfter,且-checkend 604800未到期返回 0。 - 线上能通过
s_client -servername拿到目标域名真实证书,不是默认站证书。 - 脚本在本地证书缺失、文件损坏、网络取不到时都能给出非 0 或 WARN,不假装成功。
- systemd service 用
Type=oneshot,timer 带Persistent=true,手动systemctl start cert-check.service能执行,list-timers能看到下一次触发时间。
