Cloudflare WAF API Custom Rules: .NET 10'dan Tam CRUD Yönetimi

0 yorum 1226 görüntülenme

Siber Güvenlik dotnet devops Cloudflare WAF API HttpClient

8 dk okuma 1439 kelime

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. ICloudflareWafRulesService arayüzü HttpClient + 429 rate limit retry + structured logging ile geleneksel dynamic parse'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

  1. Custom Rules API: 3-katmanlı yapı
  2. ICloudflareWafRulesService: CRUD arayüzü
  3. Idempotent seed: aynı script 100 kez koşulsa
  4. WAF Rule Expression DSL: 6 tuzak
  5. Error handling: 429 + 401 + 5xx
  6. Test stratejisi: unit + integration + smoke
  7. Sıkça Sorulan Sorular
  8. İlgili Yazılar

Cloudflare Custom Rules API CRUD flow: List → diff → Create/Update/Skip/Delete pipeline + structured audit log
Cloudflare Custom Rules API CRUD flow: List → diff → Create/Update/Skip/Delete pipeline + structured audit log


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:

  1. 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.
  2. Action — eşleşmede ne yapılacağı: block, challenge, js_challenge, managed_challenge, log, skip. Block en sert, log sessizce iz tutar.
  3. 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)


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.

SSMS canlı sorgu sonucu — AttackProbes son 20 satır: WafRules tablosu Sub-Faz B prod'a geçtiğinde aynı şema deseniyle Description + Expression + Action + Priority kolonlarını döner; IP'ler son 2 octet maskeli


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 (örn zone:waf:edit eksik). 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?

  1. 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

Yorumlar (0)

Yorum ve puan bırakın

Henüz yorum yapılmamış. İlk yorumu sen bırak.