Free

ให้ Claude ลบ IP ของเซิร์ฟเวอร์ต้นทางออกจาก DNS สาธารณะ

IP ของ origin หลัง Cloudflare รั่วผ่าน subdomain deploy — สาม repo สิบสี่วินาที แก้รวบในรอบเดียว


บ่ายวันนั้นผมจ้อง GitHub อยู่ครู่หนึ่ง สาม repo ปิด PR เวลา 16:20:31, 16:20:38 และ 16:20:45 — ห่างกัน สิบสี่วินาที

  • smarts #38: deploy.smarts.mddeploy.smarts
  • how2claude #13: deploy.how2claude.comdeploy.how2claude
  • pickful #118: deploy.pickful.ai / deploy.pickful.xyzdeploy.pickful / deploy.pickful-alpha

ทั้งสามเป็นการแก้บั๊กชนิดเดียวกัน เดิมทีผมแค่คุยกับ Claude เรื่องปัญหาเล็ก ๆ ที่ไม่เกี่ยวข้องกันในการ deploy ของ how2claude แต่มันเหลือบไปดู config/deploy.yml แล้วบอกผมว่า IP ของเซิร์ฟเวอร์ต้นทางของเครื่องนั้นโผล่อยู่ใน DNS สาธารณะแบบเปลือยเปล่า

ภาพลวงตาที่หลอกผมมาหลายเดือน

โปรเจกต์ของผมทุกตัวอยู่หลัง Cloudflare ก้อนเมฆส้มเล็ก ๆ ในแดชบอร์ด Cloudflare หมายถึง "เรคคอร์ดถูก proxy" — request HTTP จะลงที่ edge node ของ Cloudflare ก่อน แล้วค่อยส่งไปที่ origin ของผม IP ของ origin ไม่เคยปรากฏใน DNS response; สิ่งที่ DNS คืนกลับเป็น anycast IP ของ Cloudflare ส่วนนี้ถูกต้อง

แต่ผม deploy ด้วย Kamal Kamal SSH เข้าไปบนเซิร์ฟเวอร์เพื่อรัน docker SSH ไม่สามารถผ่าน HTTP proxy ได้ ดังนั้นผมจึงต้องการ hostname ที่ ไม่ผ่าน proxy ของ Cloudflare เพื่อ SSH ตอนนั้นค่าคอนฟิกของผมเป็นแบบนี้:

# config/deploy.yml
servers:
  web:
    - deploy.how2claude.com   # ← เรคคอร์ด A ตรงไปยัง IP ของ origin, ก้อนเมฆเทา (DNS only)

ใน Cloudflare how2claude.com กับ www.how2claude.com เป็นสีส้ม ส่วน deploy.how2claude.com เป็นสีเทา มันต้องเป็นสีเทา ไม่งั้น SSH จะไปไม่ถึงเครื่อง

สีเทาแปลว่า DNS สาธารณะ ใครก็ตามสามารถรัน:

$ dig deploy.how2claude.com +short
<IP origin ของผม>

และข้อตกลงตั้งชื่อ deploy.<domain> มันคือสัญญาณบอกใบ้ในตัวเอง — สแกน subdomain deploy.* กับลิสต์ของโดเมน SaaS ยอดนิยมแล้วคุณจะได้ IP origin จำนวนหนึ่งที่ควรจะซ่อนอยู่หลัง Cloudflare

ข้าม WAF ของ Cloudflare ข้าม rate limiting ข้ามการป้องกัน DDoS ห่างไป dig คำสั่งเดียว

ทำไม Claude ถึงสังเกตเห็น

วันนั้นเราคุยกันเรื่องอื่น ผมขอให้มันรีวิวเอกสาร deploy ที่ผมเพิ่งเขียน มันเปิด config/deploy.yml เพื่อตรวจอ้างอิง อ่านไปถึงบรรทัดที่ 7 — - deploy.how2claude.com — หยุดสักพักแล้วพูดทำนองนี้:

