Files
tamabox/outputs/cloudflare-origin-lockdown-guide.md
T
2026-09-08 10:10:44 +08:00

168 lines
8.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` 注释已更新。