قواعد Cloudflare WAF المخصصة عبر API: إدارة CRUD كاملة من .NET 10

0 تعليق 1227 مشاهدة

الأمن السيبراني dotnet devops Cloudflare WAF API HttpClient

11 دقيقة قراءة 2112 كلمة

ملخص قصير

تتولّى هذه المقالة شرح كيفية إدارة قواعد Cloudflare Custom Rules من تطبيق .NET 10 عبر API v4، مع ضمان وجود أربع قواعد مدفوعة بالكود واستثناء يدوي واحد يُعاد ضبطها في كل تشغيل pod بفضل seed idempotent. الواجهة ICloudflareWafRulesService المدعومة بـ typed HttpClient وإعادة محاولة Polly لحالات 429 وstructured logging كفيلة بتجاوز فخ تحليل dynamic التقليدي. الكود الكامل واستراتيجية الاختبار وحوادث الإنتاج متوفّرة في الأقسام التالية، وهي مأخوذة من نظام إنتاج حيّ يخدم ثلاث zones متزامنة.

ملاحظة المؤلف: كل الكود والأمثلة في هذه المقالة مستقاة من نظام bilalkose.com.tr الحيّ. توجد حاليًا أربع قواعد مدفوعة بالكود واستثناء self-IP يدوي واحد نشط (اعتبارًا من 2026-05-12)؛ وعند إعادة تشغيل أيّ pod، يُشغّل WafRulesSeeder عمليةَ seed idempotent مدفوعة بـ diff قائم على MD5. وقد رسم Pillar 1 في قسم H2-3 هذا السطح بشكل سطحي، لذا تتعمّق المقالة الحالية في تفاصيل التنفيذ ومعالجة الأخطاء واستراتيجية الاختبار.


جدول المحتويات

  1. Custom Rules API: بنية من 3 طبقات
  2. ICloudflareWafRulesService: واجهة CRUD
  3. Seed Idempotent: تشغيل نفس السكربت 100 مرة
  4. WAF Rule Expression DSL: 6 مزالق
  5. معالجة الأخطاء: 429 + 401 + 5xx
  6. استراتيجية الاختبار: unit + integration + smoke
  7. الأسئلة المتكررة
  8. مقالات ذات صلة

تدفق CRUD لـ Cloudflare Custom Rules API: List ثم diff ثم Create أو Update أو Skip أو Delete مع structured audit log
تدفق CRUD لـ Cloudflare Custom Rules API: List ثم diff ثم Create أو Update أو Skip أو Delete مع structured audit log


Custom Rules API: بنية من 3 طبقات

تعدّ Cloudflare Custom Rules (التي كانت تُسمّى سابقًا «Firewall Rules») قواعدَ مرتبطة بـ zone، إذ يحتفظ كل نطاق بمجموعة قواعد خاصة به؛ فالخطة المجانية تتيح خمس قواعد، فيما ترفع خطة Pro الحدّ إلى خمس وعشرين قاعدة. ويستضيف إعدادنا ثلاث zones منفصلة (bilalkose.com وbilalkose.com.tr وmaya.bilalkose.com.tr)، ويحمل كل zone أربع قواعد مدفوعة بالكود إلى جانب استثناء يدوي واحد.

تتكوّن كل Custom Rule واحدة من ثلاثة مكونات:

  1. Filter expression: وهو DSL مخصص لـ Cloudflare، مثل (http.request.uri.path contains "/.env") or (lower(http.user_agent) contains "sqlmap")، ويطابق على IP وASN ودولة وheaders وpath وmethod وquery string.
  2. Action: أي ما يحدث عند المطابقة، ومن خياراته block وchallenge وjs_challenge وmanaged_challenge وlog وskip؛ فـ block هو الأقوى، في حين يكتفي log بتسجيل أثرٍ بصمت.
  3. Priority: قيمة integer، إذ تُطابَق القيمة الأصغر أوّلًا، وعند تساوي الأولوية يلجأ النظام إلى ترتيب التعريف.

