Teknoloji

Nginx Rate Limiting ve Fail2ban ile wp‑login.php ve XML‑RPC Brute‑Force Saldırılarını Nasıl Saksıya Alırsın?

Bir Sabah Logların Arasına Düşmek: Neden Bu Konuyu Konuşuyoruz?

Hiç dün akşam her şey yolundayken sabah kahveni alıp masaya oturduğunda, log dosyalarının çığ gibi büyüdüğünü gördün mü? Ben bir gün tam olarak bunu yaşadım. Sunucuya baktım, CPU pek ses etmiyor ama bant genişliğinde ufak bir kıpırdanma, Nginx loglarında ardı ardına aynı yollar: /wp-login.php ve /xmlrpc.php. Mesaj belli: Biri kapıyı zorlamış. Düşündüm ki, bu sadece benim başıma gelmiyor; WordPress kullanan pek çok kişinin “tanıdık” sorunu. Şu meşhur brute-force dalgaları.

İşte burada iki eski dost devreye giriyor: Nginx Rate Limiting ve Fail2ban. Biri nazikçe “yavaş kardeşim” derken, diğeri sabırsızı kapıdan alıp kenara koyuyor. Bu yazıda ikisini birlikte nasıl taktiksel ve sıcak bir uyum içinde çalıştırabileceğimizi konuşacağız. Nginx tarafında akışı sınırlayacağız, istekleri ritme sokacağız; Fail2ban ile de ısrarcıları kısa süreli tatillere göndereceğiz. Arada Cloudflare, gerçek IP, loglama ve ufak tefek pürüzler hakkında da tatlı notlar var. Sonunda elinde hem anlaşılır hem de uygulanabilir bir reçete olacak.

Hedef Neden Hep wp-login.php ve XML‑RPC?

Önce hikâyenin kahramanlarını tanıyalım. wp-login.php WordPress’in giriş kapısı. Tahmin edilebilir, herkesçe biliniyor. Saldırganlar, kullanıcı adı ve parola deneyerek bu kapıyı zorlamayı pek seviyor. Yüzlerce, bazen binlerce deneme ile küçük bir açık arıyorlar. Kimi zaman çıldırtacak kadar ısrarcı olabiliyorlar, özellikle de bir yerlerden sızdırılmış kullanıcı adı listeleri ellerindeyse.

xmlrpc.php ise farklı bir dünya. Uzaktan yönetim, mobil uygulama, Jetpack gibi araçlar burada devreye giriyor. Güzel tarafı şu: Bir istekle birden fazla giriş denemesini paketleyebiliyorlar. Bu da saldırganın iştahını kabartıyor. Eğer sen XML‑RPC’yi kullanmıyorsan, bu kapı gereksiz açık sayılır. Kullanıyorsan da öyle her isteği kabul etmek zorunda değilsin; akışını sınırlamak iyi fikir.

Günün sonunda ikisi de aynı problemi doğuruyor: Gereğinden fazla deneme, gereksiz kaynak tüketimi ve güvenlik riski. Benim yaklaşımım şu oldu: Nginx ile ritmi düşür, Fail2ban ile ısrarcıyı dışarı al. Bu komboyu doğru kurunca hem kaynakların nefes alıyor hem de gece daha rahat uyuyorsun.

Nginx Rate Limiting: “Yavaş, Derin Nefes” Tekniği

Mesela şöyle düşün: Kapında bir sıra oluşuyor, kapıdaki görevli nazik ama net. “Herkes sırayla.” Nginx’in limit_req modülü tam bunu yapıyor. İstek başına bir ritim belirliyorsun, bir de taşma payı tanımlıyorsun. Böylece ani patlamaları yutuyor ama ısrarcıyı tuhaf bir tempoya zorluyor. Bu bile pek çok botun hevesini kırıyor.

Ben genelde giriş kapısında sadece POST isteklerini sınırlıyorum. Ziyaretçi giriş sayfasını görüntüleyebilir, sorun yok; ama arka arkaya parola deneyecekse, işte orada fren. XML‑RPC içinse biraz daha sıkı davranıyorum çünkü tek istekte birden fazla deneme yapılabiliyor.