hostname นี้ resolve ผ่าน DNS สาธารณะใช่ไหมครับ? นั่นแปลว่าใครก็ตามที่รัน DNS query สามารถได้ IP ของ origin นั้น และ edge proxy ของ Cloudflare ก็ถูกข้าม

ผมนิ่งไปวินาทีหนึ่ง เรื่องนี้ผม ควรจะ เห็นมาตั้งนานแล้ว — แค่มองไอคอนก้อนเมฆเทาตอนตั้งค่า Cloudflare ซ้ำอีกครั้งและถามตัวเองว่ามันหมายความว่าอย่างไรก็พอ — แต่ผมไม่ได้ทำ ผมปฏิบัติต่อ hostname นี้เหมือนเป็นอะไรที่ "ภายใน" เพียงเพราะคำนำหน้าเป็น deploy. สมองผมแอบมอบความเป็นส่วนตัวที่มันไม่เคยมีให้กับมันโดยเงียบ ๆ

Claude ไม่มีอคติแบบนั้น มันอ่าน string ใน yaml field และถามคำถามเชิงกลไก: string นี้กลายเป็น IP ได้อย่างไร? คำตอบ: DNS สาธารณะ ข้อสรุป: IP ของ origin เป็นสาธารณะ

วิธีแก้: alias ใน /etc/hosts

วิธีแก้กลับง่ายจนน่าอาย Kamal ขอให้ SSH client ในเครื่องเป็นคน resolve hostname ดังนั้น hostname นี้ต้อง resolve ได้แค่ บนเครื่องของผม — ไม่ต้อง resolve ได้ทั่วอินเทอร์เน็ต

ทำให้ hostname ใน yaml สั้นลง:

servers:
  web:
    - deploy.how2claude    # ← สังเกต: ไม่มี .com

แล้วเพิ่มหนึ่งบรรทัดใน /etc/hosts ของผม:

198.51.100.42  deploy.how2claude

CI runner ที่รัน deploy ก็ต้องการบรรทัดเดียวกัน (env var หรือใส่ตรง ๆ ด้วย echo >> /etc/hosts ใน workflow)

ผลลัพธ์:

  • ไม่มีเรคคอร์ด deploy.how2claude.* อยู่ใน DNS สาธารณะที่ไหนเลย
  • ผู้โจมตีไม่สามารถดึง IP ผ่าน DNS ได้ — อย่างน้อยทางนี้ก็ปิดแล้ว
  • คำสั่ง kamal deploy / app exec / app logs ทุกตัวทำงานต่อได้ตามปกติ เพราะ /etc/hosts ถูกอ่านก่อน DNS
  • Cloudflare สะอาดขึ้น: subdomain ก้อนเมฆเทาที่ดูเก้ ๆ กัง ๆ นั้นหายไปเฉย ๆ

accessory ของฐานข้อมูลก็ต้องเปลี่ยนเหมือนกัน:

accessories:
  db:
    image: postgres:17
    host: deploy.how2claude   # ← hostname เดียวกัน

Claude ขนเรื่องนี้ไปทั้งสาม repo

นี่คือส่วนที่ผมอยากบันทึกไว้

หลังจากเรา ship fix ของ how2claude ผมกำลังจะเปลี่ยน context Claude หยุดผมไว้: "คุณก็ใช้ Kamal กับ smarts และ pickful ใช่ไหมครับ? ขอเช็คสองอันนั้นด้วย"

มันก็ไปเช็ค

  • smarts: deploy.smarts.md — ปัญหาเดียวกัน เรคคอร์ด A สาธารณะ
  • pickful: deploy.pickful.ai (production), deploy.pickful.xyz (alpha), บวกกับ deploy-test1.pickful.ai ที่ตายไปนานแล้ว และ deploy.staging.yml ที่ยังอ้างถึงโดเมนที่ปลดระวางไปแล้ว blockgeek.com

