02:47 WIB: WhatsApp klien e-commerce fashion muslim Surabaya:
“Bro, site down. Error 502 terus dari tadi. Tolong cek.”
02:48 WIB: Notifikasi Uptime Kuma masuk via Telegram dan email. Status: DOWN sejak 02:43 WIB.
02:49 WIB: SSH masuk ke VPS Vultr Tokyo. top langsung tunjukkan angka yang tidak masuk akal: load average 89.21, 45.07, 22.43, di VPS dengan 2 vCPU. RAM 1,7 GB dari 2 GB. Swap aktif 412 MB.
02:51 WIB: tail -f /var/log/nginx/access.log | grep wp-login. Output scroll terlalu cepat untuk dibaca. Ratusan baris per detik. POST ke /wp-login.php dari ribuan IP berbeda, user-agent acak (Chrome 89, Firefox 102, Safari iPad, Edge mobile, semua kelihatan normal kalau lihat satu-satu).
02:54 WIB: Diagnose selesai: ini bukan brute force klasik dari satu IP. Ini HTTP flood layer 7 dari botnet distributed. ±200 request per detik ke /wp-login.php dan /xmlrpc.php, dari ribuan IP unik. Bandwidth network masih normal, origin VPS tidak kena flood paket, dia kena flood request PHP.
02:55 WIB: Mulai mitigasi 3-layer.
04:13 WIB: Site stabil. Request ke origin turun dari 1.247.000/jam ke 6.200/jam (baseline normal). CPU origin dari 92% sustained ke 12%. TTFB dari 14.300 ms (timeout) ke 198 ms. Total waktu mitigasi: 86 menit. Total downtime: 47 menit (sebelum Cloudflare proxy + WAF aktif penuh).
Botnet masih nyerang 3 hari berikutnya. Semua ke-block di edge Cloudflare. Klien tidak rugi karena traffic real masih masuk lewat JS Challenge.
Artikel ini adalah playbook yang saya pakai di malam itu dan sudah saya replikasi ke 12 klien WordPress sebagai SOP standar selama 14 bulan terakhir. Cumulative IP banned di semua klien: 380.000+. Nol downtime karena DDoS L7 setelah setup ini aktif.
1. Anatomi Serangan L7: Beda dengan Volumetric DDoS
DDoS punya dua keluarga besar dengan strategi pertahanan yang berbeda:
Layer 3/4 (volumetric). Flood paket TCP/UDP/ICMP dengan tujuan habisin bandwidth network atau buffer kernel. Ukuran biasanya gigabit per detik. Origin VPS tidak akan pernah bisa lawan ini sendiri, bandwidth uplink kena banjir sebelum paket masuk firewall. Solusinya: Cloudflare Magic Transit, AWS Shield Advanced, atau ISP-level scrubbing. Cloudflare Free tidak cukup untuk L3/4 volumetric >10 Gbps.
Layer 7 (application). Request HTTP yang valid secara protokol, tapi targetkan endpoint mahal di sisi aplikasi: /wp-login.php, /xmlrpc.php, search query, comment spam. Bandwidth network masih normal (kadang <50 Mbps), tapi origin CPU/RAM/PHP-FPM kena flood. Di kasus klien saya: 1,2 juta request/jam, tapi bandwidth network cuma 38 Mbps. Yang habis bukan pipa, yang habis adalah PHP worker.
Untuk L7, Cloudflare Free CUKUP kalau Anda mau setup 3-layer dengan benar. Cloudflare blok 85% di edge, Nginx rate limit redam 13% sisanya yang lolos, fail2ban ban IP yang dapat lolos juga supaya request ketiga dari IP yang sama langsung connect-refused di firewall level.
Diagram Defense-in-Depth

