Administrator
发布于 2026-10-22 / 0 阅读
0
0

从一个空站到 HTTPS:我在这台服务器上踩到的六个坑

背景

服务器是阿里云轻量应用服务器,上面跑着 Halo 博客,域名正在备案。备案没过之前 443 开不了,证书也签不了,所以 HTTPS 这件事一直被推到最后。

推到最后的结果是:方案早就写好了,真到要跑的时候才发现它跑不了。

这篇文章记的是"一个全新 Halo 站 + 一份网上抄来的 Nginx 反代脚本",会遇到哪些互相打架的地方。


坑 1:脚本要 apt install nginx,可 nginx 早就装好了

最常见的一类错误是:脚本假设服务器是台"什么都没有"的干净机器,于是上来就装一遍该装的东西。

但实际情况是,你的 Halo 前面早就站着一个 Nginx 了。你甚至可能就是靠它才能访问到 Halo。

这时候再执行一次:

apt install -y nginx

apt 会发现"系统里已经有个 nginx 了",然后把你那个 1.27.2 降级成发行版仓库里的旧版本。降级过程中配置文件格式不兼容、服务重启失败,站点就没了。

教训:写运维脚本的第一步应该是检测现状,而不是假设现状。 判断逻辑应该是"nginx 在吗?在就跳过装,只校验版本和配置"。


坑 2:limit_req_zone 写在了 server 块里

限流是好事,但指令放错层级会直接让 Nginx 拒绝启动:

server {
    listen 80;
    limit_req_zone $binary_remote_addr zone=halo_limit:10m rate=10r/s;  # ❌ 语法错误
}

limit_req_zone 只允许出现在 http 上下文(也就是配置文件的最外层,或者 include 进来的文件顶层),写在 server 甚至 location 里面,nginx -t 会直接报错:

nginx: [emerg] "limit_req_zone" directive is not allowed here

而 include /etc/nginx/conf.d/*.conf; 这行本身就在 http 块里,所以把配置文件写成独立文件丢进 conf.d,天然就是正确的上下文。

# /etc/nginx/conf.d/halo-site.conf —— 顶层,直接写
limit_req_zone $binary_remote_addr zone=halo_limit:10m rate=30r/s;

坑 3:add_header 在 location 里会把安全头全部吃掉

这是 Nginx 里最隐蔽的一个继承规则。

安全响应头通常写在 server 块:

server {
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
}

但只要任何一个 location 块里也写了 add_header,那么这个 location 里的响应就只会带上 location 自己写的那几条,server 级的全部作废:

location ~* \.(js|css|png)$ {
    proxy_pass http://127.0.0.1:8090;
    add_header Cache-Control "public, immutable";   # ❌ 安全头在这里被清空了
}

规则不是"合并",是"覆盖"。所以静态资源虽然不会被告警,但也拿不到 X-Frame-Options 了。

正确做法:安全头全部放 server 级,location 里改用 expires:

location ~* \.(js|css|png)$ {
    proxy_pass http://127.0.0.1:8090;
    expires 30d;        # ✅ 走 expires 指令,不影响 add_header 继承
}

坑 4:签证书之前就把 HTTP 跳成了 HTTPS

很多人的脚本里,80 端口的跳转是一开始就打开的:

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

问题是:证书是下一步才签的。签证书需要通过 80 端口访问 /.well-known/acme-challenge/ 来验证域名归属。

如果 80 端口一上来就 301 到 https,而此时 443 上还没有证书、连 443 server 块都还没写——整个站点在这几十秒里是死的。

正确顺序是分两阶段:

  1. 阶段一:80 端口正常反代 Halo,ACME 路径单独放行 → 签证书
  2. 阶段二:证书拿到后,把 80 改成 301,加 443 server 块 → 切 HTTPS

切换过程中站点不中断。


坑 5:Connection "upgrade" 被无条件下发

WebSocket 代理的经典写法是:

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

问题在于第二行是写死的。哪怕这个请求压根不是 WebSocket 升级(普通页面加载、API 调用),也照样告诉后端"我要升级协议"。

规范做法是用一个 map 控制:

# http 上下文
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;   # ✅ 有才升级,没有就 close
}

坑 6:写完配置不检查"到底生效没有"

最隐蔽的一种失败:你把配置文件写到了某个目录,nginx -t 也过了,脚本打印"✅ 配置已生效"——但那个目录根本没被 nginx.conf include。

配置文件放错地方 = 什么都没发生,脚本却报成功。

两道保险:

# 1. 确认 include 路径
grep -n "include" /etc/nginx/nginx.conf

# 2. 配置写完后,用 -T 导出"实际生效"的完整配置再查一遍
nginx -T | grep -q "server_name example.com" || { echo "配置没被载入"; exit 1; }

nginx -T 和 nginx -t 的区别是:-t 只做语法检查,-T 会把真正加载进内存的完整配置打印出来。用它来做"生效校验"最可靠。


附带一条:别把 Halo 端口暴露到公网

最后说个做对了的。

很多人改完 Nginx 会顺手开一个 8090 端口让 Halo 直连,方便调试。这样就等于绕过了 Nginx——前面配的所有限流、安全头、以后加的 HTTPS 全部失效。

正确做法是让 Halo 只监听 127.0.0.1,公网只暴露 80/443:

# 确认外网访问不到 Halo 自身端口
curl -s -o /dev/null -w "%{http_code}" http://<你的IP>:8090
# 期望:000(连接被拒)

小结

这六条里,没有一条是"知识盲区",全都是假设现状和没验证结果造成的:

坑本质
重装 nginx假设了服务器状态
limit_req_zone 层级没查指令的上下文要求
add_header 覆盖不清楚继承规则
提前 301没考虑中间态
Connection 写死抄了习惯写法
不校验生效只做了语法检查,没做结果检查

运维脚本里,"我觉得配好了"和"它真的生效了"之间,差的往往是最后那两行校验。

备案还有几天要等,正好有时间把这套东西理顺。等域名一解析过来,直接跑就行。


评论