在生产环境中,更新 TLS 证书是一个常见但敏感的操作。如果直接重启 Nginx 服务,会导致连接中断,影响用户体验。本文介绍如何在 Nginx 中实现 TLS 证书的热更新,在不中断服务的情况下完成证书更新。

什么是证书热更新?

证书热更新(Hot Reload)是指在不停止服务的情况下,重新加载新的证书文件。Nginx 支持通过发送 reload 信号来实现配置和证书的平滑重新加载。

与完全重启(nginx -s stop 然后 nginx)相比,reload 的优势在于:

  • 不会中断正在处理的请求
  • 平滑过渡,新连接使用新证书,旧连接继续使用旧证书
  • 服务可用性不受影响

Nginx reload 机制

Nginx 支持以下几种信号:

  • nginx -s reload - 平滑重新加载配置
  • kill -HUP <master_pid> - 发送 HUP 信号,作用与 reload 相同
  • kill -USR1 <master_pid> - 重新打开日志文件
  • kill -USR2 <master_pid> - 优雅升级(用于升级 Nginx 二进制文件)

当 Nginx 收到 reload 信号时:

  1. 主进程(Master Process)解析新的配置文件
  2. 如果配置合法,主进程启动新的工作进程(Worker Process)
  3. 旧的工作进程优雅退出(处理完现有连接后关闭)
  4. 新的工作进程使用新的证书

证书热更新操作步骤

1. 准备新的证书文件

首先,将新的证书和私钥文件复制到指定目录:

1
2
3
4
5
6
7
# 复制新的证书文件
sudo cp server_new.crt /etc/nginx/ssl/server.crt
sudo cp server_new.key /etc/nginx/ssl/server.key

# 确保文件权限正确
sudo chmod 600 /etc/nginx/ssl/server.key
sudo chmod 644 /etc/nginx/ssl/server.crt

2. 验证证书文件

在重新加载之前,验证证书和私钥是否匹配:

1
2
3
4
5
# 提取证书公钥
openssl x509 -in /etc/nginx/ssl/server.crt -noout -modulus | openssl md5

# 提取私钥公钥
openssl rsa -in /etc/nginx/ssl/server.key -noout -modulus | openssl md5

如果两者的 MD5 值一致,说明证书和私钥匹配。

同时检查证书有效期:

1
2
# 检查证书有效期
openssl x509 -in /etc/nginx/ssl/server.crt -noout -dates

3. 测试 Nginx 配置

在重新加载之前,务必测试配置文件是否正确:

1
2
3
4
5
6
# 测试 Nginx 配置语法
sudo nginx -t

# 如果配置测试通过,应该看到类似输出:
# nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
# nginx: configuration file /etc/nginx/nginx.conf test is successful

4. 触发配置 reload

配置测试通过后,触发 reload:

1
2
3
4
5
6
7
8
# 方法一:使用 nginx -s reload
sudo nginx -s reload

# 方法二:直接发送 HUP 信号
sudo kill -HUP $(cat /var/run/nginx.pid)

# 方法三:使用 systemctl(适用于 systemd)
sudo systemctl reload nginx

5. 验证新证书已生效

reload 完成后,验证新证书是否已经生效:

1
2
3
4
5
# 使用 openssl s_client 查看当前使用的证书
echo | openssl s_client -servername example.com -connect localhost:443 2>/dev/null | openssl x509 -noout -subject -dates

# 或者使用 curl 访问并查看证书信息
curl -v https://localhost/ 2>&1 | grep -E "SSL certificate|subject|expires"

配置示例

