核心是解析证书notafter时间并计算剩余天数:先校验证书可读,再用openssl提取enddate、标准化时区后转为时间戳,与当前时间差值除以86400得整数天数,≤7则告警或触发reload。

如何判断Nginx证书是否将在7天内过期
核心是解析证书的 notAfter 时间,不能只靠 openssl x509 -in cert.pem -enddate 手动看——得用脚本算出剩余天数。关键点在于:OpenSSL 输出格式不统一(不同版本可能带时区缩写或不带),date -d 解析失败率高;更稳的方式是用 openssl x509 -in cert.pem -enddate -noout 提取原始字符串,再用 date -f 或 awk + date -d 做标准化转换。
常见错误现象:date: invalid date 'Jan 15 12:34:56 2025 GMT'——因为部分系统默认不认 GMT,得替换成 +0000;或者证书路径写错导致 openssl 报错但脚本没检查退出码,后续逻辑全错。
- 先校验证书文件存在且可读:
[[ -r "/etc/nginx/ssl/example.com.crt" ]] - 提取到期时间并转为 Unix 时间戳:
openssl x509 -in /etc/nginx/ssl/example.com.crt -enddate -noout | awk -F'= ' '{print $2}' | sed 's/ GMT$//; s/$/ +0000/' | xargs -I{} date -d {} +%s 2>/dev/null - 计算差值:
expr \( $expires_epoch - $(date +%s) \) / 86400(整除得剩余天数)
证书更新后如何安全触发 Nginx 热重载
不能直接 nginx -s reload 就完事。如果新证书格式错误、权限不对、或私钥密码未移除,reload 会失败,但 Nginx 进程还在跑旧配置——表面没事,实际新证书根本没生效。必须加检查环节。
使用场景:ACME 工具(如 acme.sh 或 certbot)更新完证书后调用此脚本;或定时任务每小时检查一次。
- 更新前先测试配置:
nginx -t,失败则中止,不 reload - 确认证书和私钥权限为
644和600(nginx用户需能读证书,但私钥不能被组/其他用户读) - 执行重载:
kill -s HUP $(cat /var/run/nginx.pid)或systemctl reload nginx(推荐后者,兼容 systemd 环境) - reload 后检查是否成功:
ss -tlnp | grep :443看 worker 进程是否已更新,或用curl -I --insecure https://example.com 2>/dev/null | grep "HTTP/"快速探活
把检查和重载打包成可 cron 调用的 Bash 脚本
脚本必须能独立运行,不依赖交互、不假设当前目录、不忽略错误退出码。尤其注意:cron 默认 PATH 很窄,openssl、date、nginx 路径要写全或显式设置 PATH。
性能影响极小,但若放在分钟级 cron 里,建议加随机 sleep 避免多台机器同时请求 Let's Encrypt 接口(不过本例只读本地证书,无此压力)。
#!/bin/bash
PATH="/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin"
<p>CERT="/etc/nginx/ssl/example.com.crt"
KEY="/etc/nginx/ssl/example.com.key"
DAYS_WARN=7</p><p>if [[ ! -r "$CERT" ]]; then
echo "ERROR: cert not readable: $CERT" >&2
exit 1
fi</p><h1>获取到期时间戳</h1><p>expires_epoch=$(openssl x509 -in "$CERT" -enddate -noout 2>/dev/null | \
awk -F'= ' '{print $2}' | \
sed 's/ GMT$//; s/$/ +0000/' | \
xargs -I{} date -d {} +%s 2>/dev/null)</p><p>if [[ -z "$expires_epoch" || "$expires_epoch" -lt "$(date +%s)" ]]; then
echo "ERROR: cert invalid or expired" >&2
exit 1
fi</p><p>days_left=$(( (expires_epoch - $(date +%s)) / 86400 ))
if [[ $days_left -gt $DAYS_WARN ]]; then
exit 0
fi</p><p>echo "INFO: cert expires in $days_left days, reloading nginx..."</p><p>if ! nginx -t >/dev/null; then
echo "ERROR: nginx config test failed" >&2
exit 1
fi</p><p>if ! systemctl reload nginx; then
echo "ERROR: nginx reload failed" >&2
exit 1
fi</p>
为什么不能只靠 certbot 的 --deploy-hook
certbot 自带的 --deploy-hook 只在证书真正更新后触发,但你没法让它“提前7天检查并 reload”——它不监听到期倒计时。如果你用的是 acme.sh,它也没内置倒计时钩子。所以必须另起定时任务,独立监控证书有效期。
容易踩的坑:
- 误以为
certbot renew --dry-run会触发 deploy-hook:不会,dry-run 不改任何文件 - 在
--deploy-hook里写 reload,但没处理证书尚未部署完成的竞态(比如 hook 执行时 Nginx 正在读旧证书,reload 后瞬间用错路径) - 脚本加入 cron 但没重定向 stdout/stderr,日志丢失,出问题时完全没线索
真正可靠的模式是:一个脚本专注“查+reload”,由 cron 拉起;另一个流程(如 certbot 定时 renew)只管更新文件。两者解耦,各自可测、可调、可查。











