服务器SSL证书过期怎么办?证书续期、部署与自动更新教程

周一早上,客服反馈说用户打开网站时浏览器弹出"您的连接不是私密连接",页面完全打不开。你用自己的电脑试了一下却是正常的——因为你的浏览器缓存了旧会话;换无痕窗口一试,红色的证书过期警告赫然在目。SSL 证书过期是运维里最典型的"低技术难度、高业务影响"事故:处理起来可能只要五分钟,但发现得晚,损失的就是整段时间的转化率。更麻烦的是,随着行业不断缩短证书最长有效期,人工续期这条路已经走不通了。本文从"怎么快速确认过期时间"讲起,给出 certbot 续期、Web 服务器部署、以及自动更新落地的完整流程。

一、先搞清楚:证书过期为什么会波及全站

证书过期后,浏览器会直接阻断访问而不是给出可跳过的警告(对 HSTS 站点尤其严格)。受影响的不只是浏览器:移动 App 里的 HTTPS 请求会失败、微信内置浏览器会拒绝加载、支付回调和第三方 API 调用会因为证书校验失败而中断、某些爬虫和监控探针也会报错。也就是说,证书过期的影响面通常比"网页打不开"要大得多。

关于有效期,需要建立一个概念:主流免费证书(如 Let's Encrypt)的有效期是 90 天;传统付费 OV/EV 证书目前最长通常不超过 398 天。而 CA/B 浏览器论坛已经通过了继续缩短证书最长有效期的行业决议,计划分阶段降到 200 天、再到 100 天,具体生效时间以官方公告为准。趋势很明确:依赖人工提醒和手工续期,迟早会出事。正确的做法是把续期做成自动化流水线,并配上独立的过期监控。

二、快速确认:证书什么时候过期

2.1 从服务器本地查证书文件

# Let's Encrypt 证书默认存放路径
ls -l /etc/letsencrypt/live/

# 查看某张证书的有效期、颁发者与使用者
openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -dates -subject -issuer

# 只看到期时间
openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem -noout -enddate

# 查看证书覆盖的域名(SAN)
openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -text | grep -A2 'Subject Alternative Name'

# certbot 自带的证书清单(会显示 Expiry Date 与剩余天数)
certbot certificates

openssl x509 -noout -dates 会输出 notBefore 与 notAfter 两行,看 notAfter 即可。注意证书文件通常是一个符号链接链,-in 指向 live/ 下的路径即可,certbot 续期后链接会自动指向新版本。

2.2 从外部验证真实生效的证书

本地文件对了不代表线上生效了——可能 Nginx 没 reload,也可能前面还有一层 CDN 或负载均衡在用另一张证书。因此必须从外部再验一次。

# 通过 TLS 握手取回线上实际下发的证书并查看有效期
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates -issuer

# 用 curl 查看握手详情中的证书信息
curl -vI https://example.com/ 2>&1 | grep -iE 'expire|subject|issuer|start date'

# curl 也可以直接输出证书到期时间
curl -sS -o /dev/null -w '%{http_code}\n' https://example.com/

2.3 三种检查方式的差异

检查方式检查对象优点局限
openssl x509 读本地文件磁盘上的证书文件最快,无需网络,可脚本批量不能反映服务是否加载了新证书
openssl s_client 远程握手线上实际下发的证书反映真实情况,可测任意域名 IP需指定 -servername,否则 SNI 场景取到的可能不是目标证书
certbot certificatescertbot 管理的证书清单直接给出剩余天数与续期配置只覆盖 certbot 签发的证书,手动申请的看不到
外部监控 / 拨测用户视角的可用性能在过期前主动告警需要额外部署监控资源

三、续期:certbot 标准流程

certbot 是目前最主流的免费证书客户端,支持自动完成域名验证(HTTP-01 或 DNS-01)、签发与续期。下面按场景给出命令。

3.1 首次申请

# 方式一:webroot 插件(推荐,不中断现有 Web 服务)
certbot certonly --webroot -w /var/www/html -d example.com -d www.example.com

# 方式二:nginx 插件,自动修改 Nginx 配置(适合全新站点)
certbot --nginx -d example.com -d www.example.com

# 方式三:standalone 插件,certbot 自己临时监听 80 端口(需先停掉占用 80 的服务)
systemctl stop nginx
certbot certonly --standalone -d example.com
systemctl start nginx

# 方式四:泛域名证书必须用 DNS-01 验证,需手动添加 TXT 记录
certbot certonly --manual --preferred-challenges dns -d '*.example.com' -d example.com

选哪种取决于你的场景:已有 Web 服务且能写网站根目录,用 --webroot;80 端口可以临时让出,用 --standalone;需要泛域名(通配符)证书,只能用 DNS-01,且建议配合支持 API 的 DNS 插件实现自动化,否则每次续期都要手工改解析,等于没有自动化。

3.2 续期操作

# 演练续期(不真正签发,用于验证配置是否正确)——这是最重要的一条命令
certbot renew --dry-run

# 正式续期(certbot 只会对剩余有效期不足 30 天的证书执行续期,未到期的会跳过)
certbot renew

# 强制续期某一张证书(忽略 30 天判断)
certbot renew --cert-name example.com --force-renewal

# 续期后自动重载 Web 服务
certbot renew --deploy-hook "systemctl reload nginx"

# 查看续期配置(含验证方式与 hook)
cat /etc/letsencrypt/renewal/example.com.conf

务必养成先跑 certbot renew --dry-run 的习惯。它会完整走一遍验证流程但不真正签发证书,能提前暴露"80 端口被挡""webroot 路径改了""DNS API 密钥失效"这类问题,而不是等到真正续期那天才发现失败。建议每个月至少演练一次。

四、部署到 Web 服务器

4.1 Nginx

server {
    listen 443 ssl;
    http2 on;
    server_name example.com www.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;
    ssl_session_tickets off;

    root /var/www/html;
    index index.html;
}

# HTTP 全量跳转 HTTPS
server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}
# 校验配置并平滑加载(不要 restart,避免连接瞬断)
nginx -t
systemctl reload nginx

