SSL证书自动续期后Nginx未生效怎么办?香港服务器多域名HTTPS排查方法
浏览器仍显示旧的证书到期时间,但证书客户端日志已经提示“续期成功”,通常不代表续期失败。更常见的情况是:新证书已经写入磁盘,Nginx 进程仍在使用内存中的旧证书;或者 Nginx 配置引用了另一份旧文件,多域名请求又被其他 server 配置接管。
排查时不要一开始就重启服务或重复申请证书。建议按照“外部实际证书 → 本地证书文件 → Nginx 生效配置 → reload 日志 → 多域名 SNI 匹配 → 自动续期钩子”的顺序处理。这个顺序从低风险检查开始,可以较快区分是证书没有更新、Nginx 没有重新加载,还是访问请求根本没有到达预期的香港服务器。

先确认故障属于哪一层
HTTPS 证书问题至少涉及三层:
- ACME 客户端是否成功签发并保存新证书。
- Nginx 是否读取了正确路径,并在续期后重新加载。
- 客户端访问的域名是否通过 SNI 命中了正确的
server配置。
可以先用下面的现象表缩小范围。
| 现象 | 优先怀疑位置 | 判断依据 |
|---|---|---|
| 本地证书日期已更新,公网仍显示旧证书 | Nginx 未 reload、访问了其他 IP 或 IPv6 配置 | 磁盘文件和外部握手结果不一致 |
| 所有域名都显示旧证书 | reload 钩子失败、Nginx 没有重新加载 | 多个站点同时未更新 |
| 只有一个域名显示旧证书 | 该域名的证书路径、server_name 或 SNI 匹配异常 | 其他域名可以正常显示新证书 |
nginx -t 失败 | 配置语法、证书路径或权限问题 | 失败时不应执行 reload |
| 证书日期正确,但浏览器提示链不完整 | 使用了叶子证书而不是 fullchain.pem | 证书本身新旧不是主要问题 |
| 指纹与本地新证书不同,且 DNS 指向多个地址 | 当前请求没有到达正在续期的服务器 | 需要分别核对 A、AAAA 和其他入口 |
Nginx 的优雅 reload 通常不会主动中断已有连接。新建立的 TLS 连接会读取新证书,已经建立的连接仍可能沿用原有会话,这是正常现象。因此,验证时要创建新的 HTTPS 连接,不能只观察已经打开的浏览器页面。
准备检查环境和配置备份
以下命令以使用 systemd 管理 Nginx 的 Linux 服务器为例,常见于 Debian、Ubuntu 等发行版。实际操作前先确认命令路径和服务名称,不要直接假设系统一定使用默认目录。
cat /etc/os-release
nginx -v
command -v nginx
systemctl status nginx --no-pager
如果 Nginx 是自定义编译的,可以查看配置文件路径:
nginx -V 2>&1 | grep -- '--conf-path'
在修改 Nginx 配置前,建议只备份配置目录,不要把私钥复制到公共目录或通过工单、聊天工具传递。
sudo cp -a /etc/nginx "/etc/nginx.backup-$(date +%F-%H%M%S)"
sudo nginx -T > "/tmp/nginx-before-$(date +%F-%H%M%S).conf" 2>"/tmp/nginx-before-error.log"
备份的作用是便于恢复被修改的配置。恢复时不要直接覆盖整个 /etc/nginx,因为备份之后可能又产生了其他正常变更。应先定位具体异常文件,恢复单个文件,然后执行 nginx -t,确认通过后再 reload。
同时确认 443 端口由哪个进程监听:
sudo ss -lntp | grep ':443'
如果没有输出,说明当前服务器并没有监听标准 HTTPS 端口;如果显示的不是预期 Nginx 进程,需要先确认 HTTPS 是否由其他服务处理。此时继续修改 Nginx 证书文件不会改变公网结果。
第一步:直接查看公网实际返回的证书
不要先相信浏览器页面上的证书信息,先用 OpenSSL 建立一次新的 TLS 连接。-servername 参数必须保留,它用于发送 SNI,模拟浏览器访问具体域名的行为。