Filosofi-nya: bukan satu lapis paling tebal, tapi tiga lapis berurutan dengan tugas yang spesifik. Kalau lapis pertama bocor, lapis kedua menahan. Kalau dua bocor, lapis ketiga ban IP itu permanen.
Untuk konteks setup VPS awal yang saya pakai di klien ini, baca dulu Setup VPS Ubuntu 24.04 dari Nol untuk WordPress: Panduan 2-Jam.
2. Pre-requisites (10 Menit Sebelum Mulai)
Sebelum sentuh apa-apa, pastikan 6 hal ini ready:
- Domain sudah Cloudflare-managed. Nameserver registrar sudah diganti ke
*.ns.cloudflare.com. Kalau belum, tambahkan domain di Cloudflare dashboard dulu, ganti NS, tunggu 5–60 menit propagasi. - SSH access ke origin VPS (root atau user dengan sudo).
- Nginx running + akses edit
/etc/nginx/nginx.conf+ server block di/etc/nginx/sites-available/. - fail2ban terinstall. Cek dengan
systemctl status fail2ban. Kalau belum:
sudo apt install -y fail2ban
sudo systemctl enable --now fail2ban
- Origin IP harus dirahasiakan setelah Cloudflare proxy aktif. Cek jangan ada bocor di MX record (pakai mail relay terpisah), error email header WordPress, atau config GitHub publik.
- Backup config dulu sebelum modifikasi apa-apa:
sudo cp -a /etc/nginx /etc/nginx.bak-$(date +%Y%m%d)
sudo cp -a /etc/fail2ban /etc/fail2ban.bak-$(date +%Y%m%d)
Catatan ⚠️ #1 Origin IP yang Sudah Bocor Tidak Bisa “Ditutup” Cloudflare
Kalau origin IP Anda sudah pernah expose ke publik (Wayback Machine, GitHub gist, error email header WordPress yang reply dari origin SMTP), Cloudflare proxy tidak akan menutupinya. Attacker bisa direct-hit ke IP itu bypass Cloudflare. Dua solusi:
- UFW deny default + allow hanya range IP Cloudflare (saya pakai cara ini, lihat di akhir Fase 2).
- Rotate IP: deploy VPS baru, migrasi data, update Cloudflare DNS, lalu destroy VPS lama. Lebih ribet tapi paling bersih.
Cek apakah IP Anda terindeks: search di Shodan + Censys + Wayback Machine dengan query https://example.com.
3. Fase 1: Diagnose Serangan dari Log (5 menit)
Sebelum mitigasi, selalu konfirmasi dulu ini serangan apa. Bisa jadi salah konfigurasi, bisa jadi traffic spike beneran, bisa jadi attack. Beda diagnosis → beda mitigasi.
3.1. Hitung Request Rate per Endpoint
tail -n 50000 /var/log/nginx/access.log \
| awk '{print $7}' | sort | uniq -c | sort -rn | head -20
Output saya saat insiden:
38612 /wp-login.php
7421 /xmlrpc.php
1983 /wp-cron.php
924 /wp-admin/admin-ajax.php
612 /
198 /shop/
94 /favicon.ico
78% request ke /wp-login.php dalam 50.000 baris terakhir. Itu bukan pola visitor normal, visitor normal didominasi homepage (/), kategori, produk.
3.2. Hitung Unique IP
tail -n 50000 /var/log/nginx/access.log \
| awk '{print $1}' | sort -u | wc -l
Output: 4823. Empat ribu IP unik dalam 50.000 baris, rasio 10:1. Ini pola distributed botnet, bukan brute force tunggal.
3.3. Cek Geografi (kalau Anda sudah punya GeoIP di Nginx log)
tail -n 50000 /var/log/nginx/access.log \
| awk '{print $1}' | sort -u \
| xargs -I {} geoiplookup {} 2>/dev/null \
| awk -F': ' '{print $2}' | sort | uniq -c | sort -rn | head -10
Output saya:
1978 CN, China
1060 RU, Russian Federation
868 BR, Brazil
530 VN, Vietnam
192 IN, India
...
Takeaway Fase 1: dalam 5 menit kita punya jawaban, distributed L7 attack ke endpoint login. Mitigasi lanjut.
4. Fase 2: Cloudflare Proxy + SSL Full Strict (8 menit)
Langkah paling impactful di seluruh playbook. Begitu proxy aktif, 60–80% serangan langsung tertahan sebelum Anda sentuh Nginx.
4.1. Toggle Proxy ke Orange
Login Cloudflare dashboard → pilih domain → tab DNS → Records. Untuk A record @ (root) dan www:
- Awan abu-abu = DNS Only (Cloudflare cuma resolve DNS, traffic langsung ke origin)
- Awan jingga = Proxied (traffic lewat edge Cloudflare dulu, baru ke origin)
Klik awan abu-abu di setiap record sampai jadi jingga. Simpan.

