在边缘节点上收口 TLS:一次落地的架构笔记

这篇记录一次真实的调整:把公网 443 交给前置传输层处理 TLS,业务 Web 服务退回到 127.0.0.1,两者之间只通过回环地址通信,并且用 PROXY protocol 把访客真实 IP 带进来。写下来主要是为了以后少踩同样的坑。

为什么要这么拆

单机跑服务时,最容易出现的问题不是性能,而是「谁该负责 TLS」不清不楚。常见组合里同时存在三件事:

把这三件事拆开之后,每一层都变得简单:前置层只负责握手和转发,Web 层只负责页面,证书由脚本统一管理。

一个经验:业务端口不直接暴露在公网,几乎所有「被扫、被刷、被爆破」的烦恼都会同时消失。

结构长什么样

最终稳定下来的链路是「公网 443 → 本地回环上的 TLS 站点 → 静态文件」。其中回环站点额外监听了两个东西:

  1. ssl 与 proxy_protocol,用来接收前置层转发过来的握手与地址信息;
  2. 一个 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 是不是在反复试探后台路径。

几处事后想起来值得强调的细节

整套东西跑稳之后,日常几乎不需要登录服务器:证书自己续,日志按天看一次,其余的交给服务本身。这才是我想要的状态。

← 返回首页 下一篇:备份这件事,越早开始越便宜