TL;DR
Cloudflare Custom Rules API v4 ile .NET 10'dan WAF kurallarını CRUD eder, idempotent seed script'iyle pod restart'ında aynı 4 rule + 1 manual exception'ı garantilersin.
ICloudflareWafRulesServicearayüzüHttpClient+ 429 rate limit retry + structured logging ile gelenekseldynamicparse'tan kaçar. Tam kod, test stratejisi ve production fiyaskoları aşağıda — Bilal'in canlı sisteminden.
Yazar notu: Bu yazıdaki kod ve örnekler bilalkose.com.tr canlı sistemden. 4 code-pushed rule + 1 manual self-IP bypass aktif (2026-05-12 itibariyle), pod restart'ında WafRulesSeeder MD5-bazlı diff ile idempotent seed çalıştırıyor. Pillar 1 H2-3'te yüzeysel anlatılmıştı; bu yazıda implementation detayı + error handling + test stratejisi.
İçindekiler
- Custom Rules API: 3-katmanlı yapı
ICloudflareWafRulesService: CRUD arayüzü- Idempotent seed: aynı script 100 kez koşulsa
- WAF Rule Expression DSL: 6 tuzak
- Error handling: 429 + 401 + 5xx
- Test stratejisi: unit + integration + smoke
- Sıkça Sorulan Sorular
- İlgili Yazılar
Custom Rules API: 3-Katmanlı Yapı
Cloudflare Custom Rules (eski adıyla "Firewall Rules") zone bazlı çalışır: domain başına ayrı kural seti tutarsın. Free plan 5 rule, Pro plan 25 rule veriyor. Bizim setup'ta bilalkose.com + bilalkose.com.tr + maya.bilalkose.com.tr için 3 ayrı zone var; her zone kendi 4 code-pushed rule'unu + 1 manual exception'ı taşıyor.
Bir Custom Rule üç parçadan oluşur:
- Filter expression — CF özel DSL'i:
(http.request.uri.path contains "/.env") or (lower(http.user_agent) contains "sqlmap")gibi koşullar. IP, ASN, country, header, path, method, query string üzerinde match yapar. - Action — eşleşmede ne yapılacağı:
block,challenge,js_challenge,managed_challenge,log,skip. Block en sert, log sessizce iz tutar. - Priority — integer; küçük değer önce eşleşir. Aynı priority'lerde tanım sırası baz alınır.
API endpoint'i v4: POST/GET/PUT/DELETE /zones/{zone_id}/firewall/rules. Auth için 2023+ önerilen API Token (scope: zone:waf:edit + zone:zone:read); eski Global API Key DEPRECATED — account-wide tam yetki riski.
Pillar 1'de "WAF rule'ları manuel yönetmek riskli" demiştik. Bu yazının asıl konusu o riskin nasıl elimine edildiği: kod-pushed seed + idempotency + audit log.
![Cloudflare Dashboard → Security → WAF → Custom rules: 5 kural canlı listede (4 [Auto] + 1 [Manual] self-IP bypass)](/uploads/blog/2026-05/b1-intro-cf-custom-rules.png)
ICloudflareWafRulesService: CRUD Arayüzü
Tasarımın özü tek arayüz: tüm Cloudflare WAF işlemleri buradan akar.
public interface ICloudflareWafRulesService
{
Task<IReadOnlyList<WafRule>> ListAsync(string zoneId, CancellationToken ct);
Task<WafRule?> GetAsync(string zoneId, string ruleId, CancellationToken ct);
Task<WafRule> CreateAsync(string zoneId, WafRuleSpec spec, CancellationToken ct);
Task<WafRule> UpdateAsync(string zoneId, string ruleId, WafRuleSpec spec, CancellationToken ct);
Task DeleteAsync(string zoneId, string ruleId, CancellationToken ct);
}
public sealed record WafRuleSpec(
string Description,
string Expression,
WafRuleAction Action,
int Priority,
bool Paused = false);
public enum WafRuleAction { Block, Challenge, JsChallenge, ManagedChallenge, Log, Skip }
record immutable spec + IReadOnlyList<> döndürme: çağıran kod state mutasyon yapamaz. Description field'ı sonraki bölümde idempotency anahtarı olarak kullanılacak.
HTTP client tarafı typed HttpClient pattern'i:
services.AddHttpClient<ICloudflareApiClient, CloudflareApiClient>(client =>
{
client.BaseAddress = new Uri("https://api.cloudflare.com/client/v4/");
var token = configuration["CloudflareApi:ApiToken"]
?? throw new InvalidOperationException("CloudflareApi:ApiToken not configured");
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
client.Timeout = TimeSpan.FromSeconds(30);
})
.AddPolicyHandler(GetRetryPolicy());
JSON serileştirme için System.Text.Json + JsonNamingPolicy.SnakeCaseLower (.NET 8+). Cloudflare API snake_case kullanıyor (zone_id, created_on); manual [JsonPropertyName] her property'ye yazmaktan kaçınıyoruz.
Tüm CF API yanıtları aynı envelope'ta gelir:
public sealed record CloudflareApiResponse<T>(
bool Success,
IReadOnlyList<CloudflareError>? Errors,
T? Result);
public sealed record CloudflareError(int Code, string Message);
Success: false ise Errors doludur; service katmanı bunu CloudflareApiException olarak fırlatır. Caller'lar try/catch yerine Result.IsSuccess pattern'i de tercih edebilirdi, ama bu küçük internal API için exception-based flow yeterli sade.

