SetHttps
Nginx · 部署

Nginx 部署与自动重载

平台只做一件事:把三个文件准确放进你指定的目录。剩下的配置、权限、重载都在你这边, 这一页给出可以直接复制的一套。

目录怎么放

一个域名一个目录,比较省心:

/etc/nginx/ssl/
└── example.com/
    ├── example.com_cert.pem
    ├── example.com_full_chain.pem
    └── example.com_key.pem

多个域名同时用,就并列放 /etc/nginx/ssl/api.example.com/、/etc/nginx/ssl/shop.example.com/。 证书文件名由平台按域名生成,共用一个目录也不会互相覆盖,但分目录更利于排查。

server 块

# /etc/nginx/conf.d/example.com.conf
server {
    listen 80;
    server_name example.com *.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    http2 on;                      # nginx < 1.25.1 用 listen 443 ssl http2;
    server_name example.com *.example.com;

    ssl_certificate     /etc/nginx/ssl/example.com/example.com_full_chain.pem;
    ssl_certificate_key /etc/nginx/ssl/example.com/example.com_key.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_stapling on;
    ssl_stapling_verify on;

    add_header Strict-Transport-Security "max-age=31536000" always;

    root /var/www/example.com;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}
server_name 写 *.example.com 只匹配一级子域名,a.b.example.com 需要另外添加或用正则。 泛域名证书本身覆盖的也是一级子域名。

私钥权限

证书由受限的部署用户上传,落盘属主是那个用户。Nginx 的 worker 进程通常以 www-data(Debian/Ubuntu)或 nginx(RHEL 系)运行,需要能读私钥。稳妥的做法:

chown root:www-data /etc/nginx/ssl/example.com/example.com_key.pem
chmod 640 /etc/nginx/ssl/example.com/example.com_key.pem
chmod 644 /etc/nginx/ssl/example.com/example.com_full_chain.pem

这些命令放在下面的重载脚本里一起执行,就不用每次手动改。

证书更新后自动重载

如果节点填了重启命令,这一步平台已经替你做完了。部署脚本会在服务器上装一个 root 所有的 sethttps-reload 包装脚本和对应的 systemd path 单元,平台上传证书后写入 reload.request 触发它。下面这些方案适用于没有配置重启命令的节点。

重载不是可选项:文件被覆盖了但 Nginx 没有 reload,线上仍然是旧证书。

方案一:监听目录(推荐)

装好 inotify-tools 后,把下面这个脚本放到 /usr/local/bin/ssl-reload.sh:

#!/usr/bin/env bash
# 监听证书目录,文件被写入后校验配置并重载 nginx
set -euo pipefail

DIR="${1:-/etc/nginx/ssl/example.com}"
GROUP="${2:-www-data}"

inotifywait -m -e close_write -e moved_to --format '%w%f' "$DIR" |
while read -r f; do
    case "$f" in
        *_key.pem)
            chown root:"$GROUP" "$f"
            chmod 640 "$f"
            ;;
        *.pem)
            chmod 644 "$f"
            ;;
    esac
    if nginx -t 2>/dev/null; then
        nginx -s reload
        logger -t ssl-reload "reloaded after $f"
    else
        logger -t ssl-reload "nginx -t failed after $f, skip reload"
    fi
done
chmod +x /usr/local/bin/ssl-reload.sh
apt install -y inotify-tools          # RHEL 系:dnf install -y inotify-tools
/usr/local/bin/ssl-reload.sh /etc/nginx/ssl/example.com www-data &

长期运行建议包成 systemd 服务:

# /etc/systemd/system/ssl-reload.service
[Unit]
Description=Reload nginx when certificates change
After=network.target

[Service]
ExecStart=/usr/local/bin/ssl-reload.sh /etc/nginx/ssl/example.com www-data
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now ssl-reload

方案二:定时重载

最省事,代价是每天多一次 reload(对线上几乎没有影响):

# /etc/cron.d/ssl-reload
17 4 * * * root nginx -t && nginx -s reload

方案三:手动

证书少、更新频率低,看到平台通知后自己执行 nginx -s reload 也可以,只是别忘。

验证真的换了证书

# 看线上实际返回的证书有效期
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates

# 看证书链是否完整(返回 0 才算完整)
echo | openssl s_client -connect example.com:443 -servername example.com -verify_return_error 2>&1 \
  | grep -E "Verify return code|depth="

# 看证书覆盖了哪些域名
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -text | grep -A1 "Subject Alternative Name"

排错

现象原因处理
浏览器正常,安卓 / curl 报证书错误只配了 _cert.pem,缺中间证书改用 _full_chain.pem
nginx -t 报 key 不匹配证书与私钥不是同一次签发三个文件一起更新,不要混用旧文件
Permission denied 读私钥worker 用户无权限按「私钥权限」一节改属主和 640
文件已更新但证书没变没有 reload,或 reload 的是另一个 nginx 进程确认 nginx -s reload 作用于同一个 master;容器内要进容器 reload
reload 后短暂 502reload 期间旧 worker 优雅退出正常现象,通常几十毫秒