أمّا نقطة نهاية API v4 فهي POST/GET/PUT/DELETE /zones/{zone_id}/firewall/rules. ولأغراض المصادقة، تظلّ التوصية بعد عام 2023 منصبّةً على API Token (بنطاق zone:waf:edit وzone:zone:read). في المقابل، يبدو أنّ Global API Key القديم قد دخل خانة DEPRECATED، إذ يحمل صلاحيات على مستوى الحساب ويفتح الباب أمام مخاطر blast-radius واسعة.

وقد أشار Pillar 1 إلى أنّ «إدارة قواعد WAF يدويًا محفوفة بالمخاطر». وتشرح هذه المقالة كيف ألغينا تلك المخاطر عبر ثلاثية واضحة: code-pushed seed، ثمّ idempotency، ثمّ audit log منظَّم.

لوحة Cloudflare ثم Security ثم WAF ثم Custom rules: خمس قواعد نشطة (أربع منها [Auto] وقاعدة [Manual] واحدة للاستثناء self-IP)


ICloudflareWafRulesService: واجهة CRUD

يتمحور التصميم حول واجهة واحدة فقط، إذ تتدفّق جميع عمليات Cloudflare WAF من خلالها، وهو ما يُبقي سطح الاعتماد ضيقًا ومألوفًا للمختبِر.

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 غير قابلة للتغيير، ويعود التابع بـ IReadOnlyList<>، بحيث لا يتمكّن الكود المُستدعِي من تعديل الحالة بطريق الخطأ. ويُستخدم حقل Description بوصفه مفتاحَ idempotency سيُعتمد عليه في القسم التالي.

تستخدم طبقة HTTP نمط typed HttpClient:

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 فيعتمد على System.Text.Json مع JsonNamingPolicy.SnakeCaseLower (.NET 8 فما فوق)، لأنّ Cloudflare API يتحدّث snake_case (zone_id وcreated_on)، وهو ما يُجنّبنا كتابة [JsonPropertyName] يدويًا على كل property. وتشترك كل استجابة من Cloudflare API في نفس الـ envelope:

public sealed record CloudflareApiResponse<T>(
    bool Success,
    IReadOnlyList<CloudflareError>? Errors,
    T? Result);

public sealed record CloudflareError(int Code, string Message);

عندما يكون Success: false، تكون قائمة Errors ممتلئة، وتقوم طبقة الخدمة برمي CloudflareApiException. ولا شكّ في أنّ نمط Result (Result.IsSuccess) جيّد أيضًا، غير أنّ exception-based flow يبدو لي أبسطَ خيارٍ لهذا API الداخلي الصغير، إذ نمتلك توافقًا واحدًا مع بقية الكود وتدفّقًا واضحًا للأخطاء عبر middleware موحَّد.

نتيجة استعلام SSMS مباشر لجدول AttackProbes آخر 20 صف: عندما يُشحن جدول WafRules في Sub-Faz B إلى الإنتاج، يتبع نفس نمط المخطط مع أعمدة Description وExpression وAction وPriority، وعناوين IP مُقنّعة في الـ octet الأخير


Seed Idempotent: تشغيل نفس السكربت 100 مرة

في أوّل إصدار، أعاد deploy واحد إنشاءَ القواعد الأربع مرّتين قبل أن نضيف فحص الـ description؛ ومنذ ذلك الحادث، أصبح المبدأ صارمًا: تشغيل نفس الـ seed مئة مرّة ينبغي أن يكون عملية بلا أثر (no-op)، فلا قاعدة تُكرَّر ولا قاعدة تُحذَف. وفي كل مرة يُعاد فيها تشغيل K8s pod، يعمل WafRulesSeeder بوصفه IHostedService يحرس هذا التعاقد.

يتكوّن النمط من خمس خطوات متتالية:

الخطوة الأولى: هوية قائمة على Description. يبدأ كل وصف لقاعدة مدفوعة بالكود بالبادئة [Auto] ، مثل [Auto] Block sensitive path scanners. ولا تتعارض هذه البادئة مع القواعد اليدوية، إذ تحمل قاعدة self-IP bypass الخاصة الوصفَ [Manual] Operator self-IP bypass، وهو ما يفصل المسارَين فصلًا حادًّا.