# 确认新证书已生效
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates -issuer

注意两个细节:ssl_certificate 要用 fullchain.pem 而不是 cert.pem,前者包含完整证书链,缺少中间证书会导致部分客户端(尤其是 Java 和某些移动 App)校验失败;私钥文件 privkey.pem 权限必须是 600 且属主为 root,否则 Nginx 启动时会告警甚至拒绝加载。

4.2 Apache


    ServerName example.com
    SSLEngine on
    SSLCertificateFile      /etc/letsencrypt/live/example.com/fullchain.pem
    SSLCertificateKeyFile   /etc/letsencrypt/live/example.com/privkey.pem
    SSLProtocol             all -SSLv3 -TLSv1 -TLSv1.1
# 校验并加载
apachectl configtest
systemctl reload apache2        # Debian/Ubuntu
systemctl reload httpd          # RHEL/CentOS/Rocky

五、自动更新:让续期这件事消失

5.1 优先使用系统自带的定时器

通过官方包安装的 certbot 通常会自带一个 systemd timer 或 cron 任务,每天随机时间执行两次(避免所有人同时请求造成 CA 侧拥塞)。先检查它是否存在并处于启用状态:

# 查看 certbot 定时器状态
systemctl status certbot.timer
systemctl list-timers | grep -i certbot

# 若未启用,手动开启
systemctl enable --now certbot.timer

# 查看最近一次执行日志
journalctl -u certbot.service -n 50

5.2 手写 cron 作为兜底

如果系统没有自带定时器,或者你需要在续期后执行额外的分发动作,可以自己写一条 crontab。建议一天执行两次,并在续期成功后触发 reload:

# 每天 03:17 与 15:17 各执行一次,续期成功后重载 Nginx
17 3,15 * * * /usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"

# 若需要在多台机器间同步证书,可在 hook 里加分发动作
17 3,15 * * * /usr/bin/certbot renew --quiet --deploy-hook "/usr/local/bin/push-cert.sh"

5.3 deploy-hook 的正确用法

--deploy-hook 只在证书真正被续期时执行,而 --post-hook 只要 certbot 尝试续期就会执行。两者混淆会导致每天无谓地 reload 服务。如果只有一个 hook 需要配置,推荐用 --deploy-hook;涉及 Web 服务器重启(standalone 模式)时才考虑 --pre-hook 与 --post-hook 配合使用。

