This commit is contained in:
tamakyi
2026-09-08 10:10:44 +08:00
commit 2ed27dd55e
288 changed files with 67061 additions and 0 deletions
+167
View File
@@ -0,0 +1,167 @@
# Cloudflare 锁定源站 实操指南(配合 P0 修复)
> 目标:让 `trust_proxy = true` 安全生效,同时**不维护 Cloudflare 庞大的 CIDR 清单**。
> 代码侧(`internal/shared/ip.go`)已改为:优先信任 `CF-Connecting-IP`,仅在 TCP 对端落在
> `trusted_proxies`(你自己的基础设施)时才解析 `X-Forwarded-For`/`X-Real-IP`。
> 关键前提:**源站必须只能从 Cloudflare 抵达**,这样攻击者无法直连并伪造 `CF-Connecting-IP`。
---
## 0. 为什么必须锁源站
上一轮的 P0 风险是:开启 `trust_proxy = true` 后,只要攻击者能**直连到源站 IP**,
他就可以在请求里塞一个 `CF-Connecting-IP: 他想封的任意 IP`,应用会信以为真。
后果:自动封禁、限流、IP 黑名单全部按**攻击者指定的 IP** 计数 → 可把正常访客(甚至你自己的 IP)打进黑名单。
两种解法都不需要 Cloudflare CIDR 白名单,只是「让攻击者根本连不到源站 / 连到也过不了 TLS」:
- **方案 A:Cloudflare Tunnel** —— 源站不开放任何公网入站端口,`cloudflared` 主动出站连 Cloudflare,
外网流量只走 CF→隧道→本地。攻击者不知道、也连不到源站 IP。**最省事,推荐。**
- **方案 B:Authenticated Origin Pull(mTLS)** —— 源站要求 Cloudflare 回源时出示客户端证书,
没证书的直连在 TLS 握手阶段就被拒,连 HTTP 头都发不出来。保留你现有公网 IP/端口时选它。
---
## 1. 方案 A:Cloudflare Tunnel(推荐)
### 1.1 原理
源站防火墙只放行运维端口(如 22),**不对外开 80/443**。Cloudflare 边缘与源站上的
`cloudflared` 之间建立一条持久出站隧道。访客的请求在 CF 边缘进入,经隧道到达本地
`cloudflared`,再转发给监听在 `127.0.0.1` 的 TamaBox。CF 在边缘做 TLS,所以回源用 HTTP 即可。
### 1.2 步骤
1. **前提**:域名已在 Cloudflare(橙色云代理),源站能主动出网(TCP 443 出站)。
2. **安装 cloudflared**(二选一):
- Debian/Ubuntu:`sudo apt install cloudflared`(或官方 deb:`curl -fsSL https://pkg.cloudflare.com/cloudflared.deb | sudo dpkg -i -`)
- Docker:`docker pull cloudflare/cloudflared:latest`
3. **登录授权**(浏览器选域):`cloudflared tunnel login`
4. **建隧道**:`cloudflared tunnel create tamabox`
记下输出的 tunnel ID 与 credentials 文件路径(默认 `~/.cloudflared/<id>.json`)。
5. **写配置** `~/.cloudflared/config.yml`:
```yaml
tunnel: <你的 tunnel id>
credentials-file: /root/.cloudflared/<id>.json
ingress:
- hostname: yourdomain.com # 改成你的域名
service: http://127.0.0.1:8080 # 改成 TamaBox 实际监听地址(见 1.4)
- service: http_status:404
```
6. **绑定 DNS**(自动加 CNAME 到 Cloudflare):`cloudflared tunnel route dns tamabox yourdomain.com`
7. **常驻运行**:
- systemd:`sudo cloudflared service install` 然后 `sudo systemctl enable --now cloudflared`
- Docker:`docker run -d --name cf-tunnel --restart unless-stopped -v ~/.cloudflared:/root/.cloudflared cloudflare/cloudflared tunnel run`
8. **收紧源站防火墙**:云安全组 / `iptables` 把 **80、443 的 0.0.0.0/0 入站全部删掉**,
只保留你的运维 IP 的 22。这样即便有人探测到源站 IP 也连不上。
### 1.3 app.ini 配置 glue
```ini
[security]
trust_proxy = true
trusted_proxies = 127.0.0.1
```
说明:`cloudflared` 连的是本地,`ResolveClientIP` 看到的 TCP 对端就是 `127.0.0.1`;
`CF-Connecting-IP` 由 Cloudflare 注入并经隧道透传,应用**优先采用它**,无需任何 CIDR。
### 1.4 TamaBox 监听地址建议
`conf/app.ini` 的 `[server] port = 80`。务必让程序**只绑本地**:
- 若用 `tamabox.go` 里 `flamego.Classic()` 默认 `0.0.0.0`,请改成 `127.0.0.1:8080`(或你 tunnel 里写的端口),
双保险——即使防火墙漏配也不会对外暴露。
- 改完重启 TamaBox。
---
## 2. 方案 B:Authenticated Origin Pull(mTLS)
### 2.1 原理
Cloudflare 回源时出示一张由 Cloudflare 私有 CA 签发的**客户端证书**;源站配置「要求并校验客户端证书」。
attackers 直接连源站 443 时,因为没有这张证书,TLS 握手直接失败,根本到不了 HTTP 层,
自然无法发送 `CF-Connecting-IP`。**这步本身已足以挡住伪造,不需要 CIDR。**
### 2.2 步骤
1. **开启 AOP**:Cloudflare 控制台 → **SSL/TLS → Origin Server → Authenticated Origin Pulls** → 开启。
2. **下载 Cloudflare Origin CA**(用于校验 CF 客户端证):同一页底部
**Origin CA certificates → Create Certificate**(或下载根 CA 证书 `cloudflare_origin_ca.pem`),
保存到源站 `/etc/ssl/cloudflare/cf-origin-ca.pem`,权限 `600`。
3. **落地方式二选一**:
- **B1(nginx 在前,最常见)** —— nginx 终止 TLS 并要求客户端证书:
```nginx
server {
listen 443 ssl;
server_name yourdomain.com;
ssl_certificate /etc/ssl/yourdom/fullchain.pem; # 源站证书(可用 CF Origin CA 签发,或自有)
ssl_certificate_key /etc/ssl/yourdom/privkey.pem;
ssl_client_certificate /etc/ssl/cloudflare/cf-origin-ca.pem; # Cloudflare 根 CA
ssl_verify_client on; # 必须校验 Cloudflare 客户端证书
ssl_verify_depth 2;
location / {
proxy_pass http://127.0.0.1:8080; # TamaBox 本地端口
proxy_set_header Host $host;
proxy_set_header CF-Connecting-IP $http_cf_connecting_ip; # 透传给应用(优先采用)
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
```
`sudo nginx -t && sudo systemctl reload nginx`
- **B2(Go 直接对外 TLS,不用 nginx)** —— 让 TamaBox 监听 TLS 并强制客户端证书
(需改启动代码,示意):
```go
srv := &http.Server{
Addr: ":443",
TLSConfig: &tls.Config{
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: cfCACertPool, // 载入 cf-origin-ca.pem
},
}
srv.ServeTLS(ln, "/etc/ssl/yourdom/fullchain.pem", "/etc/ssl/yourdom/privkey.pem")
```
4. **防火墙(可选加固)**:源站 443 只放行 Cloudflare 回源 IP。**但 AOP 的 mTLS 已挡住伪造,
CIDR 只是减少无效握手,可按需省略**——正好契合你「不想维护大 CIDR」的诉求。
### 2.3 app.ini 配置 glue
- 用 nginx(B1):`trust_proxy = true`,`trusted_proxies = 127.0.0.1`
(nginx 在本地,TCP 对端 `127.0.0.1`)。`CF-Connecting-IP` 经 nginx 透传被应用优先采用;
即便没透传,XFF 由受信的 `127.0.0.1` 解析也能拿到真实 IP。
- 用 Go 直连 TLS(B2):`trust_proxy = true`,`trusted_proxies` 可留空
(TCP 对端就是 Cloudflare,`CF-Connecting-IP` 直接采用)。
---
## 3. 怎么选
| 场景 | 选 |
|------|----|
| 单实例、怕配错、不想管证书/防火墙 | **方案 A(Tunnel)** |
| 已有 nginx、必须保留公网 IP、希望源站也强制 TLS | **方案 B(AOP)** |
---
## 4. 验证(上线前务必做)
1. **直连源站公网 IP 应失败**:
- A:`curl -I https://<源站IP>` 应超时 / 连接拒绝。
- B:`curl -I https://<源站IP>` 应 TLS 握手失败(`sslv3 alert handshake failure` 之类)。
2. **经域名访问正常**:`curl -I https://yourdomain.com` 返回 200。
3. **确认记录的是真实客户端 IP**:用不同网络访问,查看限流/统计/日志,
不应再是 Cloudflare 边缘 IP。
4. **伪造头测试(仅限你自己的 IP)**:
`curl -H "CF-Connecting-IP: 1.2.3.4" https://yourdomain.com` —— 应用应忽略该头,记录你的真实 IP。
5. **自动封禁仍生效**:连续触发 404 阈值,确认被封的是真实客户端 IP,不是 CF 边缘。
---
## 5. 注意事项
- `trust_proxy = true` 一旦开启,**源站就必须锁定**,否则比 `false` 更危险
(应用会更信任客户端头)。代码注释已写明此约束。
- Tunnel 下建议 TamaBox 监听 `127.0.0.1`(不要 `0.0.0.0`),双保险。
- 隧道 credentials 与证书文件权限 `600`,**不要进 Git 仓库**。
- 多实例 / 容器:把 `cloudflared` 跑在与 TamaBox 能直连的网络(同主机或同 docker 网络)。
- 证书 / 隧道有有效期,纳入监控或自动续期(Cloudflare 证书默认较长,Tunnel 凭证基本免维护)。
---
## 6. 与代码的对应关系(便于回溯)
- `internal/shared/ip.go`:`ResolveClientIP` 优先 `CF-Connecting-IP`,仅 `isTrustedProxy(remote)` 才解析 XFF。
- `internal/conf/static.go`:`Security.TrustedProxies`(`trusted_proxies`)新字段。
- `internal/cmd/web.go`:`conf.Init()` 后调用 `shared.InitTrustedProxies()` 解析清单。
- `conf/app.sample.ini`:`trust_proxy` / `trusted_proxies` 注释已更新。