Aşağıdaki örnek Nginx tarafında temel bir kurulum. Değerler yol gösterici, senin trafik profilin ve kullanıcı deneyimin doğrultusunda yumuşatabilirsin.

# /etc/nginx/nginx.conf (veya http{} bloğu)
# IP başına ritim alanları
limit_req_zone $binary_remote_addr zone=wp_login:10m rate=5r/m;       # wp-login için dakikada 5
limit_req_zone $binary_remote_addr zone=xmlrpc:10m rate=30r/m;        # xmlrpc için dakikada 30

# 429 kodunu net belirtelim
limit_req_status 429;

# İsteğin POST olup olmadığını işaretlemek için ufak bir işaretçi
map $request_method $is_post {
    default 0;
    POST    1;
}

# İsteğin login/xmlrpc olup olmadığını loglarda ayırmak daha güzel olur
map $uri $is_sensitive_endpoint {
    default 0;
    /wp-login.php 1;
    /xmlrpc.php  1;
}

log_format sensitive '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    '"$http_referer" "$http_user_agent"';

Sunucu bloğunda ise şu şekilde işleyebiliriz. Burada püf noktası, limit_except ile yalnızca POST’ları sınırlamak ve 429 veren istekleri göze batmayan bir metinle karşılamak. Kullanıcıya 429 göstermek yerine ufak bir gecikme ve “sonra dene” mesajı koymak, botları pek ilgilendirmez ama insanlar için anlaşılır olur.

# /etc/nginx/sites-enabled/senin-site.conf  (server{} içinde)

# Duyarlı yollar için ayrı log dosyası açmak istersen:
access_log /var/log/nginx/sensitive.log sensitive if=$is_sensitive_endpoint;

location = /wp-login.php {
    # GET serbest, POST sınırlı
    limit_except GET {
        limit_req zone=wp_login burst=10 nodelay;
        add_header Retry-After 60 always;
    }
    # Burada normal PHP yönlendirme bloğunu ekle
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}

location = /xmlrpc.php {
    # XML-RPC genelde sadece POST alır, o yüzden direkt limit uyguluyoruz
    limit_req zone=xmlrpc burst=20 nodelay;
    add_header Retry-After 60 always;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}

# İsteğe bağlı: 429'lar için zarif bir yanıt
error_page 429 = @ratelimited;
location @ratelimited {
    return 429 'Biraz yavaş gidelim. Lütfen kısa bir süre sonra tekrar dene.';
    add_header Content-Type text/plain;
}

Burada kullandığım oranlar örnek. Giriş sayfasında dakikada beş başarılı deneme, taşma payı on; XML‑RPC’de dakikada otuz, taşma payı yirmi gibi. Ziyaretçi kitlene göre yükseltip düşürebilirsin. Ben önce sıkı başlar, sonra şikâyet gelmezse biraz gevşetirim. En güzeli ufak ufak ayar çekmek.

Teknik meraklısıysan Nginx’in bu modülü için resmi dokümana göz atman iyi olur. Ben ilk kurcaladığımda sahayı daha iyi anlamamı sağladı: Nginx limit_req modülü dokümantasyonu.

Fail2ban: Israrcıyı Nezaketen Dışarı Almak

Nginx ritmi düşürdü. Güzel. Ama bazı saldırganlar var ki tespih gibi ısrarla deniyor. Burada Fail2ban devreye girdi mi işler kolaylaşıyor. Benim yaklaşım: Nginx’in 429 verdiği POST isteklerini izlemek ve bunun belirli bir sayıyı geçmesi durumunda IP’yi bannlamak. Böylece hem masum ziyaretçiyi üzmüyorsun hem de saldırgana net bir sınır çiziyorsun.

Önce basit bir filtre yazalım. Varsayılan Nginx log formatını kullandığını varsayıyorum; IP, tarih, istek ve durum kodu klasik akışta.

# /etc/fail2ban/filter.d/nginx-wp-xmlrpc-rl.conf
[Definition]
failregex = ^<HOST>s-s.*s"POSTs/wp-login.phpsHTTP/S+"s429s
            ^<HOST>s-s.*s"POSTs/xmlrpc.phpsHTTP/S+"s429s
