nginx ile "Dağıtık" Saldırılara Karşı Alt Ağ Bazlı Rate Limiting
Ya da: 70 farklı IP'den gelen ama hiçbiri tek başına şüpheli görünmeyen bir saldırıyı nasıl durdururuz?
Bir gün sunucum aniden yavaşlamaya başladı. Log'lara baktığımda klasik bir "tek kötü IP" görüntüsü yoktu — bunun yerine aynı /24 bloğundan (yani "192.168.1.X" tarzı, sadece son rakamı değişen) 70'e yakın farklı IP, her biri kendi başına makul sayıda istek atıyordu. Hiçbiri tek başına alarm verecek kadar agresif değildi, ama hepsi birden sunucuyu dize getiriyordu.
Bu yazıda, bu tarz "dağıtık ama düşük yoğunluklu" saldırılara karşı nginx'in kendi imkanlarıyla (üçüncü parti araç, Cloudflare falan olmadan) nasıl önlem alınacağını, sıfırdan anlatıyorum.
1. Önce temel: nginx'te "hız sınırlama" nasıl çalışır?
nginx, gelen istekleri bir kovaya damlayan su gibi düşünür (buna "leaky bucket" algoritması denir). Şöyle bir kural tanımlarsın:
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
Türkçesi: "Her IP için ayrı bir kova aç (`$binary_remote_addr` = IP adresi). Bu kova saniyede 10 isteklik bir akışı sindirebilsin. Bütün kovaları tutmak için 10MB'lık bir bellek alanı ayır (`zone=mylimit:10m`)."
Sonra bu kuralı bir yere uygulaman lazım:
location / {
limit_req zone=mylimit burst=20 nodelay;
}
burst=20: ani patlamalarda 20 isteklik ekstra tampon tanı (yoksa gerçek bir kullanıcının açtığı sayfadaki 5-6 paralel istek bile anında engellenir).nodelay: bu tampondaki istekleri bekletmeden hemen işle, sadece tampon dolunca reddet. (Bunu koymazsan nginx istekleri sıraya koyup yavaşça işler — bu da gerçek kullanıcıyı fark ettirmeden yavaşlatır.)
Eşik aşılınca nginx varsayılan olarak 503 Service Unavailable döner.
2. Peki neden bu bazen yetmiyor?
Kritik nokta şu: kova, anahtar ne ise ona göre açılır. Yukarıdaki örnekte anahtar $binary_remote_addr, yani IP adresi. Bu demek oluyor ki:
100 farklı IP saldırıyorsa, nginx 100 ayrı kova açar. Hiçbiri diğerini etkilemez. Saldırgan, her IP'yi eşiğin biraz altında tutarak bu korumayı tamamen atlatabilir — ki botnet'ler zaten genelde böyle çalışır.
Benim yaşadığım durum tam olarak buydu: 70 farklı IP, hepsi aynı /24 bloğundan (örnek: 57.141.0.3, 57.141.0.61, 57.141.0.64...), hiçbiri tek başına eşiği geçmiyordu.
3. Çözüm: map ile "sahte" bir ortak anahtar üretmek
İhtiyacımız olan şey: 70 farklı IP'nin aynı kovaya düşmesi. nginx'te "bu IP'nin /24'ü nedir" diye hazır bir değişken yok, ama map direktifiyle kendimiz üretebiliyoruz:
map $remote_addr $subnet_key {
default $remote_addr;
"~^(?<net>\d+\.\d+\.\d+)\.\d+$" $net;
}
Bu satır korkutucu görünebilir ama parça parça çok basit. Bir IP adresi üzerinden (57.141.0.71) soldan sağa gidelim:
| Parça | Anlamı |
|---|---|
~ | "Bu bir düz metin değil, bir regex kalıbı" (nginx map'e özel bir işaret) |
^ ... $ | "Metnin başından sonuna kadar tamamen eşleşsin" |
\d | "Bir rakam" (0-9) |
\d+ | "Bir veya daha fazla rakam" |
\. | "Gerçek bir nokta karakteri" (düz . regex'te "her şey" demek, o yüzden \ ile "gerçekten nokta" yapıyoruz) |
(?<net>...) | "Bu parantezin içinde eşleşeni bir kenara ayır, adını net koy, sonra $net diye kullanayım" |
Şimdi bunu 57.141.0.71 üzerine uygulayalım:
^ → başlangıç
\d+ → "57"
\. → "."
\d+ → "141"
\. → "."
\d+ → "0" ┐
├─ bunlar (?<net>...) İÇİNDE → net = "57.141.0"
\. → "." ┘ (parantez burada kapanıyor)
\d+ → "71" ← SON rakam grubu parantez DIŞINDA, yani atılıyor
$ → bitiş
Sonuç: $net = "57.141.0" — son oktet (.71) kırpıldı. 57.141.0.3 için de aynı işlemi yapsan yine "57.141.0" çıkar. İşte tam istediğimiz şey: son rakamı ne olursa olsun, aynı /24 bloğundaki her IP artık aynı $subnet_key'e sahip.
Genel kural olarak aklında kalsın: \d+ = rakam(lar), \. = gerçek nokta, (?<isim>...) = "bu kısmı işaretle", ^...$ = "baştan sona tam eşleşsin". Bu dört kalıp, günlük regex ihtiyaçlarının büyük kısmını karşılar.
4. Hepsini birleştirmek
Artık $subnet_key değişkenimiz var, onu normal limit_req_zone'da anahtar olarak kullanabiliriz:
http {
# ... nginx.conf'un http{} bloğu içinde, herhangi bir server{} bloğunun dışında:
map $remote_addr $subnet_key {
default $remote_addr;
"~^(?<net>\d+\.\d+\.\d+)\.\d+$" $net;
}
limit_req_zone $subnet_key zone=subnet_limit:10m rate=30r/s;
server {
server_name example.com;
location / {
limit_req zone=subnet_limit burst=60 nodelay;
# ... geri kalan config'in (proxy_pass vb.) aynen kalıyor
}
}
}
Not: map ve limit_req_zone sadece http{} seviyesinde tanımlanabilir (server/location içinde olmaz). limit_req (uygulama satırı) ise istediğin server{} veya location{} içine yazılır — böylece sadece saldırı gördüğün siteyi etkiler, diğerlerine dokunmaz.
5. Neden her yere değil, tek tek siteye uyguluyoruz?
Aynı sunucuda birden fazla site barındırıyorsan, bu kuralı hepsine birden http{} seviyesinde yazmak cazip gelebilir — teknik olarak mümkün, çünkü nginx ayarları üstten alta miras alınır. Ama önermiyorum, çünkü:
- Her sitenin trafik "kişiliği" farklıdır. Bir webmail sayfası sürekli arka planda "yenile" isteği atar, bir blog sayfası öyle çalışmaz — tek bir sayı ikisine de adil oturmaz.
- Yanlış ayarın etki alanı. Rakamları ilk seferde muhtemelen tam doğru tutturamazsın (kalibrasyon zaman alır). Global yazarsan bir hata TÜM siteleri aynı anda etkiler; tek siteye yazarsan hata payı orada sınırlı kalır.
- Sadece kanıt olan yere uygula. Saldırı gördüğün siteye özel önlem al; saldırı görmediğin siteye körlemesine kısıtlama koyup gerçek kullanıcıları riske atma.
6. Uygularken dikkat edilecekler
- Önce mutlaka syntax testi yap:
"syntax is ok" / "test is successful" görmeden asla reload etme.nginx -t - Reload et (restart değil):
Reload, aktif bağlantıları kesmeden config'i tazeler.service nginx reload - İlk birkaç gün error log'unu izle:
Bu satırlar, kuralın gerçekten kimi/ne zaman engellediğini gösterir. Gerçek kullanıcılar burada görünmeye başlarsa,grep "limiting requests" /var/log/nginx/error.lograte/burstdeğerlerini yukarı çek. Saldırı hâlâ geçiyorsa aşağı çek.
Özet
Klasik IP-bazlı rate limiting, saldırı tek bir kaynaktan geldiğinde işe yarar. Saldırı onlarca/yüzlerce IP'ye dağıtılmışsa (ki modern botnet'ler genelde böyle çalışır), tek çare, isteği hangi IP attığına değil, hangi ağ bloğuna ait olduğuna göre saymaktır. Bunun için nginx'e hazır bir özellik yok, ama map direktifiyle üç-beş satırlık bir regex'le kendi çözümünü yazabiliyorsun — üçüncü parti bir servise ihtiyaç duymadan, tamamen kendi sunucunda.

Yorum Gönder