IP ของ origin หลัง Cloudflare รั่วผ่าน subdomain deploy — สาม repo สิบสี่วินาที แก้รวบในรอบเดียว
บ่ายวันนั้นผมจ้อง GitHub อยู่ครู่หนึ่ง สาม repo ปิด PR เวลา 16:20:31, 16:20:38 และ 16:20:45 — ห่างกัน สิบสี่วินาที
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 เรื่องปัญหาเล็ก ๆ ที่ไม่เกี่ยวข้องกันในการ 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 คำสั่งเดียว
วันนั้นเราคุยกันเรื่องอื่น ผมขอให้มันรีวิวเอกสาร 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 เป็นสาธารณะ
/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 สาธารณะที่ไหนเลยkamal deploy / app exec / app logs ทุกตัวทำงานต่อได้ตามปกติ เพราะ /etc/hosts ถูกอ่านก่อน DNSaccessory ของฐานข้อมูลก็ต้องเปลี่ยนเหมือนกัน:
accessories:
db:
image: postgres:17
host: deploy.how2claude # ← hostname เดียวกัน
นี่คือส่วนที่ผมอยากบันทึกไว้
หลังจากเรา ship fix ของ how2claude ผมกำลังจะเปลี่ยน context Claude หยุดผมไว้: "คุณก็ใช้ Kamal กับ smarts และ pickful ใช่ไหมครับ? ขอเช็คสองอันนั้นด้วย"
มันก็ไปเช็ค
deploy.smarts.md — ปัญหาเดียวกัน เรคคอร์ด A สาธารณะdeploy.pickful.ai (production), deploy.pickful.xyz (alpha), บวกกับ deploy-test1.pickful.ai ที่ตายไปนานแล้ว และ deploy.staging.yml ที่ยังอ้างถึงโดเมนที่ปลดระวางไปแล้ว blockgeek.compickful ยุ่งที่สุดในสาม 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:
config/deploy*.yml แล้ว grep หา host: กับ servers: ลิสต์ทุก hostname ที่ปรากฏdig +short กับแต่ละตัว ตัวไหนที่คืน IP ของ origin แทนที่จะเป็นค่าว่าง คือกำลังรั่วdeploy.<project>) แล้วใส่ IP ลงใน /etc/hostsecho "$IP deploy.<project>" | sudo tee -a /etc/hostsถ้าทีมคุณดูแลหลายโปรเจกต์ที่ใช้รูปแบบ infrastructure เดียวกัน เขียน grep แบบบรรทัดเดียวที่กวาด deploy*.yml ทั้งหมดในทุก repo จะน่าเชื่อถือกว่ามาเช็คทีละอัน
ไม่ใช่ Cloudflare ล้มเหลว ไม่ใช่ Kamal ออกแบบผิด — เครื่องมือทั้งสองทำสิ่งที่ควรทำได้อย่างถูกต้อง
บทเรียนคือ: ค่า default หลอกคุณอย่างเงียบ ๆ deploy.<your-domain>.com ฟังดูเหมือนชื่อภายใน แต่ DNS ไม่มีแนวคิดของคำว่า "ภายใน" — เรคคอร์ด A ก็คือเรคคอร์ด A และเมื่อมันถูกเผยแพร่แล้วก็เป็นสาธารณะ ชื่อสามารถสร้างภาพลวงให้คุณรู้สึกถึงความเป็นส่วนตัว และไอคอนก้อนเมฆเทาเล็ก ๆ ในแดชบอร์ด Cloudflare ก็คือเวอร์ชันภาพของภาพลวงตัวเดียวกัน: นั่งอยู่ตรงนั้นเงียบ ๆ ไม่มีคำเตือนสีแดง แต่ที่มันบอกคุณจริง ๆ คือ "เรคคอร์ดนี้ไม่ผ่านฉัน"
ลองให้ Claude อ่านคอนฟิก deploy ของคุณสักรอบ มันไม่ได้แชร์ภาพลวงนั้น