Files
tamabox/outputs/cloudflare-origin-lockdown-guide.md
T
tamakyi e3e59e7c69 chore: baseline snapshot before moderation-strictness feature
Repo .git was damaged (refs/ missing, index truncated, object store near-empty).
Repaired via git init (rebuilt refs), stale index preserved as .git/index.corrupt-20260910.bak,
then full worktree committed as a rollback baseline.
2026-09-10 10:56:27 +08:00

8.9 KiB
Raw Blame History

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:
    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

[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 并要求客户端证书:
      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 并强制客户端证书 (需改启动代码,示意):
      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 注释已更新。