pickful ยุ่งที่สุดในสาม repo เพราะมีหลาย destination (production / alpha / test2) บวกกับสัมภาระทางประวัติศาสตร์ ขณะกวาดอยู่ Claude ก็เก็บกวาด destination staging กับ test1 ที่ตายไปแล้วด้วย — ไม่มีเหตุผลให้คงอยู่ต่อแล้ว แค่ noise

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)

สิบสี่วินาที แน่นอนว่านี่เป็นแค่เวลาที่ GitHub เคลียร์คิว merge ออก — งานจริงกระจายอยู่ในสองชั่วโมงก่อนหน้า แต่รูปลักษณ์ที่มีผลคือ: หนึ่งการค้นพบ, สาม repo ได้รับการแก้ไข

ถ้าผมทำคนเดียว เวอร์ชันเหมือนจริงคือ: แก้ใน how2claude, จด TODO ว่า "ทำ smarts กับ pickful ด้วย" แล้วเฝ้าดู TODO นั้นนอนอยู่ในลิสต์สามเดือน ผมเห็นลำดับเหตุการณ์แบบนี้กับตัวเองมาแล้วหลายครั้ง

เช็คลิสต์ที่ก๊อปไปใช้ได้เลย

ถ้าคุณ deploy ด้วย Kamal, Capistrano หรือเครื่องมือ deploy ใด ๆ ที่ใช้ SSH:

  1. เปิด config/deploy*.yml แล้ว grep หา host: กับ servers: ลิสต์ทุก hostname ที่ปรากฏ
  2. รัน dig +short กับแต่ละตัว ตัวไหนที่คืน IP ของ origin แทนที่จะเป็นค่าว่าง คือกำลังรั่ว
  3. เดินดูแดชบอร์ด CDN (Cloudflare ฯลฯ) แล้วมอง subdomain ที่เป็น "ก้อนเมฆเทา" — นั่นคือชุดผู้สมัคร
  4. เปลี่ยนชื่อเป็น alias แบบไม่มี TLD (deploy.<project>) แล้วใส่ IP ลงใน /etc/hosts
  5. อัพเดต deploy job ใน CI ด้วย บน GitHub Actions: echo "$IP deploy.<project>" | sudo tee -a /etc/hosts
  6. ลบเรคคอร์ด A สาธารณะตัวเดิม

ถ้าทีมคุณดูแลหลายโปรเจกต์ที่ใช้รูปแบบ infrastructure เดียวกัน เขียน grep แบบบรรทัดเดียวที่กวาด deploy*.yml ทั้งหมดในทุก repo จะน่าเชื่อถือกว่ามาเช็คทีละอัน

บทเรียนที่แท้จริง

ไม่ใช่ Cloudflare ล้มเหลว ไม่ใช่ Kamal ออกแบบผิด — เครื่องมือทั้งสองทำสิ่งที่ควรทำได้อย่างถูกต้อง

บทเรียนคือ: ค่า default หลอกคุณอย่างเงียบ ๆ deploy.<your-domain>.com ฟังดูเหมือนชื่อภายใน แต่ DNS ไม่มีแนวคิดของคำว่า "ภายใน" — เรคคอร์ด A ก็คือเรคคอร์ด A และเมื่อมันถูกเผยแพร่แล้วก็เป็นสาธารณะ ชื่อสามารถสร้างภาพลวงให้คุณรู้สึกถึงความเป็นส่วนตัว และไอคอนก้อนเมฆเทาเล็ก ๆ ในแดชบอร์ด Cloudflare ก็คือเวอร์ชันภาพของภาพลวงตัวเดียวกัน: นั่งอยู่ตรงนั้นเงียบ ๆ ไม่มีคำเตือนสีแดง แต่ที่มันบอกคุณจริง ๆ คือ "เรคคอร์ดนี้ไม่ผ่านฉัน"

ลองให้ Claude อ่านคอนฟิก deploy ของคุณสักรอบ มันไม่ได้แชร์ภาพลวงนั้น