403 Kosong yang Bukan Salah Nginx
Ada satu layanan AI inference yang saya jalankan langsung di mesin lokal saya, lalu saya ekspos ke publik lewat tunnel supaya bisa diakses dari sebuah subdomain di pujamalca.my.id. Nginx di VPS bertugas jadi reverse proxy, meneruskan trafik dari domain publik itu ke port tunnel di belakangnya. Biasanya setup semacam ini beres dalam lima menit — sampai gejala aneh ini muncul.
Gejala: 403 dengan body kosong total
Setiap kali saya akses lewat domain publik, responsnya selalu 403 Forbidden — tapi bukan 403 biasa yang ada halaman error atau pesannya. Ini benar-benar kosong. Tidak ada body, tidak ada keterangan apa pun. Yang bikin makin bingung, tunnel-nya sendiri sehat: koneksinya stabil, tidak ada tanda putus atau timeout di layer manapun yang bisa saya lihat.
Curiga pertama pasti jatuh ke nginx atau ke sertifikat TLS, karena itu lapisan paling terlihat dari luar. Tapi sebelum mengutak-atik konfigurasi secara acak, saya coba mempersempit dulu di lapisan mana sebenarnya masalah ini terjadi.
Mempersempit masalah: curl langsung ke backend
Triknya sederhana: saya curl langsung ke port tunnel lokal di VPS, tanpa lewat domain publik dan tanpa TLS sama sekali. Supaya perbandingannya adil, saya sertakan header Host yang persis sama dengan yang dikirim browser saat mengakses lewat domain publik.
curl -H "Host: subdomain-publik-saya" http://localhost:PORT_TUNNEL/
Hasilnya: 403 kosong yang identik. Ini titik penting. Kalau nginx atau TLS yang jadi biang keladi, curl langsung ke backend tanpa lewat keduanya seharusnya sukses. Karena hasilnya tetap sama persis, artinya masalahnya bukan di nginx, bukan di sertifikat — tapi di layanan backend itu sendiri, dan lebih spesifik lagi: dipicu oleh header Host yang saya kirim.
Root cause: proteksi anti-DNS rebinding
Setelah ditelusuri, ternyata layanan AI inference yang saya pakai memang sengaja menolak request apa pun yang header Host-nya bukan localhost. Ini bukan bug — ini fitur keamanan resmi untuk mencegah serangan DNS rebinding, proteksi bawaan yang memang lazim ada di tool-tool AI atau dev server yang awalnya dirancang untuk dipakai secara lokal saja.
Di sisi lain, perilaku default nginx sebagai reverse proxy adalah meneruskan header Host asli dari client apa adanya ke backend. Jadi begitu browser mengirim request ke domain publik saya, nginx meneruskan header Host: subdomain-publik-saya itu ke backend — dan backend langsung menolaknya karena dianggap request yang berpotensi DNS rebinding.
Dua perilaku yang masing-masing benar dan wajar sesuai desainnya, tapi begitu bertemu, hasilnya jadi error yang membingungkan kalau tidak tahu ceritanya.
Solusi: override Host header ke backend
Fix-nya cuma satu baris tambahan di konfigurasi proxy_pass nginx: override header Host yang dikirim ke backend supaya selalu localhost, tanpa mengubah apa pun yang dilihat browser atau client di luar.
location / {
proxy_pass http://localhost:PORT_TUNNEL;
proxy_set_header Host localhost;
}
Reload nginx, coba lagi lewat domain publik — langsung normal. Client tetap mengakses lewat domain publik seperti biasa, tidak ada perubahan apa pun yang terlihat dari luar.
Pelajaran
Kalau ketemu 403 dengan body benar-benar kosong, dan sudah dipastikan bukan nginx maupun TLS yang jadi biang keladi, curigai dulu validasi header Host di level aplikasi backend. Ini khususnya relevan untuk tool-tool AI atau dev server, yang memang sering sengaja dirancang menolak akses selain dari localhost sebagai proteksi keamanan bawaan.
Cara mempersempitnya juga layak diingat: curl langsung ke backend sambil menyamakan header yang relevan — di sini Host — adalah cara cepat memisahkan masalah "di jalur proxy" dari masalah "di aplikasi itu sendiri", tanpa perlu utak-atik konfigurasi nginx lebih dulu.