Nginx 更新 ACME 证书后随机返回旧证书的排查与修复

ACME 证书续期之后,浏览器访问 HTTPS 时好时坏,一部分请求仍然返回旧证书甚至提示已过期。本文完整记录一次这类故障的排查思路与最终修复方案,核心问题是 systemd 服务配置与编译版 Nginx 不匹配,导致两个 master 进程同时在线。

故障现象

✅ 证书文件已确认更新:
   /usr/local/nginx/conf/cert.pem → notAfter=May 27 2026(新证书)

❌ 但客户端访问时随机返回旧证书:
   • openssl 测试:20 次请求中 ~35% 返回 notAfter=Feb 18 2026(旧证书)
   • 浏览器访问:偶尔提示"您的连接不是私密连接" / "证书已过期"
   • myssl.com 检测:同时显示"已过期"和"可信"两条矛盾记录

同一域名反复请求,证书有效期在两个日期之间随机切换——证书文件确实更新了,但没有完全生效。

定位过程

先带 SNI 直接验证服务器下发的证书:

# 验证服务器返回的证书(带 SNI)
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates

# 输出示例(随机出现):
# notBefore=Nov 20 11:20:38 2025 GMT
# notAfter=Feb 18 11:20:37 2026 GMT    ❌ 旧证书

# 或
# notBefore=Feb 26 09:53:46 2026 GMT
# notAfter=May 27 09:53:45 2026 GMT    ✅ 新证书


# 验证本地证书文件内容
openssl x509 -in /usr/local/nginx/conf/cert.pem -noout -subject -enddate -issuer

# 输出(始终正确):
subject=CN=example.com
notAfter=May 27 09:53:45 2026 GMT    ✅ 新证书
issuer=C=US, O=Let's Encrypt, CN=E7

新旧两个日期随机出现,说明问题出在进程侧而不是证书文件。检查 Nginx 进程树:

# 查看 Nginx 进程树(含启动时间)
ps -eo pid,ppid,cmd,lstart | grep nginx | grep -v grep
# ❌ 旧实例(2025-11-20 启动,持有旧证书内存)
root  2743757  ...  nginx: master process nginx -
├─ 2104488 2743757 nginx: worker process   Sun Nov 30 11:50:56 2025

# ✅ 新实例(2026-02-26 启动,持有新证书内存)  
root  2895550  ...  nginx: master process /usr/local/nginx/sbin/nginx
├─ 2895551 2895550 nginx: worker process   Thu Feb 26 22:34:17 2026

# 查看 443 端口监听情况
ss -tlnp | grep ":443"

# 输出示例:
LISTEN 0 511 0.0.0.0:443 0.0.0.0:* users:(("nginx",pid=2899455,fd=10),("nginx",pid=2899454,fd=10))

新旧两个 master 同时存在且都监听 443,新连接被随机分给两个进程,这就是”随机返回旧证书”的直接原因。

根因分析

查看 systemd 使用的服务单元:

# 查看 systemd 服务配置
cat /usr/lib/systemd/system/nginx.service

[Unit]
Description=nginx - high performance web server
Documentation=http://nginx.org/en/docs/
After=network-online.target remote-fs.target nss-lookup.target
Wants=network-online.target

[Service]
Type=forking
ExecStartPre=/usr/local/nginx/sbin/nginx -t
ExecStart=/usr/local/nginx/sbin/nginx
ExecReload=/usr/local/nginx/sbin/nginx -s reload
ExecStop=/usr/local/nginx/sbin/nginx -s quit
PrivateTmp=true

[Install]
WantedBy=multi-user.target
Type=forking
ExecStartPre=/usr/local/nginx/sbin/nginx -t
ExecStart=/usr/local/nginx/sbin/nginx
ExecReload=/usr/local/nginx/sbin/nginx -s reload
ExecStop=/usr/local/nginx/sbin/nginx -s quit   # ❌ 关键问题
PrivateTmp=true
❌ 直接原因:
   两个 Nginx master 进程同时监听 443 端口 → 请求随机分发 → 随机返回旧/新证书

❌ 深层原因:
   1. systemd 配置缺失 PIDFile → 无法跟踪手动编译版 nginx 进程
   2. systemd 配置路径指向 RPM 版,与实际运行版本不匹配
   3. 手动执行 nginx 二进制 + systemctl 混用 → 产生多实例冲突
   4. Nginx worker 启动时将证书加载到内存 → 文件更新不会自动刷新

✅ 修复核心:
   • 统一通过 systemd 管理 nginx 启停
   • 修正 systemd 配置路径,匹配手动编译版实际路径
   • 添加 PIDFile 确保 systemd 能正

概括来说:单元文件按 RPM 版路径编写,又缺少 PIDFile,systemctl 的 reload 与 start 操作不到编译安装的真实进程,旧 master 一直没被回收。

修复步骤

第一步,彻底停掉新旧两个实例:

① 尝试优雅停止
systemctl stop nginx 2>/dev/null
/usr/local/nginx/sbin/nginx -s quit 2>/dev/null

② 等待 3 秒
sleep 3

③ 强制杀死残留进程
pkill -9 nginx

④ 确认端口已释放
if ss -tlnp | grep -q ":443.*nginx"; then
    echo "警告:443 端口仍有 nginx 进程监听,尝试 fuser 强制释放..."
    fuser -k 443/tcp 2>/dev/null
    sleep 2
fi