echo | openssl s_client \
-connect example.com:443 \
-servername example.com \
2>/dev/null | openssl x509 -noout \
-subject -issuer -dates -serial -fingerprint -sha256
将 example.com 替换为实际域名。重点观察:
notBefore和notAfter:判断公网返回证书的签发及到期时间。subject或 SAN 中的域名:判断证书是否覆盖当前域名。serial和 SHA-256 指纹:用于和本地证书进行精确比较。issuer:辅助判断返回的是哪一条证书链。
多域名环境不要只测主域名。可以逐个测试每个域名:
for domain in example.com www.example.com api.example.com; do
echo "===== $domain ====="
echo | openssl s_client \
-connect "${domain}:443" \
-servername "$domain" \
2>/dev/null | openssl x509 -noout \
-subject -issuer -dates -serial -fingerprint -sha256
done
如果不同域名返回不同证书,这是正常的前提之一:它们可能使用不同证书或不同 server 配置。但如果其中一个域名返回了完全不相关的证书,通常是 server_name 没匹配、默认虚拟主机被选中,或配置中存在重复的域名声明。
绕过 DNS 检查指定服务器
如果域名存在多个 A 记录、AAAA 记录,或者前面还有其他 HTTPS 入口,可以使用 curl --resolve 强制把域名解析到指定 IP。这样仍然保留正确的 Host 和 SNI,但不会使用系统 DNS 解析结果。
curl -vkI \
--resolve example.com:443:203.0.113.10 \
https://example.com/
这里的 203.0.113.10 仅为示例地址,应替换为香港服务器的实际公网 IPv4。-k 只用于排查阶段让 curl 即使遇到证书错误也继续显示握手过程,不表示生产环境可以忽略证书校验。
如果指定服务器后能看到新证书,而直接访问域名仍是旧证书,问题就不在 Nginx 证书文件本身,应继续核对:
dig +short A example.com
dig +short AAAA example.com
A 记录和 AAAA 记录可能指向不同入口。若用户通过 IPv6 访问,实际请求可能没有到达刚刚检查的 IPv4 服务器,也可能命中了另一份 IPv6 listen 配置。
第二步:确认续期写入的是哪一份证书
以 Certbot 为例,先查看它管理的证书名称、域名和到期时间:
sudo certbot certificates
常见的 Certbot 目录结构如下:
/etc/letsencrypt/live/example.com/fullchain.pem
/etc/letsencrypt/live/example.com/privkey.pem
live 目录中的文件通常是指向 archive 目录的符号链接。查看实际指向:
sudo ls -l /etc/letsencrypt/live/example.com/
sudo readlink -f /etc/letsencrypt/live/example.com/fullchain.pem
sudo readlink -f /etc/letsencrypt/live/example.com/privkey.pem
查看本地证书内容:
sudo openssl x509 \
-in /etc/letsencrypt/live/example.com/fullchain.pem \
-noout -subject -issuer -dates -serial -fingerprint -sha256
将这次输出的指纹与前面公网握手得到的指纹比较:
- 本地和公网指纹相同:证书文件已经被 Nginx 或前置入口使用,问题可能已经解决,继续检查浏览器访问路径或其他域名。
- 本地指纹较新,公网仍旧:优先检查 Nginx reload、DNS、IPv4/IPv6 和 SNI。
- 本地指纹仍旧:续期客户端没有真正更新当前证书,或者你查看的是错误的证书目录。
- 本地日期已更新,但域名不匹配:续期对象可能是另一张证书,需核对
certbot certificates中的域名列表。
检查 Nginx 是否引用了旧副本
很多“续期成功但 Nginx 不更新”的根因,是 Nginx 配置没有引用 /etc/letsencrypt/live/,而是引用了手工复制的文件,例如:
/etc/nginx/ssl/example.com/cert.pem
/etc/nginx/ssl/example.com/privkey.pem
搜索所有证书配置:
sudo grep -RInE 'ssl_certificate(_key)?' \
/etc/nginx /etc/letsencrypt/renewal 2>/dev/null
如果 Nginx 使用的是 /etc/nginx/ssl/ 下的固定副本,那么 Certbot 更新 live 目录并不会自动更新这份副本。可以选择以下一种方式修复:
- 让 Nginx 直接引用 Certbot 的稳定路径。
- 保留独立的 Nginx 证书目录,但在续期成功后使用
--deploy-hook或 renewal hook 将新文件安装到该目录,再执行 Nginx reload。
不建议每次续期后手工复制证书。手工复制容易漏掉某个域名,也可能只复制了证书而没有同步私钥或证书链。
检查证书和私钥是否匹配
如果修改过证书路径,或者出现 Nginx reload 报错,可以比较证书公钥和私钥公钥的摘要。这个操作不会输出私钥内容:
CERT=/etc/letsencrypt/live/example.com/fullchain.pem
KEY=/etc/letsencrypt/live/example.com/privkey.pem
sudo openssl x509 -in "$CERT" -pubkey -noout \
| openssl pkey -pubin -outform pem \
| sha256sum
sudo openssl pkey -in "$KEY" -pubout \
| sha256sum
两行摘要应当一致。如果不一致,说明证书和私钥不是同一对,Nginx 无法使用这组文件。若私钥加密,命令可能要求输入口令;不要把口令写进脚本、命令历史或自动续期配置。
第三步:以 Nginx 实际加载配置为准
只看某个站点文件不够。Nginx 可能通过 include 引入了其他文件,也可能在 conf.d、sites-enabled 中存在同域名的重复配置。使用 nginx -T 查看 Nginx 当前会读取的完整配置:
sudo nginx -T > /tmp/nginx-full.conf 2>/tmp/nginx-full-error.log
然后搜索域名、监听端口和证书路径:
grep -nE 'listen .*443|server_name|ssl_certificate(_key)?' \
/tmp/nginx-full.conf
也可以直接搜索配置目录:
sudo grep -RInE \
'server_name|listen .*443|ssl_certificate(_key)?' \
/etc/nginx/sites-enabled /etc/nginx/conf.d 2>/dev/null
重点确认以下几项:
- 目标域名只出现在预期的 HTTPS
server块中。 ssl_certificate使用的是新证书所在路径。ssl_certificate_key与证书属于同一域名和同一证书版本。- 多域名的
server_name没有拼写错误、漏写或多余通配符。 - IPv4 和 IPv6 的
listen 443配置没有指向另一份站点配置。 - 站点文件确实被
include,而不是只存在于未启用目录中。
一个使用 Certbot 路径的示例配置如下:
server {
listen 443 ssl;
listen [::]:443 ssl;
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;
location / {
proxy_pass http://127.0.0.1:8080;
}
}
fullchain.pem 包含站点证书和中间证书,通常比只配置 cert.pem 更适合直接提供给客户端。若使用独立证书目录,也应确保该目录中的证书文件包含完整链,并且路径在续期后仍然保持稳定。
多域名和 SNI 的典型冲突
HTTPS 在同一个 IP 和 443 端口上承载多个域名时,Nginx 主要依靠 SNI 选择证书。以下情况容易造成“只有一个域名不生效”:

