Cloudflare Workers ile Kötü Trafikleri Rate Limiting ile Engelleme
Edge’de çalışan bir fonksiyon yazıp sunucuna tek satır kod eklemeden kötü trafiği engelleyebiliyorsun. Bu, Cloudflare Workers’ın sysadminlere sunduğu en güçlü özelliklerden biri. Rate limiting konusu kulağa basit gelebilir ama production ortamında yanlış yapılandırılmış bir rate limiter, meşru kullanıcıları engellerken bot trafiğini geçirebilir. Bu yazıda sıfırdan gerçek dünya senaryolarına kadar Cloudflare Workers ile rate limiting nasıl yapılır, detaylı anlatalım.
Cloudflare Workers Neden Bu İş İçin İdeal?
Geleneksel rate limiting için genellikle Nginx’e limit_req_zone eklersin, ya da uygulama katmanında Redis tabanlı bir çözüm kurarsın. Her ikisi de sunucuna ulaşan trafiği işlemek zorunda. Yani DDoS saldırısı veya scraping botu, en azından sunucunun ağ katmanına kadar gelir.
Cloudflare Workers ise Cloudflare’in edge node’larında çalışır. Türkiye’den gelen bir istek İstanbul’daki Cloudflare PoP’una ulaşır ve orada işlenir. Sunucun bunu hiç görmez. Bu yaklaşımın avantajları:
- Sıfır gecikme eklentisi: Edge’de çalıştığı için origin’e gitmeden karar verilir
- Küresel dağıtım: Tüm Cloudflare PoP’larında aynı anda çalışır
- KV Store: Workers KV ile global state tutabilirsin
- Durable Objects: Tutarlı sayaç yönetimi için kullanılabilir
Temel Rate Limiting Mantığı
Önce en basit haliyle başlayalım. Bir IP adresinden belirli sürede kaç istek geldiğini sayıp, limiti aşanları engelleyelim.
// wrangler.toml'da KV namespace bağlaman gerekiyor:
// [[kv_namespaces]]
// binding = "RATE_LIMIT"
// id = "your-kv-namespace-id"
export default {
async fetch(request, env, ctx) {
const ip = request.headers.get('CF-Connecting-IP');
const key = `rate_limit:${ip}`;
// KV'den mevcut sayacı al
const current = await env.RATE_LIMIT.get(key);
const count = current ? parseInt(current) : 0;
// Limit: 60 saniyede 100 istek
const LIMIT = 100;
const WINDOW = 60; // saniye
if (count >= LIMIT) {
return new Response('Too Many Requests', {
status: 429,
headers: {
'Retry-After': WINDOW.toString(),
'X-RateLimit-Limit': LIMIT.toString(),
'X-RateLimit-Remaining': '0',
},
});
}
// Sayacı artır, TTL ile birlikte kaydet
ctx.waitUntil(
env.RATE_LIMIT.put(key, (count + 1).toString(), {
expirationTtl: WINDOW,
})
);
// Orijinal isteği ilet
const response = await fetch(request);
return response;
},
};
Bu basit örnek çalışır ama bir sorunu var: Fixed Window problemi. Pencere sınırında burst saldırıları yapılabilir. Mesela 59. saniyede 100 istek, 61. saniyede 100 istek. Toplam 2 saniyede 200 istek geçer.
Sliding Window Rate Limiting
Daha sağlam bir çözüm için sliding window algoritması kullanmalısın. Her istek için zaman damgası saklanır ve sadece son WINDOW saniyedeki istekler sayılır.
export default {
async fetch(request, env, ctx) {
const ip = request.headers.get('CF-Connecting-IP');
const now = Date.now();
const LIMIT = 100;
const WINDOW_MS = 60 * 1000; // 60 saniye
const key = `sliding:${ip}`;
// Mevcut timestamp listesini al
const stored = await env.RATE_LIMIT.get(key, { type: 'json' });
let timestamps = stored || [];
// Pencere dışındaki eski timestamp'leri temizle
timestamps = timestamps.filter(ts => now - ts < WINDOW_MS);
if (timestamps.length >= LIMIT) {
const oldestInWindow = timestamps[0];
const retryAfter = Math.ceil((oldestInWindow + WINDOW_MS - now) / 1000);
return new Response(
JSON.stringify({
error: 'Rate limit exceeded',
retryAfter: retryAfter,
}),
{
status: 429,
headers: {
'Content-Type': 'application/json',
'Retry-After': retryAfter.toString(),
'X-RateLimit-Limit': LIMIT.toString(),
'X-RateLimit-Remaining': '0',
'X-RateLimit-Reset': new Date(oldestInWindow + WINDOW_MS).toISOString(),
},
}
);
}
// Yeni timestamp ekle ve kaydet
timestamps.push(now);
ctx.waitUntil(
env.RATE_LIMIT.put(key, JSON.stringify(timestamps), {
expirationTtl: Math.ceil(WINDOW_MS / 1000),
})
);
const response = await fetch(request);
// Response header'larına rate limit bilgisi ekle
const newResponse = new Response(response.body, response);
newResponse.headers.set('X-RateLimit-Limit', LIMIT.toString());
newResponse.headers.set(
'X-RateLimit-Remaining',
(LIMIT - timestamps.length).toString()
);
return newResponse;
},
};
Endpoint Bazlı Farklı Limitler
Gerçek dünyada her endpoint aynı limiti hak etmez. Login endpoint’i dakikada 5 istek alabilirken, statik asset endpoint’i çok daha fazlasını kaldırmalıdır.
const RATE_LIMITS = {
'/api/auth/login': { limit: 5, window: 60 },
'/api/auth/register': { limit: 3, window: 300 },
'/api/search': { limit: 30, window: 60 },
'/api/data': { limit: 100, window: 60 },
default: { limit: 200, window: 60 },
};
function getRateLimit(pathname) {
// Exact match önce dene
if (RATE_LIMITS[pathname]) {
return RATE_LIMITS[pathname];
}
// Prefix match
for (const [pattern, config] of Object.entries(RATE_LIMITS)) {
if (pattern !== 'default' && pathname.startsWith(pattern)) {
return config;
}
}
return RATE_LIMITS.default;
}
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const ip = request.headers.get('CF-Connecting-IP');
const { limit, window } = getRateLimit(url.pathname);
const key = `rl:${ip}:${url.pathname}`;
const current = await env.RATE_LIMIT.get(key);
const count = current ? parseInt(current) : 0;
if (count >= limit) {
return new Response(
JSON.stringify({ error: 'Too many requests', path: url.pathname }),
{
status: 429,
headers: {
'Content-Type': 'application/json',
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': '0',
},
}
);
}
ctx.waitUntil(
env.RATE_LIMIT.put(key, (count + 1).toString(), {
expirationTtl: window,
})
);
return fetch(request);
},
};
Bot ve Scraper Tespiti
Rate limiting tek başına yeterli değil. Akıllı botlar limit altında kalarak scraping yapabilir. Bunları tespit etmek için ek sinyaller kullanmalısın.
function calculateBotScore(request) {
let score = 0;
const ua = request.headers.get('User-Agent') || '';
// User-Agent kontrolleri
const suspiciousUAs = [
/python-requests/i,
/curl//i,
/wget//i,
/scrapy/i,
/bot/i,
/crawler/i,
/spider/i,
/headless/i,
];
for (const pattern of suspiciousUAs) {
if (pattern.test(ua)) {
score += 30;
break;
}
}
// Boş veya çok kısa User-Agent
if (ua.length < 10) score += 25;
// Accept header yoksa şüpheli
if (!request.headers.get('Accept')) score += 15;
// Accept-Language yoksa şüpheli
if (!request.headers.get('Accept-Language')) score += 10;
// Cloudflare bot skoru varsa kullan
const cfBotScore = request.cf?.botManagement?.score;
if (cfBotScore !== undefined) {
if (cfBotScore < 30) score += 40;
else if (cfBotScore < 60) score += 20;
}
// Tor exit node kontrolü
if (request.cf?.isEUCountry === false && request.cf?.threatScore > 20) {
score += 15;
}
return score;
}
export default {
async fetch(request, env, ctx) {
const ip = request.headers.get('CF-Connecting-IP');
const botScore = calculateBotScore(request);
// Yüksek bot skoru olan trafiği daha sıkı limit
let limit = 100;
let windowSeconds = 60;
if (botScore >= 50) {
// Muhtemelen bot, çok sıkı limit
limit = 10;
windowSeconds = 300;
} else if (botScore >= 25) {
// Şüpheli, orta limit
limit = 30;
windowSeconds = 60;
}
const key = `bot_rl:${ip}`;
const current = await env.RATE_LIMIT.get(key);
const count = current ? parseInt(current) : 0;
if (count >= limit) {
// Bot trafiğini logla
ctx.waitUntil(
logBlockedRequest(env, {
ip,
botScore,
url: request.url,
ua: request.headers.get('User-Agent'),
timestamp: new Date().toISOString(),
})
);
return new Response('Access Denied', { status: 403 });
}
ctx.waitUntil(
env.RATE_LIMIT.put(key, (count + 1).toString(), {
expirationTtl: windowSeconds,
})
);
return fetch(request);
},
};
async function logBlockedRequest(env, data) {
// Logları KV'ye yaz (veya external logging servisine gönder)
const logKey = `blocked_log:${Date.now()}`;
await env.RATE_LIMIT.put(logKey, JSON.stringify(data), {
expirationTtl: 86400, // 24 saat sakla
});
}
IP Allowlist ve Denylist Yönetimi
Bazı IP’leri tamamen engellemek veya whitelist’e almak isteyebilirsin. Cloudflare Workers bunu dinamik olarak yapmanı sağlar.
async function checkIPLists(ip, env) {
// KV'den allowlist ve denylist'i al
const [allowlistData, denylistData] = await Promise.all([
env.RATE_LIMIT.get('allowlist', { type: 'json' }),
env.RATE_LIMIT.get('denylist', { type: 'json' }),
]);
const allowlist = allowlistData || [];
const denylist = denylistData || [];
// IP veya CIDR kontrolü
const isAllowed = allowlist.some(entry => ipMatchesCIDR(ip, entry));
const isDenied = denylist.some(entry => ipMatchesCIDR(ip, entry));
return { isAllowed, isDenied };
}
function ipToInt(ip) {
return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet), 0) >>> 0;
}
function ipMatchesCIDR(ip, cidr) {
if (!cidr.includes('/')) {
return ip === cidr;
}
const [network, bits] = cidr.split('/');
const mask = ~((1 << (32 - parseInt(bits))) - 1) >>> 0;
return (ipToInt(ip) & mask) === (ipToInt(network) & mask);
}
export default {
async fetch(request, env, ctx) {
const ip = request.headers.get('CF-Connecting-IP');
const { isAllowed, isDenied } = await checkIPLists(ip, env);
// Denylist'teki IP'yi direkt engelle
if (isDenied) {
return new Response('Forbidden', { status: 403 });
}
// Allowlist'teki IP'ye rate limit uygulama
if (isAllowed) {
return fetch(request);
}
// Normal rate limiting mantığı devam eder
const key = `rl:${ip}`;
const current = await env.RATE_LIMIT.get(key);
const count = current ? parseInt(current) : 0;
if (count >= 100) {
// Otomatik denylist'e ekle (çok fazla istek yapıyorsa)
if (count >= 500) {
ctx.waitUntil(addToDenylist(ip, env));
}
return new Response('Too Many Requests', { status: 429 });
}
ctx.waitUntil(
env.RATE_LIMIT.put(key, (count + 1).toString(), {
expirationTtl: 60,
})
);
return fetch(request);
},
};
async function addToDenylist(ip, env) {
const denylist = (await env.RATE_LIMIT.get('denylist', { type: 'json' })) || [];
if (!denylist.includes(ip)) {
denylist.push(ip);
await env.RATE_LIMIT.put('denylist', JSON.stringify(denylist));
console.log(`Auto-blocked IP: ${ip}`);
}
}
Denylist Yönetim Script’i
IP listelerini CLI’dan yönetmek için Wrangler kullanabilirsin:
# Wrangler kurulumu ve login
npm install -g wrangler
wrangler login
# KV namespace oluştur
wrangler kv:namespace create "RATE_LIMIT"
wrangler kv:namespace create "RATE_LIMIT" --preview
# Denylist'e IP ekle
wrangler kv:key put --namespace-id="YOUR_NS_ID" "denylist"
'["192.168.1.100", "10.0.0.0/8", "185.220.101.0/24"]'
# Allowlist ekle (örn. ofis IP'leri)
wrangler kv:key put --namespace-id="YOUR_NS_ID" "allowlist"
'["203.0.113.0/24", "198.51.100.50"]'
# Mevcut denylist'i görüntüle
wrangler kv:key get --namespace-id="YOUR_NS_ID" "denylist"
# Bir IP'nin kaç istek yaptığını kontrol et
wrangler kv:key get --namespace-id="YOUR_NS_ID" "rl:192.168.1.100"
# Worker'ı deploy et
wrangler deploy
# Logları izle (real-time)
wrangler tail
Token Bucket Algoritması
Bazı senaryolarda burst trafiğe izin vermek isteyebilirsin. Örneğin normal şartlarda dakikada 60 istek yapan bir kullanıcı, 2 dakika beklerse 120 istek burst yapabilmeli. Token Bucket bu durumu çözer.
const BUCKET_CAPACITY = 100; // Maximum token sayısı
const REFILL_RATE = 1; // Saniyede eklenecek token
const REFILL_INTERVAL = 1000; // millisecond
export default {
async fetch(request, env, ctx) {
const ip = request.headers.get('CF-Connecting-IP');
const key = `bucket:${ip}`;
const now = Date.now();
const stored = await env.RATE_LIMIT.get(key, { type: 'json' });
let tokens;
let lastRefill;
if (!stored) {
// İlk istek, bucket'ı dolu başlat
tokens = BUCKET_CAPACITY;
lastRefill = now;
} else {
tokens = stored.tokens;
lastRefill = stored.lastRefill;
// Geçen süreye göre token ekle
const elapsed = (now - lastRefill) / 1000; // saniye
const tokensToAdd = Math.floor(elapsed * REFILL_RATE);
if (tokensToAdd > 0) {
tokens = Math.min(BUCKET_CAPACITY, tokens + tokensToAdd);
lastRefill = now;
}
}
if (tokens < 1) {
// Token kalmadı, isteği reddet
const timeToRefill = Math.ceil((1 - tokens) / REFILL_RATE);
return new Response(
JSON.stringify({
error: 'Rate limit exceeded',
tokensRemaining: 0,
refillIn: timeToRefill,
}),
{
status: 429,
headers: {
'Content-Type': 'application/json',
'X-RateLimit-Remaining': '0',
'Retry-After': timeToRefill.toString(),
},
}
);
}
// Bir token harca
tokens -= 1;
ctx.waitUntil(
env.RATE_LIMIT.put(
key,
JSON.stringify({ tokens, lastRefill }),
{ expirationTtl: BUCKET_CAPACITY / REFILL_RATE + 60 }
)
);
const response = await fetch(request);
const newResponse = new Response(response.body, response);
newResponse.headers.set('X-RateLimit-Remaining', Math.floor(tokens).toString());
newResponse.headers.set('X-RateLimit-Limit', BUCKET_CAPACITY.toString());
return newResponse;
},
};
Wrangler Konfigürasyonu
Tüm bu Worker’ları deploy etmek için wrangler.toml dosyasını doğru yapılandırman gerekiyor:
# wrangler.toml örneği
cat > wrangler.toml << 'EOF'
name = "rate-limiter"
main = "src/index.js"
compatibility_date = "2024-01-01"
[vars]
ENVIRONMENT = "production"
[[kv_namespaces]]
binding = "RATE_LIMIT"
id = "your-production-kv-id"
preview_id = "your-preview-kv-id"
[triggers]
routes = [
{ pattern = "example.com/*", zone_name = "example.com" },
{ pattern = "api.example.com/*", zone_name = "example.com" }
]
# CPU limiti aşımını önlemek için
[limits]
cpu_ms = 50
EOF
# Geliştirme ortamında test et
wrangler dev --local
# Production'a deploy et
wrangler deploy
# Belirli bir ortama deploy
wrangler deploy --env production
# Worker metriklerini izle
wrangler metrics
Gerçek Dünya Senaryosu: API Güvenliği
Bir e-ticaret sitesinin API’sini düşün. Login brute force, product scraping ve checkout spam sorunlarıyla karşılaşıyorsun. İşte bütünleşik bir çözüm:
// Gerçek dünya: E-ticaret API koruma katmanı
const ENDPOINT_CONFIGS = {
'/api/v1/auth/login': {
limit: 5,
window: 300,
blockDuration: 3600,
message: 'Çok fazla giriş denemesi. 1 saat sonra tekrar deneyin.',
},
'/api/v1/checkout': {
limit: 10,
window: 60,
blockDuration: 300,
message: 'Çok fazla ödeme denemesi.',
},
'/api/v1/products': {
limit: 200,
window: 60,
blockDuration: 60,
message: 'API limiti aşıldı.',
},
'/api/v1/search': {
limit: 60,
window: 60,
blockDuration: 120,
message: 'Arama limiti aşıldı.',
},
};
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const ip = request.headers.get('CF-Connecting-IP');
const country = request.cf?.country || 'XX';
// Config'i bul
let config = null;
for (const [path, cfg] of Object.entries(ENDPOINT_CONFIGS)) {
if (url.pathname.startsWith(path)) {
config = { path, ...cfg };
break;
}
}
if (!config) {
return fetch(request);
}
// Aktif block kontrolü
const blockKey = `block:${ip}:${config.path}`;
const isBlocked = await env.RATE_LIMIT.get(blockKey);
if (isBlocked) {
const blockData = JSON.parse(isBlocked);
return new Response(
JSON.stringify({
error: config.message,
blockedAt: blockData.blockedAt,
reason: blockData.reason,
}),
{
status: 429,
headers: { 'Content-Type': 'application/json' },
}
);
}
// Rate limit kontrolü
const countKey = `count:${ip}:${config.path}`;
const current = await env.RATE_LIMIT.get(countKey);
const count = current ? parseInt(current) : 0;
if (count >= config.limit) {
// Block uygula
ctx.waitUntil(
env.RATE_LIMIT.put(
blockKey,
JSON.stringify({
blockedAt: new Date().toISOString(),
reason: `Exceeded ${config.limit} requests in ${config.window}s`,
ip,
country,
endpoint: config.path,
}),
{ expirationTtl: config.blockDuration }
)
);
return new Response(
JSON.stringify({ error: config.message }),
{
status: 429,
headers: { 'Content-Type': 'application/json' },
}
);
}
ctx.waitUntil(
env.RATE_LIMIT.put(countKey, (count + 1).toString(), {
expirationTtl: config.window,
})
);
const response = await fetch(request);
const newResponse = new Response(response.body, response);
newResponse.headers.set('X-RateLimit-Limit', config.limit.toString());
newResponse.headers.set(
'X-RateLimit-Remaining',
Math.max(0, config.limit - count - 1).toString()
);
return newResponse;
},
};
Monitoring ve Alerting
Rate limiting çalışıyor mu, ne kadar trafik engellendi gibi soruları yanıtlamak için metrics toplaman gerekiyor. Cloudflare Workers Logpush veya bir webhook ile bunu dışarıya taşıyabilirsin:
# Cloudflare API ile Worker loglarını sorgula
curl -X GET
"https://api.cloudflare.com/client/v4/accounts/{account_id}/workers/scripts/{script_name}/tail"
-H "Authorization: Bearer YOUR_API_TOKEN"
-H "Content-Type: application/json"
# Blocked IP sayısını KV'den çek
wrangler kv:key list --namespace-id="YOUR_NS_ID" --prefix="block:" |
python3 -c "import sys, json; data=json.load(sys.stdin); print(f'Toplam engellenmiş IP: {len(data["result"])}')"
# Son 1 saatteki engelleme loglarını listele
wrangler kv:key list --namespace-id="YOUR_NS_ID" --prefix="blocked_log:" | head -50
# Wrangler tail ile canlı log izleme (filtrelenmiş)
wrangler tail --format=pretty --search="Rate limit exceeded"
Yaygın Hatalar ve Çözümleri
Production’da karşılaşılan tipik sorunlar şunlar:
- KV Yazma Gecikmesi: KV eventually consistent’tır, yani aynı IP’den eş zamanlı istekler aynı sayacı okuyabilir. Bunun için Durable Objects kullanmak daha güvenilirdir ama maliyeti artırır.
- False Positive: Meşru kullanıcıların engellenmesi. Bunu önlemek için IP yerine JWT token veya API key bazlı rate limiting tercih et. Shared IP’ler (NAT arkası kurumsal ağlar gibi) sorun çıkarabilir.
- Worker CPU Limiti: Karmaşık bot tespit algoritmaları 50ms CPU limitini aşabilir. Sliding window yerine fixed window kullanmak veya hesaplamayı sadeleştirmek gerekebilir.
- KV Maliyet: Yüksek trafikli sitelerde KV okuma/yazma maliyetleri artabilir. Her istek için iki KV operasyonu yapıyorsan, Workers Paid planında KV fiyatlarını hesapla.
- HTTPS Olmayan Origin: Worker üzerinden geçen isteklerin origin’e yönlendirilmesinde SSL sertifika sorunlarıyla karşılaşabilirsin.
fetchçağrılarındacf: { ssl: true }seçeneğini kullan.
Sonuç
Cloudflare Workers ile rate limiting, sunucu tarafında herhangi bir değişiklik yapmadan edge seviyesinde güçlü bir güvenlik katmanı oluşturmana imkan tanıyor. Basit sabit pencere sayacından, token bucket algoritmasına ve akıllı bot tespitine kadar her seviyede uygulama yapabilirsin.
Başlangıç için önerilen yol şu şekilde olabilir: önce endpoint bazlı basit rate limiting ile başla, production’da gerçek trafiği izle, false positive’leri analiz et ve sonra daha gelişmiş algoritmalar ekle. Token bucket özellikle API’ler için, sliding window ise auth endpoint’leri için iyi bir tercih.
Unutma, rate limiting tek başına yeterli değil. Cloudflare WAF kuralları, bot management ve rate limiting birlikte kullanıldığında gerçek anlamda sağlam bir savunma elde edersin. Workers’ı diğer Cloudflare özellikleriyle entegre etmek, sysadmin olarak senin en büyük avantajın olacak.