ignoreregex =

Şimdi bir de jail kuralı. Burada hangi portları kapatacağını, kaç denemeden sonra ban atacağını ve ne kadar süreyle dışarı alacağını belirliyorsun. İstersen “nftables” aksiyonunu da kullanabilirsin, sunucunda nftables çalışıyorsa bu daha güncel bir yol.

# /etc/fail2ban/jail.d/nginx-wp-xmlrpc-rl.conf
[nginx-wp-xmlrpc-rl]
enabled  = true
filter   = nginx-wp-xmlrpc-rl
# nftables kullanıyorsan şu aksiyonu tercih et (dağıtımına göre değişebilir):
# action = nftables-multiport[name=nginx-rl, port="http,https", protocol=tcp]
# iptables kullanıyorsan:
action   = iptables-multiport[name=nginx-rl, port="http,https", protocol=tcp]
logpath  = /var/log/nginx/access.log
findtime = 5m
maxretry = 5
bantime  = 1h

Fail2ban’i yeniden başlatmadan önce kuralın loglarla eşleştiğinden emin olmak güzel. Ben genelde test için aşağıdakini çalıştırıyorum:

fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/nginx-wp-xmlrpc-rl.conf

Çıkan sonuçta eşleşen satırlar görüyorsan, doğru yoldasın. Ardından Fail2ban’i ayağa kaldır:

systemctl restart fail2ban
fail2ban-client status nginx-wp-xmlrpc-rl

Burada yasaklı IP sayısını, kaç kez tetiklendiğini görürsün. Gerekirse findtime, maxretry ve bantime gibi parametreleri ufak ufak ayarlayarak kendi sitenin ritmine uydurursun. Ben ilk günlerde biraz sıkı bırakıp birkaç gün sonra verileri okuyup daha dengeli değerlere çekerim.

Ek bir not: Cloudflare arkasındaysan ağ katmanında IP banlamak pek işe yaramaz, çünkü bağlantı sunucuna Cloudflare IP’lerinden gelir. Yine de loglarda gerçek istemci IP’sini görmek istiyorsan Fail2ban ile Cloudflare action kullanabilir veya Nginx içinde yasaklanan IP’leri bir harita dosyasına yazdıran aksiyonlar tercih edebilirsin. Çok derine dalmadan, pratik kalmak istersen Nginx rate limit çoğu zaman tek başına saldırıyı söndürür, Fail2ban ise arıza yapanları daha hızlı budar. Fail2ban’in dokümantasyonuna göz atmak istersen: Fail2ban resmi dokümantasyonu.

Proxy Arkası, Gerçek IP, Loglar ve Küçük Tuzaklar

İşin en çok atlanan kısmı burası. Eğer Cloudflare veya başka bir CDN/proxy kullanıyorsan, Nginx’in gerçek istemci IP’sini görmesi gerekir. Aksi halde loglarda hep CDN IP’lerini görürsün; Fail2ban de yanlış IP’yi hedef alır. Bunun çözümü basit: Gerçek IP başlığını tanıtmak.

# /etc/nginx/nginx.conf (http{} içinde)
# Cloudflare örneği:
set_real_ip_from  173.245.48.0/20;
set_real_ip_from  103.21.244.0/22;
set_real_ip_from  103.22.200.0/22;
set_real_ip_from  103.31.4.0/22;
# ... (Cloudflare IPv4/IPv6 bloklarının tamamı)
real_ip_header    CF-Connecting-IP;

Cloudflare IP blokları değişebildiği için güncel listeyi periyodik kontrol etmekte fayda var. Ayrıca HTTP/2 ve HTTP/3 kullanıyorsan, ağın uçtan uca modernleşmesi hem performans hem de stabilite için iyi hissettiriyor. Buna dair pratik bir kurulum akışı istersen, şu rehber elini sıcak tutar: Nginx ve Cloudflare’da HTTP/2 ve HTTP/3’ü etkinleştirme rehberi.