الخطوة الثانية: List + match. اسرد كل القواعد في الـ zone، ثمّ طابق الـ description ضد المجموعة الحالية:

var existing = await _service.ListAsync(zoneId, ct);
var existingByDesc = existing
    .Where(r => r.Description.StartsWith("[Auto] "))
    .ToDictionary(r => r.Description, r => r);

الخطوة الثالثة: Diff hash. احسب MD5 hash للثلاثي expression + action + priority للمواصفة الجديدة، ثمّ قارنه بـ hash القاعدة الموجودة:

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);
}

الخطوة الرابعة: Decide & execute. إذا تساوى الـ hash فالقرار SKIP، وإن لم تكن القاعدة موجودة أصلًا فالقرار CREATE، أمّا إذا اختلف الـ hash فالقرار UPDATE.

الخطوة الخامسة: Cleanup. احذف أيّ قاعدة بادئتها [Auto] غير موجودة في هذا الـ seed، فيما تُترك القواعد اليدوية ([Manual] أو التي بلا بادئة) دون أيّ مساس.

سجِّل كل إجراء بـ structured logging حتى يبقى المسار قابلًا للتدقيق لاحقًا:

_logger.LogInformation(
    "WafSeeder {Action}: ZoneId={ZoneId} RuleId={RuleId} Description={Description}",
    action, zoneId, ruleId, description);

وعند إعادة تشغيل الـ pod، تتدفّق هذه السجلات إلى Loki وPromtail، فتظهر إحصائيات الإجراءات في Grafana على هيئة لوحة قابلة للمتابعة بنظرة واحدة. وإذا كان Cloudflare API غير قابل للوصول أثناء الـ seed، فإنّ التطبيق لا يتعطّل، إذ يعمل بمنطق best-effort ويكتفي بإصدار تحذير، وهو ما يحمي مرحلة startup من فشل خارجي عابر.


WAF Rule Expression DSL: 6 مزالق

لغة expression لـ Cloudflare مشتقّة من مشروع wirefilter مفتوح المصدر، وتبدو معظم بنية الجملة فيها مألوفة للوهلة الأولى، غير أنّ مفاجآتها الصغيرة تعضّ عند أوّل اختبار حقيقي. وقد رصدنا في الإنتاج ستّةً من هذه المزالق، نوردها هنا بترتيب الخطورة الفعلية لا الترتيب النظري.

أوّلها أنّ عامل contains حسّاس لحالة الأحرف؛ فالتعبير http.user_agent contains "Curl" لن يطابق «curl» لأنّ Cloudflare لا يُطبّع النصّ تلقائيًا، والإصلاح الموثوق هو lower(http.user_agent) contains "curl". ربّما يبدو هذا تفصيلًا تافهًا، لكنّه السبب الأشيع في فشل قواعد UA filtering عند المراجعة الأولى.

المزلق الثاني يخصّ IPv6 الذي يجب أن يُحاط بعلامات اقتباس، بخلاف IPv4 الذي لا يحتاجها؛ فـ ip.src in {1.2.3.4} صحيح، في حين أنّ ip.src in {::1} غير صحيح، والشكل المطلوب هو ip.src in {"::1"} (علامات اقتباس مزدوجة إلزامية لـ IPv6). وفي القوائم المختلطة، يكون التقسيم إلى قاعدتين هو الخيار الأنظف عمليًّا.

