服务器证书过期怎么办?HTTPS证书续期与自动化管理教程

浏览器突然弹出"您的连接不是私密连接",或者 API 调用方反馈请求全部失败,一查发现是 HTTPS 证书过期了——这是运维里最让人尴尬的一类事故:它不是被攻击,也不是业务代码出问题,纯粹是一个可以提前预防的到期事件。更麻烦的是,证书过期往往发生在你最想不到的时候,比如周末、半夜,而这个时间点上你手上未必有签发平台的账号,签发流程也未必立刻走得通。

这篇文章把证书从"快要过期"到"自动续期"的完整链路讲清楚:怎么查看现有证书什么时候到期、怎么用 Certbot 和 acme.sh 续期、怎么用 cron 或 systemd timer 自动化、续期之后怎么让 Nginx 重新加载证书、怎么做到期告警、通配符证书怎么搞、以及"证书链不完整"这类常见坑怎么排查。命令区分 RHEL/CentOS/Rocky 与 Debian/Ubuntu 两套。

一个前提:证书续期的本质是"向 CA 证明你拥有这个域名",主流方式是 HTTP-01(在网站根目录放一个校验文件)和 DNS-01(在 DNS 里加一条校验记录)。本文只讲这两种合规的、面向自己域名的续期方式,不涉及任何绕过校验的做法。

一、为什么证书会过期,以及为什么会"突然"过期