六、常见故障速查

现象 / 报错常见原因处理方式
--dry-run 报 "Unauthorized" 或 404webroot 路径与 Nginx 配置不一致,或 80 端口被防火墙/CDN 拦截核对 -w 参数与实际根目录;确认 /.well-known/acme-challenge/ 可被外部访问
Challenge failed for domain(DNS-01)TXT 记录未生效或 API 密钥失效用 dig -t txt _acme-challenge.example.com +short 验证记录是否已生效
证书已续期但浏览器仍报过期Nginx/Apache 未 reload,进程仍持有旧证书执行 systemctl reload nginx,再用 s_client 复验
部分客户端报证书链不完整使用了 cert.pem 而非 fullchain.pem把 ssl_certificate 改为 fullchain.pem 后 reload
Nginx 告警私钥权限过宽privkey.pem 权限不是 600chmod 600 privkey.pem 并确保属主为 root
续期太频繁被限流触发 CA 的签发频率限制(如同一注册域名每周证书数量上限、重复证书每周上限)去掉 --force-renewal,改用默认 30 天判断逻辑;具体限额以 CA 官方文档为准
CDN 上配置的证书与源站不一致只续了源站,忘了更新 CDN 侧证书把 CDN 证书也纳入清单,或使用 CDN 提供的托管证书自动续期

七、落地步骤清单

  1. 盘点全量证书:用 certbot certificates 与 openssl x509 -noout -dates 列出所有证书及到期时间,形成清单,别忘了 CDN、负载均衡、对象存储自定义域名上配置的证书。
  2. 确认验证方式可用:跑一次 certbot renew --dry-run,确保 HTTP-01 或 DNS-01 验证链路通畅。
  3. 配置自动续期:启用 certbot.timer 或写入 crontab,并挂上 --deploy-hook 让续期后自动 reload。
  4. 规范部署配置:Nginx 使用 fullchain.pem,限制 TLS 1.2 及以上,私钥权限 600。
  5. 建立独立过期监控:不能只依赖续期脚本成功。建议加一个每天检查 notAfter 并在剩余天数低于 21 天时告警的任务,形成双保险。
  6. 演练一次完整流程:在测试环境用 --force-renewal 走一遍从续期到 reload 到外部验证的全过程,确认链路无死角。
  7. 文档化与交接:把证书清单、续期方式、负责人写进运维文档,避免人员变动导致无人跟进。

小结

SSL 证书过期这件事,技术难度很低,考验的是流程是否闭环。核心动作是四个:看得见(用 openssl x509 -noout -dates 和 certbot certificates 掌握所有证书的到期时间)、续得了(certbot renew --dry-run 先演练、certbot renew 再执行)、装得对(Nginx 用 fullchain.pem、TLS 1.2 起、reload 而非 restart)、自动跑(certbot.timer 或 crontab 配合 deploy-hook)。在这四个动作之外,再加一条独立的过期监控作为兜底,就能把这类事故彻底消灭。

避坑建议

  • 不要只续源站忘记 CDN:这是最常见的遗漏,用户看到的是 CDN 上的证书,源站续了也没用。
  • 不要滥用 --force-renewal:每次都强制续期会快速触发 CA 的签发频率限制,正常情况让 certbot 自己判断(默认剩余不足 30 天才续)。
  • 不要用 cert.pem 代替 fullchain.pem:缺少中间证书会让部分客户端校验失败,而且这种失败是局部的、很难复现,排查成本极高。
  • 不要只依赖续期脚本的成功返回:脚本成功但 reload 失败、或者被前面的负载均衡挡住,都会导致线上仍是旧证书,必须有外部视角的独立校验。
  • 不要在证书过期当天才想起续:把告警阈值设在剩余 21 天甚至更早,给自己留出处理 DNS API 失效、插件升级等意外情况的时间。
  • 不要忽略私钥与配置文件备份:整盘迁移或重建时,重新签发比迁移证书更省事,但如果有固定 pinned 证书的业务,备份就必不可少。