Cloudflare 后面的源站 IP 会从 deploy 子域漏出去——三个仓库 14 秒一次性修齐
那天下午我盯着 GitHub 看了一会儿。三个仓库都关了 PR,时间戳分别是 16:20:31、16:20:38、16:20:45 —— 14 秒。
smarts #38:把 deploy.smarts.md 改成 deploy.smartshow2claude #13:把 deploy.how2claude.com 改成 deploy.how2claudepickful #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.com、www.how2claude.com 都是橙色的,但 deploy.how2claude.com 是灰色的——必须灰色,否则 SSH 就连不上了。
灰色意味着公开 DNS。任何人都可以:
$ dig deploy.how2claude.com +short
<我的源站 IP>
然后 deploy.<domain> 这种命名约定本身就是个明显的标记——你只要随便扫一些常见 SaaS 域名的 deploy.* 子域名,就能批量收割原本应该藏在 Cloudflare 后面的源站 IP。
绕过 Cloudflare WAF、绕过速率限制、绕过 DDoS 防护,一个 dig 命令的事。
那天聊的其实是另一个话题。我要它检查一份新写的部署文档,它打开 config/deploy.yml 对照着读,读到第 7 行 - deploy.how2claude.com,停了一下,然后说了大致这样一句话:
你这个 hostname 是公开 DNS 解析的吧?那就意味着任何 DNS 查询都能拿到这个 IP,Cloudflare 的边缘代理就被绕过去了。
我当时就愣了一下。这个事我大概应该早就想到——只要 Cloudflare 配置时多看一眼那个灰色云朵,应该就能反应过来——但我没有,我把这个 hostname 当成"内部"的,只是因为名字里有 deploy. 前缀,潜意识就给它加了一层根本不存在的私有性。
Claude 没有这个潜意识。它读到的就是一个 yaml 字段的字符串值,然后机械地问:这个字符串怎么解析成 IP?答案是公网 DNS。结论:源站 IP 公开。
修起来出乎意料地简单。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)。
完成后:
deploy.how2claude.* 的记录deploy / app exec / app logs 命令照常工作,因为 /etc/hosts 在 DNS 之前被查deploy.* 这种灰色云朵的尴尬子域名直接删掉数据库 accessory 那一行同样要改:
accessories:
db:
image: postgres:17
host: deploy.how2claude # ← 同样的 hostname
这才是我想写这篇的原因。
How2claude 改完之后我准备转去做下一件事,Claude 主动停下说:你不是还有 smarts 和 pickful 也用 Kamal 吗,我去看看那两个是不是同样的问题。
它去看了。
deploy.smarts.md —— 同样的问题,A 记录公开deploy.pickful.ai (production)、deploy.pickful.xyz (alpha)、还有一个早就废弃的 deploy-test1.pickful.ai 和一份引用了过期 blockgeek.com 域名的 deploy.staging.ymlpickful 比另外两个复杂一点,因为它有多个 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 的部署工具:
config/deploy*.yml,搜 host: 和 servers:,把所有出现的 hostname 列出来dig +short。如果返回的是源站 IP 而不是空,就是漏的deploy.<project>),把 IP 写进 /etc/hostsecho "$IP deploy.<project>" | sudo tee -a /etc/hosts如果你公司有多个项目共享同一套基础设施,写一个简单的 grep 脚本扫所有仓库的 deploy*.yml,比一个一个查靠谱。
不是 Cloudflare 没用,也不是 Kamal 设计有问题——两个工具都在做它们该做的事。
教训是:默认值很容易把你骗了。deploy.<your-domain>.com 听起来像一个内部命名,但 DNS 本身没有"内部"概念,A 记录就是 A 记录,写出去就是公开的。一个名字可以让你产生它是私有的错觉,而 Cloudflare 控制台里那个灰色云朵图标就是这个错觉的可视化版本——它在屏幕上看起来很安静,没有红色警告,但它说的是"这条记录不经过我"。
让 Claude 看你的 deploy 配置一眼。它没有这种错觉。