4.2. SSL/TLS Mode = Full (Strict)
Tab SSL/TLS → Overview. Pilih Full (strict).
| Mode | Aman? | Keterangan |
|---|---|---|
| Off | Tidak | HTTP saja, jangan dipakai |
| Flexible | TIDAK aman | Edge ke origin lewat HTTP, MITM risk |
| Full | Sebagian | HTTPS edge-to-origin tapi cert tidak diverifikasi |
| Full (Strict) | Aman | HTTPS edge-to-origin + verify cert (Let’s Encrypt origin) |
Kalau origin Anda sudah pakai Let’s Encrypt (lihat Fase 5 di artikel setup VPS), pilih Full (Strict).
4.3. UFW Allow Cloudflare IP Only
Inilah yang menutup bypass langsung ke origin IP. Download list IP Cloudflare:
curl -s https://www.cloudflare.com/ips-v4 -o /tmp/cf-v4.txt
curl -s https://www.cloudflare.com/ips-v6 -o /tmp/cf-v6.txt
Apply:
sudo ufw delete allow 80/tcp 2>/dev/null
sudo ufw delete allow 443/tcp 2>/dev/null
while read ip; do sudo ufw allow from "$ip" to any port 80,443 proto tcp; done < /tmp/cf-v4.txt
while read ip; do sudo ufw allow from "$ip" to any port 80,443 proto tcp; done < /tmp/cf-v6.txt
sudo ufw reload
sudo ufw status numbered | head -20
Output (dipotong):
Status: active
To Action From
-- ------ ----
[ 1] 80,443/tcp ALLOW IN 173.245.48.0/20
[ 2] 80,443/tcp ALLOW IN 103.21.244.0/22
[ 3] 80,443/tcp ALLOW IN 103.22.200.0/22
[ 4] 80,443/tcp ALLOW IN 103.31.4.0/22
...
Takeaway Fase 2: request yang lewat IP non-Cloudflare ditolak di firewall level, tidak pernah sampai Nginx.
5. Fase 3: Cloudflare WAF: 5 Custom Rules Free Tier (12 menit)
Cloudflare Free batas 5 custom rules. Saya pakai 5 rule ini di semua klien, urut prioritas.
Buka Security → WAF → Custom Rules → Create rule.
Rule 1: Block Tor Exit Node
Expression:
(ip.src.country eq "T1")
Action: Block. (T1 = pseudo-country Cloudflare untuk Tor exit nodes.)
Rule 2: Block Known Bad Bots
Expression:
(cf.client.bot)
Action: Managed Challenge. Variable cf.client.bot true kalau Cloudflare detect bot dari signature mereka.
Rule 3: Protect wp-login + xmlrpc
Expression:
(http.request.uri.path contains "/wp-login.php") or
(http.request.uri.path contains "/xmlrpc.php")
Action: Managed Challenge. Ini rule paling efektif di insiden klien saya, 78% serangan ke /wp-login langsung ke-challenge.
Rule 4: Block Empty User-Agent
Expression:
(http.user_agent eq "")
Action: Block. Browser sah selalu kirim User-Agent. Empty UA = bot sloppy.
Rule 5: Challenge Akses /wp-admin dari Luar Indonesia
Expression:
(ip.src.country ne "ID") and (http.request.uri.path contains "/wp-admin")
Action: Managed Challenge. Sesuaikan kode negara dengan audience Anda. Kalau klien punya tim remote ke luar negeri, ganti dengan ASN whitelist atau tambah country code lain.

