免费

让 Claude 把源站 IP 从公网 DNS 里抹掉

Cloudflare 后面的源站 IP 会从 deploy 子域漏出去——三个仓库 14 秒一次性修齐


那天下午我盯着 GitHub 看了一会儿。三个仓库都关了 PR,时间戳分别是 16:20:31、16:20:38、16:20:45 —— 14 秒

  • smarts #38:把 deploy.smarts.md 改成 deploy.smarts
  • how2claude #13:把 deploy.how2claude.com 改成 deploy.how2claude
  • pickful #118:把 deploy.pickful.ai / deploy.pickful.xyz 改成 deploy.pickful / deploy.pickful-alpha

修的是同一类问题。本来只是和 Claude 在 how2claude 这个仓库聊一个不相关的部署小坑,它顺手扫了一眼 config/deploy.yml 然后告诉我:你这台机器的源站 IP 在公网 DNS 里是裸奔的。

一个把自己骗了好几个月的错觉

我所有项目都套了 Cloudflare。Cloudflare 控制台里那个橙色的小云朵亮着,意思是"经过代理"——HTTP 请求会先到 Cloudflare 边缘节点,再到我的源站。源站 IP 不会出现在 DNS 响应里,DNS 拿到的是 Cloudflare 的 anycast IP。这一层做对了。

但我用 Kamal 部署。Kamal 通过 SSH 连到服务器跑 docker。SSH 不能走 HTTP proxy,所以我需要一个 不被 Cloudflare 代理 的 hostname 来 SSH。我当时的做法是:

# config/deploy.yml
servers:
  web:
    - deploy.how2claude.com   # ← 直接 A 记录指向源站 IP,灰色云朵(DNS only)

Cloudflare 控制台里 how2claude.comwww.how2claude.com 都是橙色的,但 deploy.how2claude.com 是灰色的——必须灰色,否则 SSH 就连不上了。

灰色意味着公开 DNS。任何人都可以:

$ dig deploy.how2claude.com +short
<我的源站 IP>

然后 deploy.<domain> 这种命名约定本身就是个明显的标记——你只要随便扫一些常见 SaaS 域名的 deploy.* 子域名,就能批量收割原本应该藏在 Cloudflare 后面的源站 IP。

绕过 Cloudflare WAF、绕过速率限制、绕过 DDoS 防护,一个 dig 命令的事。

为什么 Claude 会注意到

那天聊的其实是另一个话题。我要它检查一份新写的部署文档,它打开 config/deploy.yml 对照着读,读到第 7 行 - deploy.how2claude.com,停了一下,然后说了大致这样一句话:

你这个 hostname 是公开 DNS 解析的吧?那就意味着任何 DNS 查询都能拿到这个 IP,Cloudflare 的边缘代理就被绕过去了。

我当时就愣了一下。这个事我大概应该早就想到——只要 Cloudflare 配置时多看一眼那个灰色云朵,应该就能反应过来——但我没有,我把这个 hostname 当成"内部"的,只是因为名字里有 deploy. 前缀,潜意识就给它加了一层根本不存在的私有性。

Claude 没有这个潜意识。它读到的就是一个 yaml 字段的字符串值,然后机械地问:这个字符串怎么解析成 IP?答案是公网 DNS。结论:源站 IP 公开。

修法:/etc/hosts 别名

修起来出乎意料地简单。Kamal 调用本地的 SSH 客户端去解析 hostname,所以这个 hostname 只要 我自己机器 上能解析就行——不需要全世界都能解析。

把 yaml 里的 hostname 改短:

servers:
  web:
    - deploy.how2claude    # ← 注意:没有 .com

然后在我自己的 /etc/hosts 里加一行:

198.51.100.42  deploy.how2claude

CI 跑 deploy 的时候,runner 上也加同样的一行(环境变量或者直接在 workflow 里 echo >> /etc/hosts)。

完成后:

  • 公网 DNS 里 没有 任何叫 deploy.how2claude.* 的记录
  • 攻击者扫不到这台机器的 IP,至少 DNS 这条路堵住了
  • Kamal 的所有 deploy / app exec / app logs 命令照常工作,因为 /etc/hosts 在 DNS 之前被查
  • Cloudflare 控制台变得更干净:deploy.* 这种灰色云朵的尴尬子域名直接删掉

数据库 accessory 那一行同样要改:

accessories:
  db:
    image: postgres:17
    host: deploy.how2claude   # ← 同样的 hostname

Claude 把这件事干到三个仓库

这才是我想写这篇的原因。

How2claude 改完之后我准备转去做下一件事,Claude 主动停下说:你不是还有 smarts 和 pickful 也用 Kamal 吗,我去看看那两个是不是同样的问题。

它去看了。

  • smarts:deploy.smarts.md —— 同样的问题,A 记录公开
  • pickful:deploy.pickful.ai (production)、deploy.pickful.xyz (alpha)、还有一个早就废弃的 deploy-test1.pickful.ai 和一份引用了过期 blockgeek.com 域名的 deploy.staging.yml

pickful 比另外两个复杂一点,因为它有多个 destination(production / alpha / test2)外加历史遗留的 staging 配置。Claude 顺手把死掉的 staging 和 test1 destination 也清了——本来就没人用,留着只是噪音。

最后的 commit 时间戳:

smarts       6482472   2026-04-27 16:20:31  Use private deploy.smarts alias instead of public deploy.smarts.md (#38)
how2claude   ccc0344   2026-04-27 16:20:38  Use private deploy.how2claude alias instead of public deploy.how2claude.com (#13)
pickful      e6bf9af   2026-04-27 16:20:45  Deploy config hygiene + privatize hostnames (#118)

跨度 14 秒。当然这只是 GitHub 把队列里 PR 都 merge 完的时间,每个仓库的实际工作分散在前面两小时——但效果上就是:一个发现,三处修齐。

如果是我自己单干,最大的概率是这样:在 how2claude 改完,记一个待办"smarts 和 pickful 也要改",然后这个待办在 todo list 里躺三个月。我亲眼见过自己很多次这种事。

一些可以直接照抄的检查项

如果你也用 Kamal / Capistrano / 任何基于 SSH 的部署工具:

  1. 打开 config/deploy*.yml,搜 host:servers:,把所有出现的 hostname 列出来
  2. 对每一个 hostname 跑 dig +short。如果返回的是源站 IP 而不是空,就是漏的
  3. 检查 Cloudflare(或任何 CDN)控制台里有没有"灰色云朵"的子域名 —— 它们就是候选名单
  4. 改成不带 TLD 的私有别名(deploy.<project>),把 IP 写进 /etc/hosts
  5. CI 的 deploy job 也要写一行 hosts 注入。GitHub Actions 里就是 echo "$IP deploy.<project>" | sudo tee -a /etc/hosts
  6. 删除原来那条公开 A 记录

如果你公司有多个项目共享同一套基础设施,写一个简单的 grep 脚本扫所有仓库的 deploy*.yml,比一个一个查靠谱。

真正的教训

不是 Cloudflare 没用,也不是 Kamal 设计有问题——两个工具都在做它们该做的事。

教训是:默认值很容易把你骗了deploy.<your-domain>.com 听起来像一个内部命名,但 DNS 本身没有"内部"概念,A 记录就是 A 记录,写出去就是公开的。一个名字可以让你产生它是私有的错觉,而 Cloudflare 控制台里那个灰色云朵图标就是这个错觉的可视化版本——它在屏幕上看起来很安静,没有红色警告,但它说的是"这条记录不经过我"。

让 Claude 看你的 deploy 配置一眼。它没有这种错觉。