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 設定一眼。它沒有這種錯覺。