IP origin di balik Cloudflare bocor lewat subdomain deploy — tiga repo, empat belas detik, beres sekaligus
Sore itu saya menatap GitHub sebentar. Tiga repo menutup PR pada 16:20:31, 16:20:38, dan 16:20:45 — selisih empat belas detik.
smarts #38: deploy.smarts.md → deploy.smartshow2claude #13: deploy.how2claude.com → deploy.how2claudepickful #118: deploy.pickful.ai / deploy.pickful.xyz → deploy.pickful / deploy.pickful-alphaKetiganya memperbaiki bug kelas yang sama. Awalnya saya hanya ngobrol dengan Claude di repo how2claude tentang masalah deploy kecil yang tidak terkait. Sambil lalu, ia menengok config/deploy.yml dan memberi tahu saya bahwa IP origin dari mesin itu terbuka lebar di DNS publik.
Semua proyek saya berada di belakang Cloudflare. Ikon awan oranye kecil di dashboard Cloudflare berarti "record di-proxy" — request HTTP mendarat di edge node Cloudflare dulu, lalu ke origin saya. IP origin tidak pernah muncul di respon DNS; yang dikembalikan DNS adalah IP anycast Cloudflare. Bagian itu sudah benar.
Tapi saya deploy dengan Kamal. Kamal melakukan SSH ke server untuk menjalankan docker. SSH tidak bisa lewat HTTP proxy, jadi saya butuh hostname yang tidak di-proxy Cloudflare untuk SSH. Setup saya saat itu:
# config/deploy.yml
servers:
web:
- deploy.how2claude.com # ← record A langsung ke IP origin, awan abu (DNS only)
Di Cloudflare, how2claude.com dan www.how2claude.com warnanya oranye. deploy.how2claude.com warnanya abu. Harus abu, kalau tidak SSH tidak bisa sampai ke mesin.
Abu artinya DNS publik. Siapa pun bisa menjalankan:
$ dig deploy.how2claude.com +short
<IP origin saya>
Dan konvensi penamaan deploy.<domain> itu sendiri sudah jadi penanda yang kentara — scan subdomain deploy.* di daftar domain SaaS umum, dan kamu dapat panen IP origin yang seharusnya bersembunyi di balik Cloudflare.
Lewati WAF Cloudflare, lewati rate limiting, lewati proteksi DDoS. Sejauh satu dig.
Topik kami sebenarnya hal lain. Saya minta dia mereview dokumen deploy yang baru saya tulis; ia membuka config/deploy.yml untuk cross-check, sampai ke baris 7 — - deploy.how2claude.com — berhenti sebentar, lalu mengatakan kira-kira begini:
Hostname itu diresolve lewat DNS publik kan? Berarti siapa pun yang melakukan query DNS bisa dapat IP origin itu, dan edge proxy Cloudflare jadi terlewat.
Saya terdiam sebentar. Ini seharusnya sudah saya lihat — saya punya cukup alasan untuk perhatikan ikon awan abu saat mengonfigurasi Cloudflare dan menanyakan apa artinya — tapi saya tidak. Saya memperlakukan hostname itu seperti sesuatu yang "internal", semata-mata karena prefiksnya deploy.. Otak saya diam-diam memberinya privasi yang sebenarnya tidak pernah ada.
Claude tidak punya bias itu. Ia membaca string di field yaml dan bertanya secara mekanis: bagaimana string ini berubah menjadi IP? Jawaban: DNS publik. Kesimpulan: IP origin bersifat publik.
/etc/hostsPerbaikannya ternyata memalukan saking sederhananya. Kamal meminta SSH client lokal untuk meresolve hostname, jadi hostname itu hanya perlu resolve di mesin saya — tidak perlu resolve di seluruh internet.
Pendekkan hostname di yaml:
servers:
web:
- deploy.how2claude # ← perhatikan: tidak ada .com
Lalu tambahkan satu baris ke /etc/hosts saya:
198.51.100.42 deploy.how2claude
Runner CI yang melakukan deploy butuh baris yang sama (env var, atau langsung echo >> /etc/hosts di workflow).
Hasil:
deploy.how2claude.* di DNS publik di mana punkamal deploy / app exec / app logs tetap jalan, karena /etc/hosts dicek sebelum DNSAccessory database juga butuh perubahan yang sama:
accessories:
db:
image: postgres:17
host: deploy.how2claude # ← hostname yang sama
Ini bagian yang ingin saya tulis.
Setelah perbaikan how2claude di-ship, saya akan beralih ke konteks lain. Claude menahan saya: "Kamu juga pakai Kamal di smarts dan pickful, kan? Coba saya cek dua itu."
Dia cek.
deploy.smarts.md — masalah yang sama, record A publikdeploy.pickful.ai (production), deploy.pickful.xyz (alpha), plus deploy-test1.pickful.ai yang sudah lama mati dan satu deploy.staging.yml yang merujuk ke domain pensiun, blockgeek.comPickful paling berantakan dari ketiganya karena punya beberapa destination (production / alpha / test2) dan beban historis. Claude sekalian menyapu destination staging dan test1 yang mati — sudah tidak ada yang pakai, hanya jadi noise.
Commit akhir:
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)
Empat belas detik. Tentu itu cuma GitHub mengosongkan antrean merge — kerja sebenarnya tersebar di dua jam sebelumnya. Tapi bentuk efektifnya: satu temuan, tiga repo terselesaikan.
Kalau saya kerjakan sendiri, versi realistisnya: perbaiki di how2claude, tulis TODO bertuliskan "lakukan smarts dan pickful juga", dan saksikan TODO itu tergeletak di list selama tiga bulan. Saya sudah mengalami urutan persis itu berkali-kali.
Kalau kamu deploy dengan Kamal, Capistrano, atau alat deploy berbasis SSH apa pun:
config/deploy*.yml dan grep host: dan servers:. Daftarkan setiap hostname yang muncul.dig +short di tiap-tiap hostname. Yang mengembalikan IP origin alih-alih kosong, itu bocor.deploy.<project>), masukkan IP-nya ke /etc/hosts.echo "$IP deploy.<project>" | sudo tee -a /etc/hosts.Kalau tim kamu punya beberapa proyek yang berbagi pola infra yang sama, tulis grep one-liner yang menyapu semua deploy*.yml di semua repo, lebih bisa diandalkan daripada cek satu per satu.
Cloudflare tidak gagal. Kamal tidak gagal. Kedua tool melakukan persis yang seharusnya mereka lakukan.
Pelajarannya: default value diam-diam menyesatkan kamu. deploy.<domainmu>.com terdengar seperti nama internal, tapi DNS tidak punya konsep "internal" — record A adalah record A, dan begitu dipublikasikan, ia publik. Sebuah nama bisa memberi ilusi privasi, dan ikon awan abu kecil di dashboard Cloudflare itu hanyalah versi visual dari ilusi yang sama: duduk di sana diam-diam, tanpa peringatan merah, tapi yang sebenarnya ia katakan adalah "record ini tidak lewat saya".
Coba minta Claude membaca config deploy kamu sekali. Dia tidak punya ilusi itu.