Idempotent Seed: Aynı Script 100 Kez Koşulsa
K8s pod restart'ında app her yeniden ayağa kalktığında WafRulesSeeder IHostedService olarak tetikleniyor. Aynı seed'in 100 kez koşması zararlı OLMAMALI — kural ne kopyalanmalı ne silinmelidir.
Pattern beş adımdan oluşur:
Adım 1 — Description-based identity. Her code-pushed kuralın description'ı [Auto] prefix'iyle başlar: [Auto] Block sensitive path scanners. Bu prefix manuel kurallarla çakışmaz (Bilal'in self-IP bypass kuralı [Manual] Bilal self-IP bypass taşıyor).
Adım 2 — List + match. Mevcut zone'daki tüm kuralları listele, description match'le mevcut kuralı bul:
var existing = await _service.ListAsync(zoneId, ct);
var existingByDesc = existing
.Where(r => r.Description.StartsWith("[Auto] "))
.ToDictionary(r => r.Description, r => r);
Adım 3 — Diff hash. Yeni spec'in expression + action + priority üçlüsünden MD5 hash al, mevcut kuralın hash'iyle karşılaştır.
private static string ComputeRuleHash(WafRuleSpec spec)
{
var raw = $"{spec.Expression}|{spec.Action}|{spec.Priority}";
var bytes = MD5.HashData(Encoding.UTF8.GetBytes(raw));
return Convert.ToHexString(bytes);
}
Adım 4 — Decide & execute. Eşitse SKIP, mevcut yoksa CREATE, diff varsa UPDATE.
Adım 5 — Cleanup. Bu seed'de yer almayan [Auto] prefix'li kuralları DELETE et. Manuel kurallar ([Manual] veya prefix'siz) dokunulmaz.
Her aksiyonu structured log'la:
_logger.LogInformation(
"WafSeeder {Action}: ZoneId={ZoneId} RuleId={RuleId} Description={Description}",
action, zoneId, ruleId, description);
Pod restart'larında bu log'lar Loki/Promtail'e gider, Grafana'da aksiyon istatistiği görünür. Seed run sırasında Cloudflare API erişilmezse app crash olmaz — best-effort, sadece warning log.
WAF Rule Expression DSL: 6 Tuzak
Cloudflare'in expression dili wirefilter projesinden türemiş. Çoğu syntax tanıdık görünür ama küçük sürprizler var. Production'da bizi yakalayan 6 tuzak:
Tuzak 1 — contains case-sensitive. http.user_agent contains "Curl" yazarsan "curl" string'i tutmaz. CF lowercase normalize yapmıyor. Fix: lower(http.user_agent) contains "curl".
Tuzak 2 — IPv6 quoted, IPv4 değil. ip.src in {1.2.3.4} ✓ ama ip.src in {::1} ✗. Doğru: ip.src in {"::1"} (double quote zorunlu IPv6'da). Karışık listede ayrı 2 rule yazmak en temizi.
Tuzak 3 — Wildcard glob yok. /admin/* URL pattern olarak çalışmaz. CF regex kullanır: http.request.uri.path matches "^/admin/". Performans için starts_with operatörü tercih edilebilir: starts_with(http.request.uri.path, "/admin/").
Tuzak 4 — ASN listesi 100 ile sınırlı. Tek expression'da ip.geoip.asnum in {15169 8075 ...} formatı maksimum 100 ASN alır. 40+ ASN'lik DatacenterAsnRegistry'mizi tek rule'a sığdırıyoruz, ama scale'de CF Lists feature gerekir (B2 cluster konusu).
Tuzak 5 — not precedence. not http.user_agent contains "Googlebot" ifadesi tüm UA'larda etkili — yanlış. Fix: parantez ile bağla (not (http.user_agent contains "Googlebot")). Karmaşık koşullarda her zaman explicit parantez kullan.
Tuzak 6 — String literal escape. Çift tırnak için \" veya alternatif tek tırnak. Newline için \n veya hex \x0a. Backslash kendi başına escape değil; \\ gerek. Production'da regex pattern içine \\. koymak yerine \\\\. yazmak gerekir — okunmaz, dikkatli ol.
Error Handling: 429 + 401 + 5xx
Rate limit (429). Cloudflare API rate limit'i token başına 1200 request / 5 dakika. Pod restart'larda 4 zone × 5 list call = 20 request, problem değil. Polly ile exponential backoff: 1sn, 2sn, 4sn, 8sn (max 30sn). Retry-After header varsa onu kullan, yoksa exponential.
Auth failure (401/403).
401 Unauthorized— token süresi dolmuş veya rotate edilmiş. Fail-fast, app log'la "Vault token rotation needed".403 Forbidden— token scope yetersiz (örnzone:waf:editeksik). Bu deploy-time hata, retry mantıksız.
Server error (5xx). Transient olarak 3 kez retry et, sonra error log. Persistent 5xx için Polly circuit breaker: 5 dakikalık bypass penceresi açarsın, app CloudflareApiException fırlatır ama startup blocklenmez.
Idempotency-Key header. CF API destekliyor, opsiyonel ama recommended. Her logical operation için GUID, retry'larda aynı key — CF tarafında duplicate detection. Bizim flow'da CreateAsync çağrılarına ekliyoruz, list/get/delete idempotent zaten.
Test Stratejisi: Unit + Integration + Smoke
Unit (xUnit + Moq). WafRulesSeeder'ın diff logic'ini fake ICloudflareWafRulesService ile test et:
- Senaryo A: zone'da hiç rule yok → 4 Create
- Senaryo B: zone'da aynı 4 rule var (hash match) → 0 aksiyon
- Senaryo C: 4 rule'dan 1'inin expression'ı değişti → 3 Skip + 1 Update
- Senaryo D: zone'da 5. eski
[Auto]rule var → 4 Skip + 1 Delete
Integration (WireMock.Net). CF API'yi mock'la, gerçek HttpClient pipeline'ı (Polly retry dahil) test et. 429 + 401 + 5xx senaryoları + Idempotency-Key replay testi.
Smoke (production). Pod startup sonrası seed log'unu parse et: Create:N, Update:M, Skip:K, Delete:L formatını Prometheus counter'a çevir: cf_waf_rules_seeded_total{action="create"}. Grafana alert: seed fail rate > %1 → Telegram'a tier-1 critical security uyarısı.
Toplam test maliyeti: ~120 satır xUnit + ~80 satır WireMock setup. CI'da 8 saniyede koşar.
Sıkça Sorulan Sorular
Free plan'da kaç custom rule olur?
- Pro plan 25. Otomatik seed 4 rule + 1 manual exception kullanıyorsa free plan yetiyor. Daha fazla rule gerektiğinde CF Lists (10K IP/list × 10 list/account = 100K kapasite) tek rule içinde scale eder.
API Token vs Global API Key — hangisini kullanmalıyım?
Token (scope: zone:waf:edit + zone:zone:read). Global Key DEPRECATED 2023+, account-wide tam yetki — token leak olursa tüm hesap riske girer. Token'lar revoke edilebilir + scope-bounded + audit log'lu.
WAF Custom Rule değişikliği ne kadar sürede aktif?
Genelde 5-10 saniye. Worldwide propagation 30 saniye altı. CF dashboard'tan manuel edit yapmak da aynı süre alır.
Idempotent seed iki pod aynı anda koşarsa ne olur?
Race condition CF API tarafında; ikinci pod 409 Conflict alır. Cluster lock için IDistributedLock (Redis-bound) eklenebilir, ama küçük scale'de gerek yok — 4 zone × 5 rule küçük operation, race penceresi milisaniyeler.
İlgili Yazılar
- Pillar 1 — Cloudflare Edge Security ve .NET (cross-link back)
- B2 — Cloudflare IP List API ile 10K IP push (B1'in scale follow-up'ı)
- B3 (gelecek) — ASN tabanlı bot tespit + DatacenterAsnRegistry derinleştirme
- C7 (gelecek) — Origin block loop: TrafficClassifier → IpListService
Yorumlar (0)
Henüz yorum yapılmamış. İlk yorumu sen bırak.