أمّا المزلق الثالث فهو غياب wildcard glob؛ إذ لا يعمل النمط /admin/* بوصفه نمط URL، لأنّ Cloudflare يستخدم regex بدلًا من ذلك، فيكتب المرء http.request.uri.path matches "^/admin/". ولأغراض الأداء، يُفضَّل عامل starts_with على شاكلة starts_with(http.request.uri.path, "/admin/")، وقد لاحظنا فارقًا ملموسًا في latency على zones عالية الترافيك.

وفي المزلق الرابع تطلّ مشكلة حجم القوائم: قائمة ASN واحدة محدودة بمئة عنصر، بمعنى أنّ expression بصيغة ip.geoip.asnum in {15169 8075 ...} لا يقبل أكثر من 100 ASN. سجلّ DatacenterAsnRegistry بـ 40 ASN فأكثر يدخل قاعدة واحدة بسلاسة، أمّا للتوسّع فإنّ الحاجة تتّجه إلى CF Lists (وسنغطّيها بالتفصيل في cluster B2).

المزلق الخامس مرتبط بأسبقية not؛ فالكتابة not http.user_agent contains "Googlebot" تنطبق على كل UAs، وهو خطأ منطقي صرف، والإصلاح يكمن في إضافة قوسين صريحَين على هيئة (not (http.user_agent contains "Googlebot")). وفي أيّ شرط غير تافه، يُستحسن دائمًا اعتماد أقواس صريحة بدلًا من الرهان على ذاكرة المُحلِّل.

يبقى المزلق السادس وهو هروب السلسلة الحرفية. استخدم \" لعلامات الاقتباس المزدوجة، أو بدّلها إلى اقتباس مفرد. ويُكتب Newline بصيغة \n أو بشكل hex \x0a. أمّا Backslash نفسه فليس escape تلقائيًا، إذ يحتاج إلى \\؛ ومن ثَمّ فإنّ كتابة نمط regex لمطابقة نقطة حرفية تستدعي \\\\. بدلًا من \\.، وهي صياغة غير قابلة للقراءة، لذا انتبه عند مراجعة diff طويل.


معالجة الأخطاء: 429 + 401 + 5xx

Rate limit (429). يبلغ حدّ الـ rate limit لـ Cloudflare API لكل token نحو 1200 طلب كل 5 دقائق، وأثناء إعادة تشغيل pod نُجري أربع zones مضروبة في 5 list calls، أي 20 طلبًا فقط، وهو رقم مريح لا يستدعي قلقًا. ومع Polly نعتمد exponential backoff بترتيب: 1 ثانية ثمّ 2 ثمّ 4 ثمّ 8 (بحدّ أقصى 30 ثانية)؛ وإذا ورد header Retry-After في الاستجابة فاحترمه، وإلّا فالتزم بالتراجع الأسّي.

فشل المصادقة (401/403).

  • 401 Unauthorized: انتهت صلاحية token أو جرى تدويره، والحلّ هو fail-fast مع تسجيل ملاحظة «Vault token rotation needed».
  • 403 Forbidden: نطاق token غير كافٍ (مثلًا zone:waf:edit مفقود)، وهو خطأ deploy-time لا فائدة من إعادة المحاولة فيه.

Server error (5xx). عاملها بوصفها transient، وأعد المحاولة حتى 3 مرات، ثمّ سجّل النتيجة. وفي حال استمرار 5xx، استخدم Polly circuit breaker بنافذة bypass مدّتها 5 دقائق، إذ يرمي التطبيق CloudflareApiException دون أن يحجب startup، وهو ما يحفظ توفّر بقية الخدمات حين يكون CF نفسه في حالة degraded.

Idempotency-Key header. يدعمه Cloudflare API بوصفه اختياريًّا لكنّه موصى به، إذ يكفي استخدام GUID واحد لكل logical operation، ثمّ الإبقاء على نفس key على إعادات المحاولة، فيتمّ اكتشاف التكرار على جانب CF تلقائيًّا. وفي تدفقنا، يحمله CreateAsync فقط، لأنّ list وget وdelete idempotent بطبيعتها.


استراتيجية الاختبار: unit + integration + smoke

Unit (xUnit + Moq). اختبر diff logic الخاصّ بـ WafRulesSeeder ضدّ ICloudflareWafRulesService وهمي، بأربعة سيناريوهات لا غنى عنها:

  • سيناريو A: zone بلا قواعد، فالنتيجة المتوقّعة 4 Create.
  • سيناريو B: zone يحوي نفس الأربع قواعد (hash match)، فالنتيجة 0 إجراء.
  • سيناريو C: واحدة من القواعد الأربع غيّرت expression، فالنتيجة 3 Skip مع Update واحدة.
  • سيناريو D: zone يحوي قاعدة خامسة بادئتها [Auto] قديمة، فالنتيجة 4 Skip مع Delete واحدة.

Integration (WireMock.Net). أنشئ Mock لـ CF API، ثمّ اختبر pipeline HttpClient الحقيقي (بما في ذلك Polly retries) على سيناريوهات 429 و401 و5xx، إلى جانب اختبار replay لـ Idempotency-Key.

Smoke (production). حلّل سجلّ seed بعد startup الـ pod بصيغة Create:N, Update:M, Skip:K, Delete:L، ثمّ غذِّ هذه الأرقام إلى عدّاد Prometheus cf_waf_rules_seeded_total{action="create"}. ومن ذلك تُبنى تنبيهات Grafana، كأن يُعدّ معدّل فشل seed أعلى من %1 تنبيهًا أمنيًّا حرجًا من tier-1 يُرسَل فورًا عبر Telegram.

وتقع التكلفة الإجمالية للاختبار في حدود 120 سطرًا من xUnit إلى جانب 80 سطرًا من إعداد WireMock، فيما يكتمل CI في نحو ثماني ثوانٍ فقط.


الأسئلة المتكررة

كم قاعدة مخصصة أحصل عليها في الخطة المجانية؟

تمنحك الخطة المجانية خمس قواعد مخصصة، بينما ترفع خطة Pro الحدّ إلى خمس وعشرين قاعدة؛ ومع وجود أربع قواعد آلية واستثناء يدوي واحد، تبقى الخطة المجانية كافية لمعظم حالات الاستخدام المتوسطة. وعند الحاجة إلى مزيد من القواعد، يمكن اللجوء إلى CF Lists (10K IP لكل قائمة مضروبة في 10 قوائم لكل حساب، أي طاقة قصوى تبلغ 100K) لأنّها تتوسّع داخل قاعدة واحدة دون استهلاك حصّة قواعد إضافية.

API Token مقابل Global API Key: أيّهما يجب أن أستخدم؟

الإجابة الأقصر هي Token، محدَّد النطاق بـ zone:waf:edit وzone:zone:read. أمّا Global Key فهو DEPRECATED بعد عام 2023، ويعمل على مستوى الحساب بأكمله؛ وإذا تسرّب فالحساب كلّه في خطر فوري. وبالمقابل، تتميّز Tokens بكونها قابلة للإلغاء ومحدودة النطاق ومُسجَّلة في audit log، وهي خصائص ثلاث لا غنى عنها لأيّ بيئة إنتاج جدّية.

كم سرعة تفعيل تغيير WAF Custom Rule؟

عمومًا بين 5 و10 ثوانٍ، فيما يجري الانتشار العالمي تحت 30 ثانية؛ كما تتصرّف تعديلات dashboard اليدوية بالطريقة نفسها تمامًا.

ماذا لو شغّل اثنان من pods عملية seed idempotent في الوقت نفسه؟

يعيش السباق على CF API نفسه، إذ يحصل الـ pod الثاني على 409 Conflict ويسقط بأمان. ولأجل cluster lock صلب، يمكن إضافة IDistributedLock مدعومًا بـ Redis، غير أنّ ذلك قد يكون مبالغًا فيه في حجم نظامنا، فأربع zones وخمس قواعد لكلٍّ منها عمليةٌ صغيرة بنافذة سباق لا تتجاوز الميلي ثانية.


خلاصة

اجتماع ثلاثة عناصر هو ما حوّل إدارة WAF عندنا من مصدر قلق إلى مسار قابل للتدقيق: واجهة ICloudflareWafRulesService ضيّقة وقابلة للاختبار، وseed idempotent يحرس تعاقد «أربع قواعد + استثناء يدوي» في كل تشغيل pod، وحدّ rate-limit مريح بقيمة 1200 طلب كل 5 دقائق يمنح هامشًا واسعًا لإعادة المحاولة عبر Polly. ويُكمل هذا المسار في cluster B2 جدولُ IP Lists لرفع السعة إلى 100K، بينما يضيف cluster C7 حلقةَ TrafficClassifier بحيث يصبح الحظر مدفوعًا بإشارات السلوك لا بالقوائم الثابتة وحدها.


مقالات ذات صلة

التعليقات (0)

اترك تعليقاً وتقييماً

لا توجد تعليقات بعد. كن أول من يعلق.