以下是一个完整的 Nginx HTTPS 配置示例,支持证书热更新:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
server {
    listen 443 ssl http2;
    server_name example.com;

    # 证书配置
    ssl_certificate /etc/nginx/ssl/server.crt;
    ssl_certificate_key /etc/nginx/ssl/server.key;

    # 完整的 TLS 配置
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;

    # OCSP Stapling
    ssl_stapling on;
    ssl_stapling_verify on;

    # 会话缓存
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;

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

无损 reload 的最佳实践

1. 使用符号链接

为了简化证书更新流程,可以使用符号链接:

1
2
3
# 配置中使用符号链接
ssl_certificate /etc/nginx/ssl/current/server.crt;
ssl_certificate_key /etc/nginx/ssl/current/server.key;

更新证书时,只需更换符号链接指向:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 创建新证书目录
sudo mkdir -p /etc/nginx/ssl/update_$(date +%Y%m%d)

# 复制新证书到新目录
sudo cp server_new.crt /etc/nginx/ssl/update_$(date +%Y%m%d)/
sudo cp server_new.key /etc/nginx/ssl/update_$(date +%Y%m%d)/

# 切换符号链接
sudo ln -sfn /etc/nginx/ssl/update_$(date +%Y%m%d)/server.crt /etc/nginx/ssl/current/server.crt
sudo ln -sfn /etc/nginx/ssl/update_$(date +%Y%m%d)/server.key /etc/nginx/ssl/current/server.key

# 触发 reload
sudo nginx -s reload

这种方法的好处是:

  • 原子操作,不会出现半更新的状态
  • 轻松回滚:如果新证书有问题,只需切换回旧的符号链接
  • 便于审计:保留历史证书版本

2. 监控 reload 状态

可以通过以下命令监控 Nginx 的工作进程:

1
2
3
4
5
# 查看 Nginx 进程状态
ps aux | grep nginx

# 实时查看连接状态
watch -n 1 'ss -tn | grep :443 | wc -l'

3. 设置合理的 worker_processes

确保 Nginx 配置中使用了合适的 worker 进程数:

1
2
3
4
5
# 自动检测 CPU 核心数
worker_processes auto;

# 每个 worker 允许处理的连接数
worker_connections 1024;

常见问题排查

问题一:reload 后证书没有更新

可能的原因:

  1. 证书文件路径错误
  2. 证书和私钥不匹配
  3. 配置文件没有正确指定证书路径

排查方法:

1
2
3
4
5
# 检查 Nginx 错误日志
sudo tail -f /var/log/nginx/error.log

# 查看当前配置加载的证书
nginx -T 2>&1 | grep -A 5 "ssl_certificate"

问题二:部分连接仍在使用旧证书

这是正常现象。旧的工作进程在处理完现有连接后会退出,新连接会使用新证书。可以通过以下命令强制关闭旧的 worker 进程:

1
2
3
4
5
# 查看当前 worker 进程
ps aux | grep nginx

# 如果确认所有请求都已处理完毕,可以优雅关闭旧进程
kill -QUIT $(cat /var/run/nginx.pid.oldbin)

问题三:reload 失败

通常是由于配置文件语法错误:

1
2
3
4
# 查看详细错误信息
nginx -t 2>&1

# 如果有语法错误,会显示具体行号和错误原因

自动证书更新脚本

结合 ACME 客户端(如 Certbot),可以编写自动证书更新脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#!/bin/bash
# auto_renew_ssl.sh

# 更新证书
certbot renew --quiet --post-hook "nginx -s reload"

# 验证新证书
if [ $? -eq 0 ]; then
    echo "$(date): Certificate renewed successfully"
    
    # 验证证书已更新
    echo | openssl s_client -servername example.com -connect localhost:443 2>/dev/null | openssl x509 -noout -enddate
else
    echo "$(date): Certificate renewal failed"
    exit 1
fi

将脚本添加到 crontab:

1
2
# 每天凌晨 3 点检查并更新证书
0 3 * * * /path/to/auto_renew_ssl.sh >> /var/log/cert_renewal.log 2>&1

总结

通过 Nginx 的 reload 机制,我们可以在不中断服务的情况下更新 TLS 证书。关键步骤包括:

  1. 准备新的证书文件并验证
  2. 使用 nginx -t 测试配置
  3. 执行 nginx -s reload 或发送 HUP 信号
  4. 验证新证书已生效

遵循本文的最佳实践,可以确保证书更新过程平稳、安全,不会影响用户体验。


参考来源