Catatan ⚠️ #2 Free Tier Max 5 Rules
Kalau Anda butuh rule keenam (misalnya whitelist IP statis kantor), Anda harus upgrade ke Cloudflare Pro ($25/bulan) atau gabungkan logika di rule existing. Saya bertahan di Free dengan 5 rule ini selama 14 bulan untuk klien e-commerce.
Takeaway Fase 3: sebagian besar serangan ke /wp-login.php dan /xmlrpc.php sekarang dapat JS Challenge sebelum sampai origin. Bot tidak bisa solve Challenge, mereka berhenti di edge.
6. Fase 4: Bot Fight Mode + Under Attack Mode + Page Rules (6 menit)
6.1. Bot Fight Mode
Security → Bots → Bot Fight Mode → toggle ON. Free tier kasih proteksi dasar terhadap bot yang sudah dikenal Cloudflare. Untuk deteksi advanced (residential botnet) butuh Bot Management (paid, $200+/bulan).
6.2. Under Attack Mode (Emergency Only)
Security → Settings → Security Level → pilih I’m Under Attack!.
Mode ini bikin setiap visitor dapat JS Challenge 5 detik sebelum lihat homepage. Efek-nya brutal untuk attack, botnet 99% tidak bisa solve JS. Tapi UX impact untuk visitor real juga signifikan: bounce rate naik, conversion bisa drop 30–50%.
Aturan saya: Under Attack Mode ON cuma saat serangan aktif. Setelah Nginx rate limit + WAF rules + fail2ban aktif (Fase 5–7), turunkan ke High atau Medium. Kalau tetap stabil, sudah cukup tanpa Under Attack.
Pro tip 💡 #1 Page Rule untuk admin-ajax.php
Saat Under Attack Mode aktif, request AJAX di WP admin (mis. Gutenberg auto-save, plugin async loader) akan gagal karena dianggap bot. Bypass dengan Page Rule:
Rules → Page Rules → Create Page Rule:
- URL:
*NAMADOMAIN.com/wp-admin/admin-ajax.php* - Setting: Security Level → Essentially Off
Tambah satu lagi untuk admin path agar admin Anda tetap responsif:
- URL:
*NAMADOMAIN.com/wp-admin/* - Setting: Security Level → Medium, Cache Level → Bypass
Free tier max 3 Page Rules, saya pakai 2 untuk admin, 1 untuk login. Cukup.

Takeaway Fase 4: Under Attack hidup sementara saat panik, lalu dimatikan setelah 3 layer lain aktif.
7. Fase 5: Nginx Rate Limit Zones (15 menit)
Layer kedua. Cloudflare bocor 13–15% request, Nginx tahan sisanya pakai limit_req_zone.
7.1. Definisi Zone di nginx.conf
Edit http {}:
# Zone 1: general traffic per IP
limit_req_zone $binary_remote_addr zone=general:10m rate=30r/s;
# Zone 2: wp-login (sangat ketat)
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
# Zone 3: wp-admin (sedang)
limit_req_zone $binary_remote_addr zone=admin:10m rate=20r/m;
# Status code untuk rate limit kena
limit_req_status 429;
Penjelasan rate:
30r/s= 30 request per detik per IP. Lebih dari cukup untuk browsing normal (visitor biasa <5 r/s).5r/m= 5 request per menit per IP ke wp-login. Brute force minimal 60 req/menit, dia akan kena 429 di request ke-6.20r/m= 20 r/menit untuk wp-admin. Editor WP normal <10 r/menit.
7.2. Apply di Server Block
Edit /etc/nginx/sites-available/NAMADOMAIN.com:
server {
listen 443 ssl http2;
server_name NAMADOMAIN.com;
root /var/www/NAMADOMAIN.com;
index index.php;
# ... ssl + log config ...
location = /wp-login.php {
limit_req zone=login burst=3 nodelay;
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_index index.php;
}
location = /xmlrpc.php {
limit_req zone=login burst=2 nodelay;
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
location ~ ^/wp-admin {
limit_req zone=admin burst=10 nodelay;
try_files $uri $uri/ /index.php?$args;
}
location / {
limit_req zone=general burst=20 nodelay;
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
}
Test + reload:
sudo nginx -t && sudo systemctl reload nginx
7.3. Verifikasi Manual
Dari laptop, hammer wp-login 6× cepat:
for i in {1..6}; do curl -o /dev/null -s -w "%{http_code}\n" \
https://NAMADOMAIN.com/wp-login.php; done
Output yang diharapkan:
200
200
200
200
429
429
Request ke-5 dan -6 kena 429 (Too Many Requests).
Catatan ⚠️ Behind Shared NAT Bisa False Positive
User di kantor / sekolah / Starlink sering keluar dari satu IP publik (NAT). Rate limit per IP bisa block legitimate user. Mitigasi:
- Kombinasi dengan Cloudflare WAF (sudah kita lakukan di Fase 3), user yang lolos JS Challenge biasanya legitimate.
- Naikkan
burst(mis. dariburst=3keburst=10) untuk lebih toleran. - Whitelist IP statis klien di Nginx pakai
geomodule.
Takeaway Fase 5: burst dipertinggi sesuai pengalaman klien Anda. Saya mulai konservatif (burst=3), naikan kalau ada laporan false positive dari admin klien.
8. Fase 6: Connection Limit + Block Bad User-Agent (8 menit)
Rate limit kontrol per request. Connection limit kontrol jumlah TCP connection simultan per IP. Beda problem, beda mitigasi.
8.1. Connection Limit
Tambah di blok http {} /etc/nginx/nginx.conf:
limit_conn_zone $binary_remote_addr zone=addr:10m;
Apply di server block (dalam server {}):
limit_conn addr 20;
Maksimal 20 koneksi simultan per IP. Browser modern biasanya max 6 koneksi paralel per host, angka 20 toleran untuk asset (CSS, JS, font, gambar) yang load paralel.
8.2. Map Block Bad User-Agent
Bot scraper dan automated tool punya User-Agent khas yang gampang dikenali. Block sebelum sampai PHP.
Tambah di blok http {}:
map $http_user_agent $bad_bot {
default 0;
~*(scrapy|wget|libwww-perl|python-requests) 1;
~*(nikto|sqlmap|nmap|masscan|zgrab) 1;
~*(MJ12bot|AhrefsBot|SemrushBot|DotBot) 1;
"" 1;
}
Apply di server block:
if ($bad_bot) {
return 444;
}
Status 444 = close connection tanpa response. Tidak konsumsi bandwidth untuk error page. Saya sengaja tidak block curl di list, saya butuh curl untuk debug origin. Kalau Anda paranoid, tambahkan tapi whitelist User-Agent debug Anda sendiri:
map $http_user_agent $bad_bot {
default 0;
~*curl 1;
~*"LarhTechAdmin/1\.0" 0; # whitelist
...
}
Test + reload:
sudo nginx -t && sudo systemctl reload nginx
curl -A "sqlmap" -I https://NAMADOMAIN.com/
# curl: (52) Empty reply from server ← 444 close
Takeaway Fase 6: scraper kelas SEO (Ahrefs, Semrush) sengaja saya block, mereka konsumsi bandwidth + skew analytics, dan klien e-commerce saya tidak butuh dilihat sama mereka.
9. Fase 7: fail2ban: 4 Jail untuk Nginx (10 menit)
Lapis ketiga. fail2ban baca log Nginx, deteksi pola attack, ban IP di iptables level. Begitu IP banned, koneksi dari IP itu drop di kernel, tidak pernah sentuh Nginx lagi selama bantime.
9.1. Jail Definitions
Edit /etc/fail2ban/jail.local:
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
ignoreip = 127.0.0.1/8 ::1 203.0.113.10
banaction = iptables-multiport
[nginx-http-auth]
enabled = true
filter = nginx-http-auth
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 3
bantime = 3600
[nginx-botsearch]
enabled = true
filter = nginx-botsearch
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 2
bantime = 86400
[nginx-limit-req]
enabled = true
filter = nginx-limit-req
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 10
findtime = 60
bantime = 7200
[wp-login-attack]
enabled = true
filter = wp-login-attack
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 3
findtime = 300
bantime = 86400
Ganti 203.0.113.10 di ignoreip dengan IP statis kantor/rumah Anda.
9.2. Custom Filter untuk wp-login
/etc/fail2ban/filter.d/wp-login-attack.conf:
[Definition]
failregex = ^<HOST>.*"POST .*/wp-login\.php.*" (200|302|401|403)
ignoreregex =
Penjelasan regex:
^<HOST>: fail2ban substitution untuk IP source, harus di awal baris log..*"POST .*/wp-login\.php.*": method POST ke URI yang mengandung/wp-login.php.(200|302|401|403): status code yang menandakan attempt login (200 form submit, 401/403 gagal auth, 302 redirect setelah submit).
maxretry=3 + findtime=300 artinya: 3 POST ke wp-login dalam 5 menit → ban 24 jam.
Pro tip 💡 #2 Whitelist Wajib
ignoreip HARUS include:
- IP statis kantor Anda
- AS15169 (Googlebot), kalau tidak, Google bisa kena ban dan SEO drop
- IP server Uptime Kuma / UptimeRobot Anda, kalau tidak, monitoring service akan jadi “attacker”
Saya pakai cara ini untuk allow Googlebot via ASN (butuh action lebih advanced di fail2ban, alternatif: gunakan IP range Googlebot yang Google publish di https://developers.google.com/search/apis/ipranges/googlebot.json):
sudo curl -s https://developers.google.com/search/apis/ipranges/googlebot.json \
| jq -r '.prefixes[].ipv4Prefix' > /etc/fail2ban/googlebot-ips.txt
Lalu tambah ke ignoreip (perlu script untuk update berkala, saya cron weekly).
9.3. Restart + Status
sudo systemctl restart fail2ban
sudo fail2ban-client status
Output (saat insiden klien, 12 jam setelah aktif):
Status
|- Number of jail: 4
`- Jail list: nginx-http-auth, nginx-botsearch, nginx-limit-req, wp-login-attack
sudo fail2ban-client status wp-login-attack
Status for the jail: wp-login-attack
|- Filter
| |- Currently failed: 14
| |- Total failed: 2841
| `- File list: /var/log/nginx/access.log
`- Actions
|- Currently banned: 1247
|- Total banned: 8923
`- Banned IP list: 203.0.113.42 203.0.113.78 198.51.100.12 ...

Unban manual kalau perlu:
sudo fail2ban-client set wp-login-attack unbanip 203.0.113.42
Takeaway Fase 7: IP yang lolos Cloudflare + Nginx rate limit dan masih nyerang origin → ban 24 jam di iptables. Tiga kali kena → 24 jam offline buat botnet.
10. Fase 8: Monitoring Real-Time + Telegram Alert (12 menit)
Defense tanpa monitoring = tunggu klien WA jam 2 pagi. Hindari itu.
10.1. Script Telegram Alert
/usr/local/bin/larhtech-attack-monitor.sh:
#!/bin/bash
TG_TOKEN="YOUR_TELEGRAM_BOT_TOKEN"
TG_CHAT="YOUR_TELEGRAM_CHAT_ID"
LOG="/var/log/nginx/access.log"
THRESHOLD=500 # request/menit ke endpoint sensitif
RATE=$(tail -n 10000 "$LOG" \
| awk -v d="$(date -d '1 minute ago' '+%d/%b/%Y:%H:%M')" \
'$0 ~ d' \
| grep -cE 'wp-login|xmlrpc')
if [ "$RATE" -gt "$THRESHOLD" ]; then
BANNED=$(fail2ban-client status wp-login-attack 2>/dev/null \
| grep "Currently banned" | awk -F: '{print $2}' | tr -d ' \t')
MSG="⚠️ Attack alert NAMADOMAIN.com
Rate: ${RATE} req/menit ke wp-login/xmlrpc
fail2ban banned: ${BANNED}
$(date '+%F %T %Z')"
curl -s -X POST "https://api.telegram.org/bot${TG_TOKEN}/sendMessage" \
-d "chat_id=${TG_CHAT}" \
-d "text=${MSG}" > /dev/null
fi
sudo chmod +x /usr/local/bin/larhtech-attack-monitor.sh
10.2. Cron Setiap Menit
sudo crontab -e
Tambahkan:
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
* * * * * /usr/local/bin/larhtech-attack-monitor.sh

Pro tip 💡 #3 Bookmark Cloudflare Security Analytics
Selama serangan aktif, buka Security → Analytics di Cloudflare dashboard, set time range “Last 1 hour”, refresh tiap 5 menit. Anda lihat:
- Total request blocked per WAF rule
- Distribusi country attacker
- Top User-Agent + ASN
- Pattern berubah real-time
Ini lebih detail dari log Nginx (karena Cloudflare lihat traffic yang tidak pernah sampai Nginx).
Untuk uptime monitoring background, saya pakai Uptime Kuma. Setup detailnya: Cara Install Uptime Kuma di VPS.
Takeaway Fase 8: alert push otomatis, dashboard Cloudflare untuk forensik real-time.
11. Fase 9: Load Test + Verifikasi (10 menit)
Setup tidak terverifikasi = setup tidak ada. Test sendiri sebelum tidur.
11.1. Install hey (HTTP Load Generator)
sudo curl -L https://hey-release.s3.us-east-2.amazonaws.com/hey_linux_amd64 \
-o /usr/local/bin/hey
sudo chmod +x /usr/local/bin/hey
11.2. Test Rate Limit wp-login
hey -n 100 -c 10 https://NAMADOMAIN.com/wp-login.php
Output (dipotong):
Summary:
Total: 2.1473 secs
Slowest: 0.4821 secs
Fastest: 0.0234 secs
Requests/sec: 46.5781
Status code distribution:
[200] 5 responses
[429] 61 responses
[403] 34 responses # Cloudflare WAF Challenge unsolvable by hey
5 lolos (sesuai burst), 61 kena 429 dari Nginx, 34 kena 403 dari Cloudflare WAF. Total 95% request berhasil diblokir.
11.3. Test Bad User-Agent
curl -A "sqlmap" -I https://NAMADOMAIN.com/
Output:
curl: (52) Empty reply from server
Status 444 = closed connection. Sukses.
11.4. Test Traffic Normal
hey -n 100 -c 5 -t 30 -H "User-Agent: Mozilla/5.0" https://NAMADOMAIN.com/
Output:
Status code distribution:
[200] 97 responses
[429] 3 responses # natural burst-end, OK
97% sukses. 3% kena 429 di akhir burst, wajar untuk load 100 request paralel dari satu IP.

Takeaway Fase 9: semua 3 layer terbukti aktif. Defense ready, lanjut tidur.
12. Post-Mortem: 3 Hal yang Saya Pelajari
Setelah insiden klien Surabaya stabil dan saya replikasi setup ke 12 klien lain, 3 lesson yang menonjol:
Lesson 1: Edge layer HARUS aktif dari awal, bukan reactive. Banyak klien minta DNS Only di awal karena “ada masalah cache plugin” atau “ngeri lambat”. Setelah insiden ini, saya selalu insist Cloudflare proxy mode ON sejak hari pertama setup. Trade-off cache plugin worth dibandingkan resiko 47 menit downtime. Kalau klien refuse, saya minta tanda-tangan disclaimer.
Lesson 2: Backup fail2ban DB sebelum tweak config. File /var/lib/fail2ban/fail2ban.sqlite3 simpan state semua banned IP. Restart fail2ban kalau jail.local salah regex bisa clear semua banned IP yang sudah terkumpul. Saya pernah kehilangan 1.247 IP banned karena typo regex di hari ke-2 insiden. Cadangkan dulu:
sudo cp /var/lib/fail2ban/fail2ban.sqlite3 /var/lib/fail2ban/fail2ban.sqlite3.bak-$(date +%s)
Lesson 3: Telegram > Email untuk alert. Selama insiden, mail server WordPress origin saya sendiri kena delay queue karena PHP-FPM overload. Email alert sampai 22 menit terlambat. Telegram bot API jalan dari curl di shell, tidak butuh mail queue, langsung push ke HP. Sejak itu Telegram alert wajib di semua klien, email cuma backup.
13. Common Gotcha & Quick Fix
| Gotcha | Symptom | Fix |
|---|---|---|
| Cloudflare proxy ON tapi origin direct-accessible | Attack tetap masuk ke origin IP meskipun WAF aktif | UFW deny default + allow hanya IP range Cloudflare (Fase 2.3) |
| fail2ban tidak ban padahal log terlihat | Currently banned: 0 setelah 1 jam attack | Cek journalctl -u fail2ban -f, biasanya logpath salah atau regex tidak match (test pakai fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/wp-login-attack.conf) |
| 429 false positive ke visitor real | Pelanggan tidak bisa checkout, “Too Many Requests” | Naikkan burst di zone yang relevant, atau pindahkan rate limit ke Cloudflare side |
| Bot Fight Mode block API client legitimate | Plugin / mobile app gagal connect ke REST API | Whitelist via WAF rule berdasar ASN / API token header, jangan block UA |
| AJAX gagal saat Under Attack Mode | Gutenberg auto-save error, plugin async loader timeout | Page Rule /wp-admin/admin-ajax.php* → Security Level Essentially Off (Pro tip #1) |
| curl debug Anda di-block oleh map bad_bot | Anda debug origin pakai curl, kena 444 | Whitelist UA pribadi (LarhTechAdmin/1.0) di map bad_bot |
| Telegram alert spam saat threshold rendah | Notifikasi 100x per jam, jadi noise | Naikkan threshold dari 500 ke 1500, atau debounce script (kirim alert max 1× per 10 menit) |
| Googlebot kena ban setelah test load berlebih | Search ranking drop, indeks Google warning | Whitelist IP range Googlebot di ignoreip fail2ban (script di Fase 7.2) |
Untuk 502 saat serangan beda dengan 502 saat upgrade PHP, referensi: Cara Mengatasi Error 502 Bad Gateway Nginx Saat Pindah Versi PHP.
14. Yang TIDAK Saya Install di Defense Stack Ini
Trade-off jujur, bukan semua tool security saya rekomendasikan. Yang sengaja saya skip:
- Wordfence. Plugin proteksi di sisi PHP. Untuk use case ini, Wordfence menjalankan semua scan + firewall di PHP-FPM origin, tambah TTFB 80–150 ms dan konsumsi RAM ekstra. Semua proteksi yang Wordfence tawarkan sudah saya cover di Cloudflare WAF (edge) + fail2ban (origin). Wordfence cocok untuk site tanpa CDN proxy, bukan setup saya.
- Sucuri Firewall. $200+/tahun untuk fungsi yang setara dengan Cloudflare Free + fail2ban di site <50k pageview/bulan. Sucuri menang di dedicated support, kalau klien butuh white-glove + tidak peduli budget, pertimbangkan. Klien LarhTech mostly budget conscious.
- CrowdSec. Layer optional advanced yang menarik (community-driven IP threat intel). Saya tidak pakai default karena butuh waktu tuning false positive 1–2 minggu, dan untuk klien biasa over-engineered. Layak coba kalau Anda manage >20 server.
- Imunify360 / BitNinja. Paid SaaS yang locked-in ke vendor. Tidak cocok untuk VPS self-managed yang prinsipnya budget conscious + self-hosting. Lebih sering ditemui di shared hosting cPanel.
Filosofi saya: kalau ada solusi gratis + native + sudah teruji, pakai itu. Paid tool dipanggil kalau ada use case spesifik (skala besar, butuh SLA, compliance).
15. Handover ke Klien: 5 Dokumen Wajib
Setelah defense aktif dan stabil 24 jam, saya kirim 5 dokumen ke klien (zip + password lewat sinyal terpisah):
defense-architecture.pdf— Diagram 3-layer flow (export dari draw.io), satu halaman, visual: Cloudflare → Nginx → fail2ban → origin. Klien tidak perlu paham detail, tapi tahu posisi tiap lapisan.cloudflare-access.md— Credential Cloudflare account (kalau saya yang manage) atau delegated access via Cloudflare for Teams (kalau klien mau own account). Plus URL ke Security Analytics yang sudah saya bookmark untuk mereka.waf-rules-snapshot.md— Screenshot 5 WAF Custom Rules + 3 Page Rules + Bot Fight Mode toggle, dengan tanggal sebagai snapshot baseline. Kalau klien (atau plugin) ubah rule tanpa konsultasi, snapshot ini referensi untuk rollback.fail2ban-cheatsheet.md— Command cheat-sheet:
bash # Cek status semua jail sudo fail2ban-client status # Cek detail satu jail sudo fail2ban-client status wp-login-attack # Unban IP (kalau klien minta) sudo fail2ban-client set wp-login-attack unbanip 203.0.113.42
sop-attack-recurrence.md— SOP “Apa yang harus dilakukan kalau serangan terulang”:- Buka Cloudflare → Security → Settings → Security Level → I’m Under Attack!
- WA / Telegram ke saya dengan screenshot dashboard
- JANGAN restart server origin (tidak akan membantu attack L7)
- Tunggu konfirmasi dari saya sebelum tweak apa-apa
- Setelah stabil, kembalikan Security Level ke Medium
16. FAQ
Apakah Cloudflare Free benar-benar cukup untuk DDoS L7? Kapan harus upgrade ke Pro?
Cukup untuk L7 attack sampai sekitar 1–2 juta request/jam (terbukti di kasus klien saya: 1,2 juta req/jam ditahan dengan 5 WAF rules + 3 Page Rules). Upgrade ke Pro ($25/bulan) kalau Anda butuh: (a) lebih dari 5 WAF custom rules, (b) Bot Management (deteksi residential botnet), atau (c) attack volumetric L3/4 di atas 10 Gbps. Untuk site dengan revenue di atas $200/bulan, Pro worth secara cost-benefit.
fail2ban vs Cloudflare WAF, kalau pilih satu, pilih mana?
Pilih Cloudflare WAF. Block di edge artinya origin tidak terbebani sama sekali, bahkan kalau attack 10 juta req/jam, origin Anda tidur. fail2ban di origin baru efektif setelah attack lolos edge (atau direct-hit ke IP origin). Tapi saran beneran: keduanya. Defense-in-depth bukan slogan, ini realita, Cloudflare bocor 13% di kasus saya, dan 13% dari 1,2 juta tetap 162.000 request yang ditangani Nginx + fail2ban.
Nginx rate limit selalu false positive untuk user di kantor. Solusi?
User behind shared NAT (kantor, sekolah, ISP CGNAT, Starlink) tampak sebagai 1 IP. Tiga opsi:
Whitelist di Cloudflare WAF berdasarkan ASN provider Anda.
Naikkan burst di Nginx (mis. burst=10 untuk wp-admin, burst=50 untuk general).
Pindahkan rate limit ke Cloudflare side pakai Cloudflare Rate Limiting (versi gratis basic, versi advanced butuh Pro).
Kombinasi #1 + #2 yang saya pakai default untuk klien dengan tim hybrid.
Saya pakai Hetzner / Contabo. Cloudflare proxy mode aman?
Aman secara teknis. Cek TOS provider, beberapa provider seperti OVH punya kebijakan khusus untuk traffic CDN proxy (umumnya boleh tapi ada limit). Hetzner Cloud + Contabo: tidak ada masalah. Vultr + DigitalOcean: fully supported. Yang tidak boleh: forward traffic non-HTTP (streaming, gaming, generic TCP) via Cloudflare proxy, itu melanggar Cloudflare TOS Free tier.
Saya pakai OpenLiteSpeed, bukan Nginx. Konfig rate limit-nya bagaimana?
OpenLiteSpeed punya equivalent di Web Admin → Configuration → Listeners → General → Throttle Limit. Konsep sama (per-IP request rate + connection limit), syntax beda. Detail config untuk OLS layak artikel terpisah, di prinsipnya, ganti limit_req_zone Nginx dengan throttle config OLS dan ganti limit_conn dengan max connection per IP. fail2ban + Cloudflare bagian playbook ini tetap berlaku sama persis.
Setelah Under Attack Mode OFF, serangan langsung balik. Solusi permanent?
Under Attack Mode adalah emergency band-aid, bukan solusi permanen. Begitu OFF, JS Challenge ke semua visitor dimatikan dan bot bisa lewat lagi. Permanent solusi: kombinasi 4 layer yang sudah saya bahas, Cloudflare WAF custom rules (Fase 3) + Bot Fight Mode (Fase 4) + Nginx rate limit (Fase 5) + fail2ban (Fase 7). Setelah ke-4 aktif, Under Attack Mode bisa OFF dan attack tetap ke-handle otomatis. Saya jarang nyalakan Under Attack mode lagi setelah klien saya migrate ke setup ini.
17. Penutup
Tiga lapis defense — Cloudflare di edge, Nginx di proxy, fail2ban di app, bukan formula sempurna. Tapi setelah saya replikasi ke 12 klien selama 14 bulan, nol downtime karena DDoS L7, 380.000+ IP banned cumulative, dan $0/bulan tambahan biaya (semua tier Free / built-in). Untuk skala UMKM dan blog niche Indonesia, ini cukup sampai traffic Anda tembus puluhan juta pageview per bulan.
Yang tidak saya bahas di artikel ini tapi worth dipelajari lanjut: matikan endpoint yang Anda tidak pakai (XML-RPC paling sering jadi attack vector, cara matikannya tanpa plugin), tuning Nginx FastCGI Cache supaya origin idle lebih rendah saat attack (panduan FastCGI cache lengkap), dan optimasi WordPress 3-layer cache supaya origin punya headroom CPU saat traffic spike normal (Page Cache vs Object Cache vs OPcache).
Untuk pembaca yang baru mulai dari nol: cara install WordPress di VPS Ubuntu dulu, lalu kembali ke artikel ini.
Punya pengalaman attack berbeda, DDoS amplification, slow loris, ransomware injection? Tinggalkan komentar di bawah dengan output log + pattern yang Anda lihat. Saya audit dan jawab.
Last Updated on Mei 23, 2026 by larhtechBro