证书是一份由 CA 签发的、带有效期的凭证,主流免费证书(如 Let's Encrypt)有效期通常只有 90 天,商业证书一般是一年。之所以设计成短期,是因为缩短有效期能减少"证书泄露后被长期滥用"的窗口。代价是,你必须持续续期,不能签一次用三年。

"突然过期"通常有三个原因。第一,续期是手动做的,忘了。第二,自动化配了,但没生效——比如 cron 根本没跑起来、续期脚本因为域名验证失败而中断、或者证书续了但 Web 服务没有重新加载。第三,监控缺失,没人知道快到期了。这三者里,第二类最隐蔽:你以为已经自动化了,实际上某一步悄悄失效,直到过期当天才暴露。

所以正确的心态是:证书管理不是"签一次就完事",而是一条需要持续运行和监控的流水线。只要把"查看到期"、"自动续期"、"续期后 reload"、"到期告警"这四步都做扎实,证书过期基本可以永久从你的事故清单里划掉。

二、先看:证书什么时候到期

排查证书问题,第一步永远是确认它现在到底还有效多久。有两个角度:从外部看线上正在用的证书,和从服务器本地看已签发的证书。

从外部看,用 openssl s_client 连接目标域名的 443 端口,读回证书信息。

# 查看远程站点证书的到期时间(-servername 用于 SNI,多域名服务器上很关键)
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -dates -subject -issuer

# 输出示例:
# notBefore=Jul  1 00:00:00 2026 GMT
# notAfter=Sep 29 23:59:59 2026 GMT     <- 这个就是到期时间
# subject=CN = example.com
# issuer=C = US, O = Let's Encrypt, CN = R3

# 只看还剩多少时间到期的更直观写法(用 notAfter 和当前时间对比)
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -enddate

如果服务器用的是 Certbot 管理证书,本地查询更方便,它会把所有证书的到期时间列出来。

# 列出 Certbot 管理的所有证书及其到期时间
sudo certbot certificates

# 输出里每个证书会显示:
# Certificate Name: example.com
#   Domains: example.com www.example.com
#   Expiry Date: 2026-09-29 (VALID: 30 days)
#   Certificate Path: /etc/letsencrypt/live/example.com/fullchain.pem

注意 Certificate Path 里的 fullchain.pem。这是 Certbot 生成的关键文件,它包含"服务器证书 + 中间证书"的完整链,Nginx 配置里应该指向它,而不是只指向 cert.pem(那只含服务器证书,会导致证书链不完整,后面会讲)。

如果用的是 acme.sh,查看方式是:

# acme.sh 列出所有已签发证书
acme.sh --list

也可以用一条通用命令直接检查任意证书文件(PEM 格式)的到期时间,适合手工管理的证书。

# 查看本地证书文件的到期时间
openssl x509 -in /path/to/cert.pem -noout -enddate -subject

三、用 Certbot 续期(Debian/Ubuntu 与 RHEL 系通用)

Certbot 是 Let's Encrypt 官方推荐的客户端,支持自动配置 Nginx/Apache,也支持手工 webroot 模式。它的续期命令很简单,但有几个关键细节必须注意。

# Debian / Ubuntu:安装
sudo apt update
sudo apt install -y certbot python3-certbot-nginx

# RHEL / CentOS / Rocky / AlmaLinux:Certbot 在 EPEL 里
sudo dnf install -y epel-release
sudo dnf install -y certbot python3-certbot-nginx

首次签发(以 Nginx 为例):

# Certbot 会自动校验域名并改写 Nginx 配置
sudo certbot --nginx -d example.com -d www.example.com

# 如果不想让它改配置,只签发证书,用 webroot 模式
sudo certbot certonly --webroot -w /var/www/html -d example.com -d www.example.com

续期有两个命令,区别很重要:

# 1) 试运行:不做任何实际改动,只检查续期会不会成功(强烈建议先用它验证)
sudo certbot renew --dry-run

# 2) 真正执行续期:只对"即将到期"的证书生效(默认剩余 30 天内才续)
sudo certbot renew

certbot renew 的智能之处在于:它会对所有证书逐一检查,只有快到期的才续,没到期的会跳过并打印 "not yet due for renewal"。所以你完全可以放心地把它放进定时任务里,每天跑一次,不需要自己去算哪天该续。

这里有个高频坑:certbot renew 会续证书,但不会自动让 Nginx 加载新证书。Nginx 启动时把证书读进内存,续期只是在磁盘上换了文件,Nginx 不会自动感知。所以必须配置"续期成功后 reload Nginx"。Certbot 支持通过 deploy hook 实现。

# 方式一:在交互式续期时配置钩子(推荐),续期成功自动 reload
sudo certbot renew --deploy-hook "systemctl reload nginx"

# 方式二:把钩子写成脚本,放在约定目录,Certbot 每次续期成功后自动执行
sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh >/dev/null <<'EOF'
#!/bin/bash
systemctl reload nginx
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

用目录方式更省心:以后不管手动续还是定时续,只要续期成功,都会自动执行这个脚本。记得 reload 而不是 restart,前者平滑,不会中断现有连接。

四、用 acme.sh 续期(更轻量,支持更多 DNS 服务商)

acme.sh 是一个纯 Shell 实现的 ACME 客户端,不需要 Python 环境,对 DNS 验证的支持尤其广泛,是做通配符证书的常用选择。

# 安装 acme.sh(会装到 ~/.acme.sh,并注册一个 cron 任务用于自动续期)
curl https://get.acme.sh | sh -s email=you@example.com
# 安装后重新加载 shell,或直接 source 配置
source ~/.acme.sh/acme.sh.env

# 如果要换用 Let's Encrypt 的默认 CA(可选)
acme.sh --set-default-ca --server letsencrypt

签发证书有两种典型方式。HTTP-01 需要 Web 服务能把校验文件serve出去;DNS-01 则通过在 DNS 添加 TXT 记录来验证,特别适合签通配符。

# HTTP-01:webroot 模式,校验文件放在网站根目录
acme.sh --issue -d example.com -d www.example.com -w /var/www/html

# DNS-01:适合通配符证书(*.example.com 只能用 DNS 方式签发)
# 以某 DNS 服务商 API 为例(需先导出对应的 API 密钥环境变量)
export CF_Token="你的DNS服务商API令牌"
acme.sh --issue --dns dns_cf -d example.com -d '*.example.com'

# 签发后,把证书安装到指定路径(不要直接让 Nginx 读 ~/.acme.sh 里的文件)
acme.sh --install-cert -d example.com \
  --key-file       /etc/nginx/ssl/example.com.key \
  --fullchain-file /etc/nginx/ssl/example.com.fullchain.pem \
  --reloadcmd      "systemctl reload nginx"

--install-cert 这一步非常重要。acme.sh 默认把证书存在自己的目录里,而它自己会更新那个目录;你应该用 --install-cert 把证书复制到固定的部署路径,并指定 --reloadcmd,这样既保证 Nginx 读的是稳定路径,又保证续期后自动 reload。acme.sh 安装时会自动注册一个 cron 任务,通常每天检查一次续期,你可以在 crontab 里确认。

# 查看 acme.sh 注册的自动续期任务
crontab -l | grep acme
# 手动强制续期一次(用于验证配置是否正确)
acme.sh --renew -d example.com --force

五、自动化:cron 与 systemd timer

无论用 Certbot 还是 acme.sh,自动化的核心都是"定期运行续期命令"。两种常见方式:cron 和 systemd timer。后者能更好地与系统日志、依赖关系集成,现代系统更推荐。

用 cron 的做法:

# 编辑 root 的 crontab
sudo crontab -e

# 每天凌晨 3 点 17 分尝试续期一次(加随机分钟避免整点拥塞)
# 注意:--quiet 会静默成功输出,便于只关注错误
17 3 * * * /usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"

用 systemd timer 的做法(Certbot 包在多数发行版里已自带一个 timer 和 service,你只需确认它是启用的):

# 查看 Certbot 自带的 timer 是否启用
systemctl list-timers | grep -i certbot
sudo systemctl status certbot.timer
sudo systemctl enable --now certbot.timer

# 查看下次触发时间
systemctl list-timers certbot.timer

如果想自己写一个 timer,也没有问题。

# /etc/systemd/system/cert-renew.service
[Unit]
Description=Renew TLS certificates
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"
# /etc/systemd/system/cert-renew.timer
[Unit]
Description=Daily certificate renewal check

[Timer]
OnCalendar=daily
RandomizedDelaySec=3600
Persistent=true

[Install]
WantedBy=timers.target
# 启用 timer
sudo systemctl daemon-reload
sudo systemctl enable --now cert-renew.timer
# 查看状态与下次触发
systemctl list-timers cert-renew.timer
# 手动触发一次 service 测试
sudo systemctl start cert-renew.service

RandomizedDelaySec=3600 让任务在一天内的触发时间随机化,避免所有机器在同一分钟集中向 CA 发请求(CA 对请求频率有软性限制)。Persistent=true 保证如果机器在计划时间点关机了,开机后会补跑一次,这对不常开的机器很实用。

六、到期告警:最后一道保险

自动化再好,也有失效的可能:验证失败、DNS 记录没同步、reload 命令路径写错。所以还需要一层"到期告警"作为保险。告警的目标很简单:在证书到期前若干天,主动通知你。

做法之一是直接检查线上证书的到期时间,把"剩余天数小于阈值"当成告警条件。可以用脚本配合监控系统,或者简单点,用 cron 每天检查一次并发通知。

# 一个到期检查脚本的思路:读取证书到期时间,比较剩余天数
#!/bin/bash
DOMAIN="example.com"
THRESHOLD=14   # 剩余不足 14 天就告警

END=$(echo | openssl s_client -connect ${DOMAIN}:443 -servername ${DOMAIN} 2>/dev/null \
      | openssl x509 -noout -enddate | cut -d= -f2)
END_TS=$(date -d "$END" +%s)
NOW_TS=$(date +%s)
DAYS=$(( (END_TS - NOW_TS) / 86400 ))

echo "${DOMAIN} 证书剩余 ${DAYS} 天"
if [ "$DAYS" -lt "$THRESHOLD" ]; then
    echo "告警:${DOMAIN} 证书将在 ${DAYS} 天后过期"
    # 在此处接入你的告警方式:邮件、webhook、监控系统接口等
fi

把这段逻辑挂到监控系统(Zabbix、Prometheus 的 blackbox exporter 等)里,是更规范的做法。黑盒拨测类工具可以直接把"证书剩余有效期"作为一个指标来采集,配一条阈值告警即可,不需要自己维护脚本。

风险提示:告警脚本里如果要访问外部接口发通知,注意别把 API 密钥硬编码在 World-readable 的脚本里。脚本文件权限收紧到 700,密钥放环境变量或受保护的配置文件。

七、常见坑:证书链不完整与续期后没 reload

有两个问题非常高频,值得单独说。

第一个是证书链不完整。浏览器和现代客户端通常能自动补全中间证书,所以本地测试一切正常,但在某些移动端、老客户端、或者用 openssl 校验时会报"无法验证证书链"。根因是配置里用了只含服务器证书的 cert.pem,而不是含完整链的 fullchain.pem。

# 错误:只用服务器证书,中间证书缺失
ssl_certificate     /etc/letsencrypt/live/example.com/cert.pem;

# 正确:用 fullchain.pem,它包含服务器证书 + 中间证书
ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

验证证书链是否完整,用一条命令就能查:

# 检查证书链是否完整(输出中应看到 "Verify return code: 0 (ok)")
echo | openssl s_client -connect example.com:443 -servername example.com 2>&1 \
  | grep -i "verify return code"

如果返回的不是 0,就要检查是不是漏了中间证书。另一个验证方法是指定 CA 包做完整校验:

echo | openssl s_client -connect example.com:443 -servername example.com \
  -CAfile /etc/ssl/certs/ca-certificates.crt 2>&1 | grep -i "verify return code"

第二个坑是续期后忘记 reload。现象很有迷惑性:磁盘上的证书文件明明已经更新了,日期也是新的,但浏览器看到的还是旧证书、快过期。原因就是 Web 服务还在用内存里的旧证书。解决方式前面已经讲了,配置 --deploy-hook 或 --reloadcmd,让续期成功后自动 reload。

# 排查:确认磁盘上的证书到期时间与线上实际提供的证书到期时间是否一致
echo "磁盘上的证书:"
sudo openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -enddate
echo "线上提供的证书:"
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -enddate
# 两者不一致,基本就是没 reload

八、通配符证书与多域名

如果你的域名下有很多子域(a.example.com、b.example.com、api.example.com……),为每个都单独签一张证书会很麻烦。这时候用通配符证书 *.example.com 会更省事。

通配符证书只能用 DNS-01 方式签发,因为它需要在 DNS 里添加 TXT 记录来证明你对整个域名的控制权。

# Certbot 签通配符(需要 DNS 插件,以某服务商为例)
sudo certbot certonly --dns-cloudflare \
  --dns-cloudflare-credentials /root/.secrets/cf.ini \
  -d example.com -d '*.example.com'

# acme.sh 签通配符(同样需要 DNS API)
export CF_Token="你的API令牌"
acme.sh --issue --dns dns_cf -d example.com -d '*.example.com'

有两点要注意。第一,*.example.com 只覆盖一级子域,不覆盖 example.com 本身,也不覆盖 a.b.example.com,所以通常要同时签 example.com 和 *.example.com 两个名字。第二,DNS 方式的自动续期依赖 DNS 服务商的 API,一旦 API 令牌过期或权限被改,续期会静默失败——所以 DNS 方式的证书更需要配好到期告警。

多域名证书(SAN 证书)则适合"几个相关域名签在同一张证书里"的场景,比如 example.com 和 example.net 一起签。Certbot 用多个 -d 参数即可。要注意的是,所有域名都得能通过同一套验证方式,否则签发会失败。

常见问题

Q:certbot renew --dry-run 报失败,但平时登录网站又正常,怎么回事?
--dry-run 用测试环境模拟续期,会走一遍域名的验证流程。如果它失败,通常说明续期实际会失败,只是你还没到续期时间所以没暴露。常见原因是验证文件路径不对、80 端口被占用或跳转配置冲突、DNS 未同步。务必让 dry-run 通过,再依赖自动续期。

Q:续期命令要跑多频繁?会不会被 CA 限流?
每天跑一次是安全且常见的。Certbot 只会对剩余期限在阈值内(默认 30 天)的证书续期,不会重复签发;Let's Encrypt 也有每周签发次数限制。加上随机化的触发时间,就不会有问题。

Q:证书续了,但浏览器还是提示过期,怎么办?
先比对磁盘证书和线上提供的证书的到期时间,如果线上还是旧的,就是没 reload。配置 deploy hook 或 reloadcmd 之后重跑一次,确认线上证书刷新。

Q:Nginx 配置里 ssl_certificate 应该指向哪个文件?
指向 fullchain.pem(含完整链),ssl_certificate_key 指向 privkey.pem。不要用 cert.pem,它缺少中间证书,会导致部分客户端校验失败。

Q:服务器在多台机器上,证书怎么统一续期和管理?
可以让一台机器负责签发,续期后把证书分发到其他机器并 reload。分发要保证安全(用密钥、限制权限),并确保分发也纳入了自动化和告警。规模更大时,可以考虑集中式的证书管理方案,但核心思路一致:签发、分发、reload、告警四步都要自动且可观测。

总结

证书过期是可以彻底预防的事故。把"查看到期、自动续期、续期后 reload、到期告警"这四件事做扎实,它就不会再成为你的问题。

查看到期用 openssl s_client 看线上证书,用 certbot certificates 或 acme.sh --list 看本地证书。自动化方面,Certbot 适合大多数场景,certbot renew 会自动跳过未到期的证书;acme.sh 更轻量,DNS 支持更广,适合通配符。

最容易忽略的一步是"续期后 reload 服务"。只更新磁盘文件而不 reload,等于没续。用 Certbot 的 --deploy-hook、目录式钩子,或 acme.sh 的 --reloadcmd 把它自动化。

告警是最后一道保险。无论自动化多完善,都要有一个"证书剩余天数过低"的告警,避免验证失败、reload 失败这类静默问题拖到过期才暴露。

配置层面记住两个高频坑:Nginx 用 fullchain.pem 而不是 cert.pem(避免证书链不完整),以及续期成功一定要 reload。把这两点做对,绝大多数"证书过期"事故都能避免。