Güvenlik katmanını sadece uygulama tarafında bırakmak bazen yetmeyebilir. Basit ama etkili bir ağ duvarı düzeni de işleri sakinleştirir. Eğer sunucunda nftables kullanıyorsan, rate limit ve küçük akış kuralları hayli leziz sonuç veriyor. Bir ara şu yazıda anlattığım yaklaşıma göz atmıştım ve hâlâ çoğu VPS’te iş görüyor: nftables ile VPS güvenlik duvarı rehberi.

Log tarafında ise bir öneri daha: Duyarlı uç noktaları ayrı log dosyasına almak tanı koymayı kolaylaştırıyor. Üstteki log_format sensitive ve access_log ... if=$is_sensitive_endpoint ikilisi bunun için. İleride merkezi loglamaya geçersen anomaliyi yakalamak çok daha kolaylaşır.

XML‑RPC’yi Kapatmalı mıyım, Kısmalı mıyım?

Güzel soru. Her zaman aynı cevap yok. Eğer WordPress mobil uygulaması, bazı otomasyon araçları veya Jetpack gibi özellikler kullanıyorsan xmlrpc.php işe yarıyor. Ama hiç kullanmıyorsan, neden açık dursun? Ben genelde şöyle yapıyorum: Önce kullanıp kullanmadığımı dürüstçe kontrol ediyorum. Kullanmıyorsam net biçimde kapatıyorum. Kullanıyorsam da Nginx ile sıkı bir rate limit, gerekiyorsa ufak bir Basic Auth duvarı veya IP tabanlı kısıt ekliyorum.

Kapatmak istersen Nginx tarafında nazikçe kapı duvarı örebilirsin:

location = /xmlrpc.php {
    return 444;  # sessizce kapat
}

Bu, botların hevesini hemen kırar. Fakat tamamen kapatmadan önce sisteminde nelere dokunduğunu düşün. Eğer emin değilsen, önce sıkı bir rate limit ile başla, gözlemle. WordPress ekosistemindeki uzaktan çağrıları anlama niyetindeysen, resmi referanslar fikir veriyor: WordPress XML‑RPC genel bakış.

Ben bazen bir “ön kapı” daha koyuyorum. Özellikle xmlrpc’nin gerçekten gerekli olduğu projelerde, küçük bir Basic Auth katmanı fena iş görmüyor. Saldırganlar için fazladan bir kapı, meşru servisler içinse kolayca geçilen bir eşik. Dikkat edilmesi gereken nokta, uygulamanın kullandığı istemcilerin bu ekstra kimliği doğru verebilmesi.

İzlemek, Test Etmek, Küçük Ayarlar Yapmak

Bir kurulumun gerçekten çalışıp çalışmadığını anlamanın en hızlı yolu küçük testler. Ben önce bir terminale geçip birkaç hızlı POST isteği atarım, Nginx’ten 429 düşüyor mu bakarım:

for i in $(seq 1 20); do 
  curl -s -o /dev/null -w "%{http_code}n" -X POST https://alanadın.com/wp-login.php; 
  sleep 0.5; 
done

Bir noktada 429 görüyorsan, Nginx ritmi yakalamış demektir. Ardından fail2ban-client status nginx-wp-xmlrpc-rl ile ban’ın tetiklenip tetiklenmediğine bakarım. İlk saatlerde biraz sıkı bir bantime koyup, birkaç gün sonra veriyi okur ayarları yumuşatırım.

Eğer merkezi loglama kuruluysa trend görmek çok daha kolay. Ben çoğu VPS’te Grafana Loki + Promtail ile merkezi loglama yaklaşımını seviyorum. 429 artış eğrisi, belirli saatlerde yoğunlaşan denemeler, hatta tek bir kaynaktan fışkıran dalga… Hepsi gözünün önüne seriliyor. Tetikleyici alarmlar kurgulamak da cabası.

WordPress’i konteynerlarda koşturuyorsan, Nginx ve PHP-FPM kombinasyonunu container içinde ya da host üstünde konumlandırman fark yaratır. Bu tarz canlıya alma ve kalıcı depolama akışlarını derli toplu bir şekilde görmek istersen, şuradaki yolculuk bence iyi bir rehber oluyor: Docker ile WordPress’i VPS’te yaşatmak.

Ufak Pürüzler, Pratik Çözümler

