KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA

06 · nginx HTTPS 部署:配置、兼容坑与回滚 — keel 龙骨

本章目标:HTTPS 返回 200,证书链完整,HTTP 全部 301 到 HTTPS。 验收标准:curl -I https://www.xxxxxx.cn/ 返回 200,且 curl -I http://www.xxxxxx.cn/ 返回 301。

本章目标:HTTPS 返回 200,证书链完整,HTTP 全部 301 到 HTTPS。
验收标准:curl -I https://www.xxxxxx.cn/ 返回 200,且 curl -I http://www.xxxxxx.cn/ 返回 301。


一、完整配置骨架(可直接抄)

设计原则:两个 server 块,80 只做跳转,443 承载全部业务 location。 不要试图在一个块里用 if ($scheme = http) 判断,也不要把 location 复制两份。

# ---------- 80:全站 301 到 https ----------
server {
    listen 80;
    server_name www.xxxxxx.cn xxxxxx.cn;

    # 证书校验通道必须留在 http 侧,不能被下面的 301 吃掉
    location /.well-known/acme-challenge/ {
        root /var/www/html;
        default_type "text/plain";
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

# ---------- 443:业务全部在这里 ----------
server {
    listen 443 ssl http2;                  # ← 注意写法,见第二节
    server_name www.xxxxxx.cn xxxxxx.cn;

    ssl_certificate     /etc/nginx/ssl/xxxxxx.cn_bundle.crt;
    ssl_certificate_key /etc/nginx/ssl/xxxxxx.cn.key;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 1d;

    # 反向代理时把协议告诉后端,否则后端生成的链接还是 http
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header Host              $host;

    root /home/ubuntu/fqm/projects/learning-platform/public;
    index index.html;

    location /assets/ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    location /learning/api/ {
        proxy_pass http://127.0.0.1:5177;   # 后端服务保持不变
    }

    location / {
        try_files $uri $uri/ /index.html;
    }

    access_log /var/log/nginx/learning.access.log;
    error_log  /var/log/nginx/learning.error.log;
}

二、版本兼容坑:http2 on; 会让 nginx -t 直接失败

这是本章最实际的一个坑。 nginx 1.25.1 之前不支持独立的 http2 on; 指令,必须写成:

listen 443 ssl http2;      # ✅ nginx < 1.25.1 用这个

而新版本推荐(未来会强制):

listen 443 ssl;
http2 on;                  # ✅ nginx >= 1.25.1

先查版本再决定:

nginx -v

写错的表现是 nginx -t 报 unknown directive "http2",因为做了备份所以会自动回滚——但这正是第 04 章强调"先备份再改"的原因:没有备份的话,这个错误会让 nginx 停在损坏的配置上,站点直接下线。

另外注意 ssl on; 这个古老写法同样已被废弃(1.15+ 会告警,之后移除)。统一用 listen ... ssl。


三、$host 还是 $server_name?

return 301 https://$host$request_uri;

用 $host:它取的是请求里的 Host 头,能正确处理 xxxxxx.cn → https://xxxxxx.cn/ 的情况。
用 $server_name 会把所有域名都跳到 server 块里的第一个名字,导致裸域名的访问被硬拉到 www 上——如果你希望保留两个域名各自跳转(而非统一到 www),这是错的。

若你决定统一到 www(推荐,配合 canonical):

server {
    listen 80;
    server_name xxxxxx.cn www.xxxxxx.cn;
    return 301 https://www.xxxxxx.cn$request_uri;
}
server {
    listen 443 ssl http2;
    server_name xxxxxx.cn;
    ssl_certificate     /etc/nginx/ssl/xxxxxx.cn_bundle.crt;
    ssl_certificate_key /etc/nginx/ssl/xxxxxx.cn.key;
    return 301 https://www.xxxxxx.cn$request_uri;
}

三个 server 块:80 全跳 www、443 裸域名跳 www、443 www 承载业务。HTTPS 跳转必须发生在 TLS 握手之后,所以裸域名的 443 块也要配证书,否则用户访问 https://xxxxxx.cn 会先看到证书告警。


四、上线动作序列(顺序不能乱)

# 1. 备份现网配置
sudo cp /etc/nginx/sites-enabled/learning /tmp/learning.http.bak

# 2. 写入新配置(用编辑器或 cat > 重定向)
sudo vim /etc/nginx/sites-available/learning

# 3. 语法检查 —— 失败就到此为止
sudo nginx -t

# 4. reload(不是 restart:不断连接)
sudo systemctl reload nginx

# 5. 验收
curl -I https://www.xxxxxx.cn/
curl -I http://www.xxxxxx.cn/

reload 与 restart 的区别要记住:reload 会等旧 worker 处理完现有连接再退出,不掉请求;restart 会短暂中断。生产环境一律 reload。

回滚(30 秒内可完成)

sudo cp /tmp/learning.http.bak /etc/nginx/sites-enabled/learning
sudo nginx -t && sudo systemctl reload nginx

把备份和回滚命令放在同一段脚本里,失败时不用现想:

CONF=/etc/nginx/sites-enabled/learning
sudo cp $CONF /tmp/learning.bak.$(date +%s)
sudo nginx -t || { sudo cp /tmp/learning.bak.* $CONF; sudo systemctl reload nginx; exit 1; }
sudo systemctl reload nginx

五、验收:不只是"能打开"

# 1. HTTP → HTTPS 是否 301
curl -sI http://www.xxxxxx.cn/ | grep -i '^location'
# 期望:location: https://www.xxxxxx.cn/

# 2. 证书链是否完整(看输出里的证书数量与 Verify return code)
echo | openssl s_client -connect www.xxxxxx.cn:443 -servername www.xxxxxx.cn 2>/dev/null \
  | openssl x509 -noout -dates -issuer
curl -vI https://www.xxxxxx.cn/ 2>&1 | grep -i 'SSL certificate verify'

# 3. 协议与 HTTP/2 是否生效
curl -sI --http2 https://www.xxxxxx.cn/ | head -1

# 4. 重定向链有没有多跳(只应有 1 跳)
curl -sIL http://xxxxxx.cn/ | grep -iE '^(HTTP/|location)'

第 2 项最容易出问题:如果 openssl s_client 报 Verify return code: 20 (unable to get local issuer certificate),说明你用的是单张域名证书而非 bundle,缺了中间证书。回到第 05 章,下载时选带 bundle 的那个文件。

第 4 项:重定向链每多一跳,爬虫和用户都要多付一次 RTT。理想状态是 http://xxxxxx.cn → https://www.xxxxxx.cn 一次到位(上面统一到 www 的配置可以做到,如果把 80 先跳 https 裸域名再跳 www,就变成两跳了)。


六、可选加固:HSTS

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

作用:告诉浏览器"以后这个域名只许用 https 访问",省掉一次跳转并防降级攻击。

⚠️ 加之前想清楚:一旦浏览器记住了 HSTS,在 max-age 内你的 https 出问题,用户无法通过访问 http 绕过,也无法点击"继续访问不安全网站"。
建议先设短一点(max-age=300)验证一周,确认 HTTPS 稳定后再改成一年。includeSubDomains 会影响所有子域,有非 HTTPS 子域时别加。


七、本章验收清单

HTTPS 通了只是表象——下一步要检查页面内部是否还残留 http,那才是"能打开但有小毛病"的根源。

进入 keel 阅读