server_name中漏掉了访问域名。- 同一个域名出现在多个
server块中。 - 某个
server块被设置为default_server,但目标域名没有匹配到其他配置。 - 访问的是
www.example.com,配置只写了example.com。 - IPv6 入口使用了不同的 Nginx 配置。
- 测试命令没有加
-servername,导致测试结果与浏览器行为不同。
如果每个域名使用独立证书,可以采用多个精确匹配的 server 块:
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
}
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:9000;
}
}
如果多个域名由同一张包含这些域名的 SAN 证书覆盖,也可以放在同一个 server_name 中。但不能因为两个域名都使用 HTTPS,就把域名 A 的证书随意配置到域名 B 的独立站点上。
nginx -t 能检查语法和文件可读性,但不能证明公网请求一定会命中正确的 server。因此,配置检查通过后,还必须使用带 -servername 的 OpenSSL 命令分别验证每个域名。
第四步:测试并执行安全 reload
修改配置或路径后,先执行测试:
sudo nginx -t
成功时通常会看到类似以下结果:
syntax is ok
test is successful
如果失败,常见含义如下:
| 错误类型 | 可能原因 | 处理方向 |
|---|---|---|
cannot load certificate | 路径错误、文件不存在或权限不足 | 检查 readlink -f、文件权限和目录权限 |
PEM_read_bio_X509 | 证书文件内容损坏或格式不对 | 查看证书头部,确认是 PEM 文件 |
key values mismatch | 证书和私钥不匹配 | 重新绑定同一张证书和私钥 |
duplicate listen options | 多个配置对同一监听端口声明冲突 | 检查重复的 listen 443 参数 |
duplicate server name | 同一监听地址上存在重复域名 | 合并或修正重复的 server_name |
permission denied | Nginx worker 无法读取证书或私钥 | 检查目录遍历权限和文件权限 |
只有测试成功后,才执行 reload:
sudo nginx -t && sudo systemctl reload nginx
reload 后确认服务仍处于运行状态:
sudo systemctl is-active nginx
sudo systemctl status nginx --no-pager
查看 reload 是否被服务管理器接受:
sudo journalctl -u nginx --since "10 minutes ago" --no-pager
如果 nginx -t 失败,不要直接使用 systemctl restart nginx 强行重启。重启可能扩大配置错误的影响范围,甚至导致原本仍可用的 HTTPS 服务无法恢复。应先恢复出错的那一个配置文件,重新执行 nginx -t,通过后再 reload。
第五步:检查自动续期后的 reload 钩子
证书自动续期和 Nginx 自动加载是两个独立动作。ACME 客户端完成证书更新后,如果没有执行 reload,Nginx 进程不会主动重新读取证书文件。
Certbot 使用 renewal hook
可以建立一个只负责检查配置并执行优雅 reload 的脚本:
#!/bin/sh
set -eu
/usr/sbin/nginx -t
/bin/systemctl reload nginx
/usr/bin/logger -t certbot-nginx-reload "nginx reloaded after certificate renewal"
将脚本保存为:
/etc/letsencrypt/renewal-hooks/deploy/20-nginx-reload
然后设置为仅允许管理员执行:
sudo chmod 750 /etc/letsencrypt/renewal-hooks/deploy/20-nginx-reload
sudo chown root:root /etc/letsencrypt/renewal-hooks/deploy/20-nginx-reload
Certbot 的 deploy hook 只应在证书实际成功续期后触发,避免定时检查每次都 reload Nginx。确认目录中存在该脚本:
sudo ls -l /etc/letsencrypt/renewal-hooks/deploy/
查看 Certbot 的定时任务和执行记录:
systemctl list-timers --all | grep -i certbot
sudo journalctl -u certbot --since "7 days ago" --no-pager
不同发行版可能使用 certbot.service、certbot.timer 或其他任务名称。如果没有对应 systemd 单元,还要检查实际运行用户的 cron 配置:
sudo crontab -l
测试时可以先单独确认 reload 脚本是否正常:
sudo /etc/letsencrypt/renewal-hooks/deploy/20-nginx-reload
这不会申请新证书,只会执行配置检查和 reload。若脚本失败,先修复脚本中的 Nginx 路径、权限或服务名称,再进行续期测试。
Certbot 的续期模拟命令如下:
sudo certbot renew --dry-run
该命令通常使用测试环境验证续期流程,不应将测试结果等同于生产证书已经更新。重点观察挑战验证、证书保存和 hook 执行日志。不要为了“立即更新”频繁重复申请生产证书,以免触发证书颁发机构的频率限制。
使用 acme.sh 时保持路径一致
如果服务器使用 acme.sh,不要同时让 Certbot 和 acme.sh 管理同一个域名的同一套证书。acme.sh 的工作目录和运行用户可能不同,先查看当前管理列表:
~/.acme.sh/acme.sh --list
建议将证书安装到 Nginx 使用的稳定路径,并设置 reload 命令。例如:
~/.acme.sh/acme.sh --install-cert -d example.com \
--key-file /etc/nginx/ssl/example.com/privkey.pem \
--fullchain-file /etc/nginx/ssl/example.com/fullchain.pem \
--reloadcmd "systemctl reload nginx"
执行前确认目标目录已经创建,并根据实际运行用户设置适当的目录权限。此命令会更新目标文件,影响的是指定域名的证书路径,不应把路径替换成生产环境中其他站点的目录。
安装后检查 Nginx 配置是否确实引用了这两个目标文件:
sudo grep -RInE 'ssl_certificate(_key)?' /etc/nginx
如果 acme.sh 更新了自己的证书目录,但 Nginx 引用的是 /etc/nginx/ssl/,而没有执行 --install-cert,就会出现“续期成功、Nginx仍然显示旧证书”的现象。
根据结果选择修复分支
完成外部证书、本地文件和 Nginx 配置检查后,可以按下面的结果处理:
本地新、公网旧,且所有域名都旧
优先执行:
sudo nginx -t
sudo systemctl reload nginx
sudo journalctl -u nginx --since "15 minutes ago" --no-pager
如果 reload 成功但公网仍旧,继续检查:
dig +short A example.com
dig +short AAAA example.com
sudo ss -lntp | grep ':443'
然后分别使用 IPv4 地址和 IPv6 入口测试。此时重点不是重复申请证书,而是确认访问路径是否仍然指向这台香港服务器,以及 443 端口是否由预期的 Nginx 进程处理。
本地仍旧,Certbot 或 acme.sh 日志显示没有更新
先查看证书客户端实际管理的域名和路径:
sudo certbot certificates
sudo ls -l /etc/letsencrypt/live/
如果是 acme.sh,则使用实际签发用户执行:
~/.acme.sh/acme.sh --list
此类问题可能与挑战验证、任务未到续期时间、权限、磁盘空间或续期配置中的域名不一致有关。先修复续期任务,不要直接修改 archive 或手动删除历史证书目录。Certbot 的 live、archive 和 renewal 目录存在关联关系,手工删除可能让后续续期任务无法运行。
只有一个域名旧,其他域名新
重点检查这个域名的:
server_name是否准确。ssl_certificate是否引用了自己的证书路径。ssl_certificate_key是否匹配。- 是否存在另一个相同
server_name的配置。 - 是否通过 IPv6 命中了另一份配置。
- 测试时是否正确使用了
-servername。
可以针对单个域名执行:
echo | openssl s_client \
-connect api.example.com:443 \
-servername api.example.com \
2>/dev/null | openssl x509 -noout \
-subject -dates -fingerprint -sha256
如果这里返回的证书与 nginx -T 中该域名配置的证书不一致,说明当前实际加载的配置和你正在编辑的文件不是同一份。
日期正确,但客户端报告证书链错误
检查 Nginx 是否配置了完整链:
sudo openssl x509 \
-in /path/to/configured/fullchain.pem \
-noout -subject -issuer
同时查看配置是否误用了:
ssl_certificate /path/to/cert.pem;
如果客户端需要完整证书链,通常应改为:
ssl_certificate /path/to/fullchain.pem;
私钥仍使用对应的 privkey.pem。修改后必须重新执行 nginx -t 和 reload。
修复后的完整验证
不要只看 systemctl reload nginx 返回成功。建议按以下顺序进行复测。
1. 确认实际加载的路径
sudo nginx -T 2>/dev/null \
| grep -nE 'server_name|ssl_certificate(_key)?|listen .*443'
确认每个域名都关联到预期证书和私钥。
2. 比较本地证书指纹
sudo openssl x509 \
-in /etc/letsencrypt/live/example.com/fullchain.pem \
-noout -dates -serial -fingerprint -sha256
如果使用的是独立安装目录,将路径替换为 Nginx 配置中的实际路径。
3. 建立新的公网 TLS 连接
echo | openssl s_client \
-connect example.com:443 \
-servername example.com \
2>/dev/null | openssl x509 -noout \
-subject -issuer -dates -serial -fingerprint -sha256
公网指纹应与 Nginx 实际引用的本地证书一致。多个域名要分别验证,不能用主域名的结果代替其他域名。
4. 复查 HTTPS 请求
curl -I https://example.com/
curl -I https://www.example.com/
curl -I https://api.example.com/
如果业务接口不支持 HEAD,可以改用:
curl -vk https://api.example.com/ -o /dev/null
这里主要观察 TLS 握手和证书信息,不要求接口一定返回特定 HTTP 状态码。
5. 观察后续一次自动续期
确认自动续期任务运行时具备以下闭环:
- ACME 客户端成功更新证书。
- 新证书写入 Nginx 配置引用的路径。
- deploy hook 被调用。
nginx -t成功。systemctl reload nginx成功。- 新连接获取到更新后的证书。
可以通过日志确认 hook 是否运行:
sudo journalctl -t certbot-nginx-reload --since "30 days ago" --no-pager
如果没有日志,说明 hook 没有被执行,或日志命令路径与系统实际路径不一致。此时应检查 hook 文件的执行权限、Certbot 运行用户和定时任务来源。
证书自动续期后的 Nginx 未生效,最常见的修复并不是重新签发证书,而是让“证书存储路径、Nginx实际加载路径、reload钩子、域名SNI匹配”四者保持一致。对于香港服务器上的多域名 HTTPS 部署,先用带 -servername 的外部握手确认实际返回结果,再用 nginx -T 找到真正生效的配置,最后以 nginx -t && systemctl reload nginx 完成低风险切换,通常比直接重启或反复申请证书更容易定位问题。