⑤ 最终验证
if ss -tlnp | grep -q ":443.*nginx"; then
    echo "失败:仍有进程监听 443 端口,请手动检查"
    ss -tlnp | grep ":443"
    exit 1
else
    echo "成功:443 端口已释放"
fi

第二步,备份原单元文件,写入适配编译版 Nginx 的新配置:

# 备份原配置
cp /usr/lib/systemd/system/nginx.service /usr/lib/systemd/system/nginx.service.bak.$(date +%F)

# 写入新配置(适配手动编译版 nginx)
cat > /usr/lib/systemd/system/nginx.service << 'EOF'
[Unit]
Description=nginx (manual compile) - high performance web server
Documentation=http://nginx.org/en/docs/
After=network-online.target remote-fs.target nss-lookup.target
Wants=network-online.target

[Service]
Type=forking
#关键:PID 文件路径(必须与 nginx.conf 中的 pid 指令一致)
PIDFile=/usr/local/nginx/logs/nginx.pid
# 环境变量:指定配置文件路径
Environment="conffile=/usr/local/nginx/conf/nginx.conf"
# 启动前测试配置,失败则不启动
ExecStartPre=/usr/local/nginx/sbin/nginx -t
# 启动命令:指向手动编译版二进制
ExecStart=/usr/local/nginx/sbin/nginx -c ${conffile}
# 重载配置:使用 $MAINPID + HUP 信号
ExecReload=/bin/kill -s HUP $MAINPID
# 优雅停止:使用 QUIT 信号让 worker 处理完请求再退出
ExecStop=/bin/kill -s QUIT $MAINPID
# 进程退出超时时间(nginx 优雅退出可能需要 10-30 秒)
TimeoutStopSec=30
# 崩溃时自动重启(生产环境强烈建议)
Restart=on-failure
RestartSec=5s
# 资源限制:高并发场景优化
LimitNOFILE=65535
LimitNPROC=65535
# 安全选项
PrivateTmp=true

[Install]
WantedBy=multi-user.target
EOF

第三步,确认 nginx.conf 中 pid 指令的路径与单元文件的 PIDFile 完全一致:

# 检查 nginx.conf 中的 pid 指令
grep "^pid" /usr/local/nginx/conf/nginx.conf

# 应输出:pid /usr/local/nginx/logs/nginx.pid;
# 如果路径不同,请修改 nginx.conf:
#   sed -i 's|^pid .*|pid /usr/local/nginx/logs/nginx.pid;|' /usr/local/nginx/conf/nginx.conf

第四步,重载 systemd 并重新启动,验证 master 进程唯一:

systemctl daemon-reload

systemctl enable nginx
systemctl start nginx

echo "验证服务状态..."
systemctl status nginx --no-pager -l

# 验证进程唯一性
MASTER_COUNT=$(ps aux | grep "nginx: master" | grep -v grep | wc -l)
if [ "$MASTER_COUNT" -eq 1 ]; then
    echo "Nginx master 进程唯一"
else
    echo "发现 $MASTER_COUNT 个 master 进程!"
    ps aux | grep "nginx: master" | grep -v grep
fi

# 验证端口监听
PORT_COUNT=$(ss -tlnp | grep ":443" | grep nginx | wc -l)
if [ "$PORT_COUNT" -eq 1 ]; then
    echo "443 端口监听唯一"
else
    echo "警告:$PORT_COUNT 个进程监听 443 端口!"
    ss -tlnp | grep ":443"
fi

验证与补充排查

连续 20 次请求,验证下发证书是否稳定为新的有效期:

for i in {1..20}; do   result=$(echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -enddate);   echo "$result" | grep -q "May 27" && echo "✅ $i: OK" || echo "❌ $i: $result"; done

必要时把服务器下发的证书与本地证书文件逐项比对:

# 查看服务器返回的证书
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -subject -issuer -dates

# 查看本地证书文件
openssl x509 -in /usr/local/nginx/conf/cert.pem -noout -text | head -20

# 检查证书剩余天数
openssl x509 -in /usr/local/nginx/conf/cert.pem -noout -enddate | cut -d= -f2 | xargs -I{} date -d "{}" +%s | xargs -I{} bash -c 'echo $(( ({} - $(date +%s)) / 86400 )) days left'

端口占用与 SELinux 的排查命令:

# 检查 443 端口被哪些进程占用
ss -tlnp | grep ":443"
lsof -i :443

# 强制释放端口(谨慎使用)
fuser -k 443/tcp

# 检查 SELinux 是否阻止 nginx 读取证书
ls -Z /usr/local/nginx/conf/cert.pem
ausearch -m avc -ts recent | grep nginx

# 临时关闭 SELinux 测试(生产环境慎用)
setenforce 0
# 测试后恢复
setenforce 1

注意事项

  • PIDFile 路径与 nginx.conf 的 pid 指令必须完全一致,否则 systemctl 依旧追踪不到主进程,问题会在下次续签时复现。
  • 手动编译版与 RPM 版共存时,先用 which nginx 确认二者指向同一份二进制,再谈单元文件路径。
  • 用 kill -s HUP 做 Reload 时,只有唯一的 master 收到信号才会平滑更换证书;重载前用 ss 确认 443 端口没有被两个进程监听。
  • 开启 SELinux 的环境若证书读取被拒绝,应通过审计日志定位标签问题,而不是直接关闭 SELinux。
© 版权声明
THE END
喜欢就支持一下吧
点赞14 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容