免費

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