结论: 证书过期(SSL Certificate Expired)的处理分三步:先用 openssl x509 -noout -dates 确认确实是证书过期而非配置错误;再用 certbot renew 续期(失败就加 --force-renewal 或换 DNS 验证);最后必须 systemctl reload nginx 重载配置,不重载的话磁盘上换了新证书、进程里还是旧的。同时把续期失败的根因(80 端口被占、DNS 验证失败、权限不足)修掉,否则 90 天后再次过期。
浏览器提示 NET::ERR_CERT_DATE_INVALID、curl 报 SSL certificate expired、Java 报 PKIX path validation failed、监控告警"证书剩余 0 天"——这些都是同一个问题。它的特殊性在于:不是突发故障,而是可预测、可自动化、却仍然高频发生的故障。Let's Encrypt 签发的免费证书有效期为 90 天(截至 2026 年仍是 90 天),一年要续三次,任何一次自动化断链都会出事。
怎么确认是证书过期,而不是别的问题?
结论:用 openssl s_client 从外部握手一次,看返回的证书起止日期;这是唯一可信的判断方式。
不要只看磁盘上的证书文件——很可能磁盘上已经是新证书,但 Nginx 进程还加载着旧的。
# 从任意机器远程检查(最权威,含 SNI)
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
# 只取过期时间
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -enddate
# 输出示例:notAfter=Mar 15 07:59:59 2026 GMT
# 检查本机证书文件是否在 7 天(604800 秒)内过期,返回值 0=未过期,1=即将过期
openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem -noout -checkend 604800
echo $?若握手直接失败(连接被拒),那是端口或服务问题;若握手成功但日期已过,才是证书过期。两者处置完全不同。
证书续期:certbot 的标准流程
结论:先 --dry-run 演练验证验证方式可用,再正式续期;续期后一定要 reload 而不是 restart。
# 1. 查看本机所有证书及其剩余天数
sudo certbot certificates
# 2. 演练(不真正签发,用于验证 80 端口/ DNS 验证通道是否通畅)
sudo certbot renew --dry-run
# 3. 正式续期(只续期剩余不足 30 天的证书)
sudo certbot renew
# 4. 强制续期(即使还没到期也重新签发,用于排查或换算法)
sudo certbot renew --force-renewal --cert-name example.com
# 5. 续期后自动重载(推荐写进 deploy-hook,一劳永逸)
sudo certbot renew --deploy-hook "systemctl reload nginx"certbot renew 默认只在证书剩余有效期小于 30 天时才真正向 CA 请求新证书,其余情况直接跳过(输出 Certificate not yet due for renewal)。所以"执行了 renew 但网站还是过期提示"通常不是续期失败,而是没有 reload。
新增证书或换验证方式时使用 certbot certonly:
# 方式 A:webroot(推荐,不中断现有 Web 服务)
sudo certbot certonly --webroot -w /var/www/html -d example.com -d www.example.com
# 方式 B:standalone(适合还没跑 Web 的机器,需 80 端口空闲)
sudo systemctl stop nginx
sudo certbot certonly --standalone -d example.com
sudo systemctl start nginx
# 方式 C:DNS 验证(泛域名 *.example.com 必须用这种)
sudo certbot certonly --manual --preferred-challenges dns \
-d example.com -d '*.example.com'
# 按提示在域名控制台添加 _acme-challenge TXT 记录,确认解析生效后再回车证书文件路径要记牢:
| 文件 | 内容 | 用途 |
|---|---|---|
cert.pem | 服务器证书(叶子证书) | 一般不用单独引用 |
chain.pem | 中间 CA 证书 | 排查链问题时用 |
fullchain.pem | cert.pem + chain.pem | Nginx/Apache 应指向这个 |
privkey.pem | 私钥 | 权限必须 600,切勿外传 |
续期之后为什么网站还是提示过期?
结论:因为 Web 服务器进程内存中仍持有旧证书。certbot renew 只换磁盘文件,不通知进程。
这是最高频的"以为修好了其实没修好"陷阱。
# Nginx:先语法检查再平滑重载(不中断连接)
sudo nginx -t && sudo systemctl reload nginx
# Apache(Debian/Ubuntu 叫 apache2,RHEL 系叫 httpd)
sudo apachectl configtest && sudo systemctl reload apache2
sudo apachectl configtest && sudo systemctl reload httpd
# 验证进程是否加载了新证书:看进程启动时间与当前握手返回的日期
sudo ps -o lstart,cmd -C nginx
echo | openssl s_client -connect 127.0.0.1:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -datesNginx 的 SSL 配置正确写法(注意是 fullchain.pem):
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
# OCSP Stapling,减少客户端校验耗时
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
resolver 223.5.5.5 119.29.29.29 valid=300s;
}reload 与 restart 的区别:reload 会启动新 worker 并优雅关闭旧 worker,已有连接不中断;restart 会短暂断开。生产环境一律用 reload。
证书链不完整(Incomplete Chain)怎么修?
结论:把 ssl_certificate 指向 fullchain.pem 而不是 cert.pem,99% 的"链不完整"问题就解决了。
证书链(Certificate Chain)是指"服务器证书 → 中间 CA 证书 → 根证书"的信任路径。如果服务器只发了叶子证书,客户端(尤其是 Android、Java、老旧 OpenSSL)无法自行补全中间证书,就会报 unable to get local issuer certificate。
典型症状很迷惑:Chrome 能正常访问,但 Android App 或 Java 程序报证书错误。原因是 Chrome 会用 AIA 扩展主动下载中间证书,而很多程序不会。
# 查看服务器实际发送的证书链有几层
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| grep -E '^[0-9]+ s:|^[0-9]+ i:|Verify return code'
# 正常应看到 2 层(s:叶子 / i:中间CA),且 Verify return code: 0 (ok)
# 验证链是否可由本机 CA 库完整校验
openssl verify -CAfile /etc/letsencrypt/live/example.com/chain.pem \
/etc/letsencrypt/live/example.com/cert.pem修复清单:
- Nginx:
ssl_certificate改为fullchain.pem - Apache:
SSLCertificateFile指向fullchain.pem,SSLCertificateKeyFile指向privkey.pem - Tomcat:keystore 导入时必须用
-chain参数把中间证书一起导入 - CDN / SLB:证书填写框通常要求"证书内容 + 中间证书"拼在一个 PEM 里,顺序为叶子在前、中间在后
自动续期为什么失败?逐条对照
结论:自动续期失败集中在三类——HTTP-01 验证时 80 端口被占或路径被重定向、DNS-01 验证时 TXT 记录没生效、以及文件权限不足。
| 失败原因 | 报错特征 | 排查与修复 |
|---|---|---|
| 80 端口被占用 | Problem binding to port 80: Could not bind to IPv4 or IPv6 | ss -lntup 'sport = :80' 找占用者;改用 --webroot 模式或临时停 Nginx |
.well-known 路径被 301/403 | Invalid response ... 404/Unauthorized | 检查 Nginx 是否对 /.well-known/acme-challenge/ 做了重定向或鉴权,需单独放行 |
| 反向代理拦截验证请求 | Connection refused 或返回 502 | 在代理层为 /.well-known/ 加单独 location 走本地目录 |
| DNS TXT 记录未生效 | DNS problem: NXDOMAIN looking up TXT | dig -t txt _acme-challenge.example.com +short 确认;海外 CA 需等海外 DNS 生效 |
| 泛域名用了 HTTP 验证 | Client with the currently selected authenticator does not support any combination of challenges | 泛域名只能用 --preferred-challenges dns |
| 权限不足 | Permission denied / Failed to create directory | 用 root 或 sudo 执行;检查 /etc/letsencrypt 属主 |
| 达到速率限制 | too many certificates already issued for ... | Let's Encrypt 每周每域名 50 张上限,停止重试,改用 --dry-run 之外的方式调试 |
| 私钥权限过宽 | Unsafe permissions | chmod 600 privkey.pem |
排错第一步永远是 certbot renew --dry-run 加 -v 看详细日志,日志本身会明确告诉你是哪一步失败。
一个常见的 Nginx 陷阱:全站 HTTP 跳 HTTPS 时把 /.well-known 也跳走了,导致验证请求拿到 301。正确写法:
server {
listen 80;
server_name example.com;
# ACME 验证路径不走跳转
location ^~ /.well-known/acme-challenge/ {
root /var/www/html;
default_type "text/plain";
}
location / {
return 301 https://$host$request_uri;
}
}怎么做到"永不过期"?监控与告警
结论:三层保险——certbot 自带的 systemd timer 自动续期 + deploy-hook 自动重载 + 独立的剩余天数监控告警。
现代 certbot 装完会自动注册定时任务,先确认它在:
systemctl list-timers | grep certbot
systemctl status certbot.timer
# 若没有,手动建 cron(每周一凌晨 3 点,随机延迟避免集中请求)
# 0 3 * * 1 root sleep $((RANDOM % 1800)) && certbot renew --quiet --deploy-hook "systemctl reload nginx"但定时器和监控不能互相替代。写一个独立探针,即使续期彻底失效也能提前 30 天告警:
#!/usr/bin/env bash
# /usr/local/bin/check-cert-expiry.sh
DOMAIN="example.com"
WARN_DAYS=30
END=$(echo | openssl s_client -connect "${DOMAIN}:443" -servername "${DOMAIN}" 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)
END_EPOCH=$(date -d "$END" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( (END_EPOCH - NOW_EPOCH) / 86400 ))
if [ "$DAYS_LEFT" -lt "$WARN_DAYS" ]; then
echo "警告:${DOMAIN} 证书仅剩 ${DAYS_LEFT} 天,请检查 certbot 续期"
exit 1
fi
echo "OK:${DOMAIN} 证书剩余 ${DAYS_LEFT} 天"配合 Prometheus + blackbox_exporter 则更省事,指标 probe_ssl_earliest_cert_expiry 直接给出到期时间戳,告警规则写 probe_ssl_earliest_cert_expiry - time() < 21 * 86400 即可。
证书过期之后怎么复盘,避免 90 天后再犯?
结论:证书过期是可预测故障,修完当次必须复盘,把"人肉续期"改成"自动续期加到期告警"的双保险,否则 90 天后大概率重演。
复盘时按顺序回答四个问题:自动续期任务是否真的注册并在运行,用 systemctl status certbot.timer 与 systemctl list-timers 确认;续期成功后是否配置了 deploy-hook 自动重载,否则磁盘换了新的、进程还是旧的;是否存在多台后端服务器或 CDN、负载均衡节点漏续,这在集群环境里最常见;到期告警探针是否独立于 certbot 之外,能在自动续期彻底失效时提前 30 天通知到人。前三条决定续期能否自动完成,最后一条决定自动失效时你能不能提前知道。
| 复盘项 | 合格标准 | 不合格时的动作 |
|---|---|---|
| 续期定时器 | certbot.timer 状态为 active | 手动补建 cron 或 systemd timer,每周至少跑一次 |
| 自动重载 | 配置了 --deploy-hook | 补上 reload 命令并做一次 --dry-run 演练 |
| 多节点覆盖 | 所有后端节点都已续期 | 逐台执行 renew 与 reload,或用配置管理工具批量下发 |
| 到期告警 | 剩余 30 天能收到通知 | 部署 blackbox_exporter 或自写 openssl 探针 |
| 私钥安全 | 权限 600 且未进入代码库 | 立即 revoke 并重新签发,同时排查泄露路径 |
把这张表固化进日常巡检清单,证书过期就不再是"半夜被叫醒"的突发事故,而是一条可以在工作时间从容处理的例行检查项。截至 2026 年,Let's Encrypt 仍在推进更短有效期证书的自动化签发方案,提前把这套自动化跑通,后续即使有效期进一步缩短也不会被动。
企业QQ咨询




