在边缘节点上收口 TLS:一次落地的架构笔记
这篇记录一次真实的调整:把公网 443 交给前置传输层处理 TLS,业务 Web 服务退回到 127.0.0.1,两者之间只通过回环地址通信,并且用 PROXY protocol 把访客真实 IP 带进来。写下来主要是为了以后少踩同样的坑。
为什么要这么拆
单机跑服务时,最容易出现的问题不是性能,而是「谁该负责 TLS」不清不楚。常见组合里同时存在三件事:
- 公网证书的签发与续期;
- 对非目标域名的探测请求做静默丢弃;
- 业务本身只需要关心内容和路由。
把这三件事拆开之后,每一层都变得简单:前置层只负责握手和转发,Web 层只负责页面,证书由脚本统一管理。
一个经验:业务端口不直接暴露在公网,几乎所有「被扫、被刷、被爆破」的烦恼都会同时消失。
结构长什么样
最终稳定下来的链路是「公网 443 → 本地回环上的 TLS 站点 → 静态文件」。其中回环站点额外监听了两个东西:
ssl与proxy_protocol,用来接收前置层转发过来的握手与地址信息;- 一个
default_server兜底块,SNI 不匹配时直接拒绝,避免被当成通用克隆站。
server {
listen 127.0.0.1:8001 ssl proxy_protocol default_server;
ssl_reject_handshake on;
}
server {
listen 127.0.0.1:8001 ssl http2 proxy_protocol;
server_name lax.eb.xixicc.com;
set_real_ip_from 127.0.0.1;
real_ip_header proxy_protocol;
root /var/www/html;
index index.html;
location / { try_files $uri $uri/ =404; }
}
注意:
listen 里带 proxy_protocol 后,任何不带 PROXY 头的连接都会直接失败。所以要么前置层始终发送,要么单独留一个普通端口给本地调试。证书自动化:webroot 与 standalone 怎么选
这块是最容易翻车的地方。standalone 模式需要自己占用 80 端口,只要前面有 Web 服务,续期就必然失败;而且失败通常是静默的——直到某天浏览器告警才被发现。
用 webroot 就没有这个问题:验证文件写进一个固定目录,Web 服务把它当普通静态文件返回就行。
location /.well-known/acme-challenge/ {
root /var/www/acme;
}
location / {
return 444; # 其余请求直接断开,不给探测留信息
}
续期成功后还需要把新证书同步到实际使用它的地方,并让服务重载。这一步同样建议写成固定命令,而不是靠手工记得去重启。
让访问日志说真话
前置层转发时,如果不加处理,日志里所有请求的来源都是 127.0.0.1,等于没有日志。解决办法是把 log_format 换成 $proxy_protocol_addr,并配合 set_real_ip_from 让业务侧也能拿到真实地址。
log_format main '$proxy_protocol_addr - - [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" "$http_user_agent"';
改完之后日志立刻变得可用:能区分正常访客和扫描器,也能看出某个 IP 是不是在反复试探后台路径。
几处事后想起来值得强调的细节
- 静态站点一定要引入
mime.types,否则 CSS 会以application/octet-stream返回,页面直接裸奔。 - 旧的
listen ... http2写法与新版本 nginx 的http2 on;不通用,升级前先确认版本。 - 证书私钥权限按最小可用配置,读到文件的应该是启动时的特权进程。
- 兜底块不要返回默认欢迎页,那等于告诉别人「这台机器刚装好」。
整套东西跑稳之后,日常几乎不需要登录服务器:证书自己续,日志按天看一次,其余的交给服务本身。这才是我想要的状态。