Her kurulumda bir iki küçük pürüz çıkar. En yaygın olanları şöyle deneyimledim. Birincisi, log formatı. Fail2ban filtresindeki regex, log formatınla birebir uyumlu olmalı. Özel bir format kullanıyorsan filtreyi ona göre güncelle. Test için fail2ban-regex kurtarıcı. İkincisi, gerçek IP. Proxy arkasında gerçek IP’yi Nginx’e tanıtmadıysan Fail2ban yanlış kişiyi cezalandırabilir. Bu da can sıkar.

Üçüncüsü, limitleri çok sıkı tutmak. Bazen iyi niyetli kullanıcıların, özellikle mobil bağlantılarda, birkaç denemesi üst üste gelebiliyor. İlk günlerde biraz sıkı başlayıp sonra veriye bakarak yumuşatmak, hem güvenliği hem kullanıcı deneyimini dengeler. Dördüncüsü, testi ihmal etmek. Terminalden bir iki curl ile 429’u görmek, Fail2ban istatistiklerini kontrol etmek alışkanlık olsun. Son olarak, Cloudflare aksiyonları. Gerçekten lazım olduğunda Fail2ban’in Cloudflare API aksiyonlarına da bakabilirsin; doğrudan Cloudflare düzeyinde engellemek bazen daha pratik oluyor.

Bu arada SSL ve modern protokoller tarafında da ufak dokunuşlar yapmak sunucunun genel sağlığını etkiliyor. Sertifika, HSTS, modern şifre takımları ve el sıkışması ayarları için ben geçmişte şu rehberden faydalanmıştım: TLS 1.3 ve modern şifrelerin sıcacık mutfağı. Güvenlik bir bütün.

Kapanış: Ritmi Sen Belirle

Şöyle bağlayayım: Brute‑force aslında çoğu zaman gürültüden ibaret. Yeter ki ritmi sen belirle. Nginx rate limiting ile bir anda yükleneni yavaşlat, Fail2ban ile ısrarcıyı kibarca dışarı al. Gerekirse xmlrpc’yi kapat, gerekirse kıs. Proxy arkasındaysan gerçek IP’yi tanıt, loglarını ayrı bir köşeye not al, sonra da birkaç küçük testle düzenini doğrula. Hepsi bu kadar. Karmaşık gibi görünse de elin alışınca çok doğal bir refleks haline geliyor.

Umarım bu yazı sana fikir vermiştir. Kendi trafiğini ve kullanıcılarını tanıyıp değerleri ona göre ayarladığında, hem sunucun rahat edecek hem sen. Takıldığın bir yerde küçük küçük ilerle, parça parça test et. Bir sonraki yazıda görüşmek üzere; logların sakince akması, ban listelerinin boş kalması dileğiyle. Bu arada detaylı belge okumayı seviyorsan Nginx’in limit modülü ve Fail2ban dokümantasyonları güzel kaynaklar; küçük molalarda göz atmak iyi geliyor.

İlgini çekebilecek dış kaynaklar: Nginx limit_req modülü, Fail2ban belgeleri ve WordPress XML‑RPC genel bakış. İçerideki yolculuklar içinse nftables ile VPS güvenlik duvarı, Loki + Promtail ile log yönetimi, WordPress’i Docker’da yaşatmak ve Cloudflare + Nginx ile HTTP/2‑3 leziz eşlikçiler olur.

Sıkça Sorulan Sorular

Kullanımına bağlı. Mobil uygulama, Jetpack gibi araçlar kullanmıyorsan tamamen kapatmak pratik. Kullanıyorsan Nginx ile sıkı rate limit uygula, gerekirse küçük bir Basic Auth veya IP kısıtı ekle.

Ayarlar çok sıkıysa edebilir. Önce biraz muhafazakâr başla, birkaç gün veriyi izle, sonra yumuşat. Sadece POST isteklerine uygulamak ve burst değerini makul tutmak deneyimi korur.

Yarar ama ağ katmanı banları Cloudflare IP’lerini hedef alır. Gerçek IP’yi Nginx’e tanıt, gerekirse Fail2ban’in Cloudflare API aksiyonlarını kullan ya da uygulama katmanında (Nginx) yasaklama stratejisi uygula.