limit_req di Nginx untuk menahan bot yang membanjiri halaman login. Bot-nya memang berkurang, tapi sekarang pengunjung biasa ikut kena error 503 atau 429, padahal mereka cuma membuka beberapa halaman. Kalau domainnya lewat Cloudflare, penyebab paling umum biasanya satu: Nginx menghitung limit berdasarkan IP Cloudflare, bukan IP pengunjung yang sebenarnya.
Masalah ini sering membingungkan karena konfigurasi rate limit-nya sendiri terlihat benar. Yang keliru adalah identitas klien yang dibaca Nginx. Di artikel ini kita fokus ke pengecekan lewat CLI: memastikan kondisi sekarang, memperbaikinya, lalu membuktikan bahwa limit benar-benar menyasar IP klien.
Kenapa ribuan pengunjung terlihat seperti segelintir IP
limit_req_zone binary_remote_addr .... Variabel binary_remote_addr berisi alamat koneksi yang masuk. Tanpa konfigurasi tambahan, isinya adalah IP Cloudflare. Satu edge melayani banyak pengunjung sekaligus, jadi semua pengunjung itu masuk ke bucket yang sama. Bucket adalah "wadah hitungan" per kunci yang dipakai limit_req untuk menghitung jatah request. Begitu wadah itu penuh, semua pengunjung di belakang edge tersebut ikut ditolak, sementara bot yang kebetulan lewat edge lain bisa tetap lolos.
Secara default, request yang ditolak limit_req dibalas dengan status 503. Kalau kamu sudah memakai limit_req_status 429, statusnya jadi 429. Ini juga alasan kenapa di sisi support masalah ini sering dikira server overload, padahal CPU dan RAM santai saja.
Cloudflare sebenarnya meneruskan IP pengunjung asli lewat header CF-Connecting-IP. Nginx tidak otomatis memakainya. Perlu modul ngx_http_realip_module dan konfigurasi yang memberi tahu Nginx: kalau koneksi datang dari rentang IP Cloudflare, ambil IP klien dari header itu.
Cek kondisi sekarang sebelum mengubah apa pun
BASH
# 1. Apakah modul realip ikut di build Nginx?
nginx -V 2>&1 | grep -o 'with-http_realip_module'
# 2. Apakah sudah ada konfigurasi real_ip dan limit_req yang aktif?
sudo nginx -T 2>/dev/null | grep -nE 'set_real_ip_from|real_ip_header|real_ip_recursive|limit_req'
# 3. IP mana yang paling banyak tercatat di access log (format combined: kolom pertama)
sudo awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
# 4. Request yang ditolak limit_req dan IP yang dianggap sebagai klien
sudo grep 'limiting requests' /var/log/nginx/error.log | tail -20limit_req tanpa set_real_ip_from atau real_ip_header, berarti limit kamu memang sedang menghitung IP Cloudflare.
Perintah ketiga biasanya paling menjelaskan. Kalau daftar teratas berisi sedikit alamat yang semuanya milik Cloudflare, dengan jumlah request yang besar, polanya cocok dengan masalah ini. Hal yang sama terlihat di perintah keempat. Baris limiting requests punya bagian client:, dan kalau isinya IP Cloudflare, Nginx menolak IP edge, bukan IP pengunjung.
Untuk memastikan sebuah IP memang milik Cloudflare, bandingkan dengan daftar rentang IP resmi yang dipublikasikan Cloudflare. Jangan menebak dari pola angkanya.
Memberi tahu Nginx dari mana IP klien diambil
set_real_ip_from berisi rentang IP Cloudflare, ditambah real_ip_header CF-Connecting-IP. Directive ini hanya membuat Nginx percaya header tersebut kalau koneksinya berasal dari rentang yang terdaftar. Kedua, log format sementara yang menampilkan IP hasil realip berdampingan dengan IP koneksi aslinya, supaya hasilnya bisa diverifikasi.
Bagian kedua ditaruh di blok http. Contoh di bawah memakai nama file terpisah. Angka rate dan ukuran zone hanya contoh ilustrasi, jadi sesuaikan dengan konfigurasi limit yang sudah kamu punya.
NGINX
# Contoh: /etc/nginx/conf.d/zz-realip-debug.conf (berlaku di konteks http)
# $remote_addr = IP setelah diproses realip (seharusnya IP pengunjung)
# $realip_remote_addr = IP koneksi asli (seharusnya IP Cloudflare)
# $http_cf_connecting_ip = isi header dari Cloudflare, untuk pembanding
log_format realip_debug 'remote_addr viarealip_remote_addr cf=http_cf_connecting_ip [time_local] statusrequest_uri';
access_log /var/log/nginx/realip-debug.log realip_debug;
# Rate limit tetap memakai $binary_remote_addr.
# Setelah realip aktif, nilainya otomatis berisi IP klien, bukan IP Cloudflare.
# limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
# limit_req_status 429;limit_req_zone tidak perlu diganti ke variabel lain. Modul realip mengubah nilai remote_addr dan binary_remote_addr sebelum rate limit dihitung. Konfigurasi limit yang lama tetap bisa dipakai apa adanya.
Daftar set_real_ip_from sebaiknya dibuat dari daftar resmi, bukan diketik manual. Rentang IP Cloudflare bisa berubah, dan daftar yang tidak lengkap akan membuat sebagian edge tetap terhitung sebagai satu klien. Jangan lupa rentang IPv6. Edge Cloudflare juga bisa menyambung ke origin lewat IPv6 kalau server kamu punya alamat IPv6 yang aktif.
Pasang, uji sintaks, lalu buktikan hasilnya
curl gagal, file bisa kosong atau berisi halaman error, jadi baca dulu isinya sebelum reload. Reload Nginx tidak memutus koneksi yang sedang berjalan, tetapi tetap jalankan nginx -t dulu. Kalau di konfigurasi lain sudah ada real_ip_header, nginx -t akan menolak directive yang dobel. Hapus salah satunya sebelum lanjut.
BASH
# 1. Buat daftar set_real_ip_from dari daftar resmi Cloudflare
{ printf 'set_real_ip_from %s;
' (curl -fsS https://www.cloudflare.com/ips-v4)(curl -fsS https://www.cloudflare.com/ips-v6); echo 'real_ip_header CF-Connecting-IP;'; } | sudo tee /etc/nginx/conf.d/cloudflare-realip.conf
# 2. Baca isinya: harus berupa baris set_real_ip_from dengan CIDR, bukan HTML atau kosong
sudo cat /etc/nginx/conf.d/cloudflare-realip.conf
# 3. Uji sintaks, baru reload kalau lolos
sudo nginx -t && sudo systemctl reload nginx
# 4. Buka situs dari browser, lalu lihat log debug
sudo tail -f /var/log/nginx/realip-debug.log
# 5. Uji limit lewat domain, pakai path yang tidak di-cache Cloudflare
for i in $(seq 1 30); do curl -s -o /dev/null -w '%{http_code}
' 'https://domainkamu.com/wp-login.php'; done | sort | uniq -c
# 6. Pastikan baris penolakan sekarang menyebut IP klien, bukan IP Cloudflare
sudo grep 'limiting requests' /var/log/nginx/error.log | tail -5via, dan nilai cf= yang sama dengan kolom pertama. Kalau kolom pertama dan kolom via masih sama-sama IP Cloudflare, berarti salah satu dari tiga hal ini terjadi: file realip belum terbaca di konteks http, rentang IP-nya tidak lengkap, atau modulnya tidak ada.
Uji di langkah kelima sebaiknya memakai path yang memang dikenai limit_req dan tidak di-cache Cloudflare. Request yang dilayani dari cache tidak pernah sampai ke Nginx, jadi tidak akan pernah kena limit. Kalau hasilnya menunjukkan campuran 200 lalu 429 atau 503 setelah beberapa request, limit sedang bekerja. Lalu cek langkah keenam. Bagian client: sekarang seharusnya berisi IP koneksi internet kamu sendiri. Artinya pengunjung lain punya bucket sendiri dan tidak ikut kena.
Setelah yakin hasilnya benar, hapus atau nonaktifkan zz-realip-debug.conf supaya tidak ada log tambahan yang terus membesar. Jalankan lagi nginx -t dan reload.
Pastikan header tidak bisa dipalsukan
set_real_ip_from 0.0.0.0/0 supaya cepat beres. Jangan lakukan itu. Dengan pengaturan seperti itu, siapa pun bisa mengirim header CF-Connecting-IP berisi IP acak, langsung ke origin. Rate limit jadi gampang dihindari, karena tiap request bisa mengaku berasal dari IP berbeda. Log juga jadi tidak bisa dipercaya untuk investigasi.
Cara mengujinya sederhana. Dari mesin yang bukan bagian dari Cloudflare, misalnya VPS lain, kirim request langsung ke IP origin dengan header palsu, contohnya curl -s -o /dev/null -H 'Host: domainkamu.com' -H 'CF-Connecting-IP: 203.0.113.10' http://IP_ORIGIN/. Alamat 203.0.113.10 berasal dari rentang khusus dokumentasi. Lalu lihat log debug. Konfigurasi yang benar mencatat IP mesin penguji kamu, bukan 203.0.113.10, karena koneksinya tidak datang dari rentang yang dipercaya.
Uji ini juga sering membuka masalah lain: origin ternyata masih bisa diakses langsung tanpa lewat Cloudflare. Itu tidak merusak realip, tetapi artinya bot bisa melewati semua proteksi di sisi Cloudflare. Membatasi port 80/443 hanya untuk rentang Cloudflare di firewall adalah langkah terpisah yang layak dipertimbangkan.
Satu catatan lagi untuk setup berlapis. Kalau di antara Cloudflare dan Nginx masih ada load balancer atau reverse proxy lain, alamat lapisan itu juga perlu masuk set_real_ip_from, dan header yang dipakai harus sampai utuh ke Nginx. Pengaturannya tergantung setiap lapisan, jadi verifikasinya tetap sama: lihat kolom remote_addr dan realip_remote_addr di log.
Pegangan praktisnya begini. Setiap kali limit_req dipasang di belakang proxy, jangan percaya angka limitnya sebelum kamu melihat sendiri IP apa yang dihitung. Jalankan nginx -T untuk memastikan directive realip aktif, bandingkan remote_addr dengan realip_remote_addr di log, uji penolakan dari path yang tidak di-cache, dan pastikan header palsu dari luar Cloudflare diabaikan. Perbarui daftar rentang IP Cloudflare secara berkala, karena daftar yang usang akan membawa masalah yang sama kembali sedikit demi sedikit.