الامتثال والسياسات ككود

Open Policy Agent ولغة Rego

18 دقيقة الدرس 4 من 27

Open Policy Agent ولغة Rego

يُعدّ Open Policy Agent (OPA) المعيار الفعلي لتطبيق السياسات عبر المنظومة السحابية الأصيلة. بُني في الأصل بواسطة Styra ثم أُهدي إلى CNCF، وحقّق مستوى الجاهزية الإنتاجية عام 2021، وبات اليوم مدمجًا داخل وحدات تحكم استقبال Kubernetes (Gatekeeper)، وشبكات الخدمات (تكامل Istio AuthorizationPolicy)، وبوابات API (Kong وEnvoy ext_authz)، والتحقق من صحة خطط Terraform (Conftest)، وعشرات المنتجات التجارية. الفكرة المحورية هي السياسة المنفصلة: بدلًا من تضمين منطق الوصول في كل خدمة، تدفع جميع القرارات إلى محرك واحد قابل للنشر المستقل يتحدث لغة واحدة — Rego.

OPA محرك سياسة للأغراض العامة، وليس أداة Kubernetes. يُقيِّم أي مدخل JSON مقابل أي سياسة Rego ويُعيد أي مخرج JSON. التحكم في استقبال Kubernetes هو أبرز حالات الاستخدام، لكن OPA يحكم أيضًا تخويل API، وإخفاء البيانات، وبوابة خط أنابيب CI، والوصول عبر SSH، وتوفير حسابات السحابة — في أي مكان تحتاج فيه قواعد قابلة للبرمجة وقابلة للتدقيق تعيش خارج كود تطبيقك.

معمارية OPA

يعمل OPA كعملية مصاحبة (sidecar) أو خدمة مستقلة. يرسل المستهلكون استعلامًا JSON (ماذا تريد أن تعرف؟) مع مدخل JSON (الشيء الذي يُقيَّم). يُقيِّم OPA الاستعلام مقابل حزمة السياسات المحمّلة ووثيقة البيانات الاختيارية (السياق مثل أدوار المستخدمين أو القوائم المسموح بها)، ثم يُعيد قرارًا JSON. لا شيء في هذا التبادل خاص بـ Kubernetes.

OPA Architecture: Input → Policy Engine → Decision OPA Architecture: Decoupled Policy Evaluation Caller Service / Webhook OPA Engine Policy Bundle Rego rules (git-versioned) Data Document Roles, allowlists, context Rego Evaluator Decision allow / deny + reason Bundle Store OCI / HTTPS — hot-reload (no restart) input JSON decision JSON bundle pull
تفصل معمارية OPA قرارات السياسة عن كود التطبيق. يرسل المستدعي وثيقة مدخل؛ يُقيِّمها OPA مقابل قواعد Rego والبيانات الخارجية، ثم يُعيد قرارًا منظمًا. تُعاد تحميل الحزم تلقائيًا — لا تحتاج تحديثات السياسة إلى إعادة تشغيل OPA.

ثلاثة أنماط نشر تغطي جميع حالات الاستخدام الإنتاجية تقريبًا:

  • Sidecar: يعمل OPA كحاوية مصاحبة لكل pod تطبيق. كمون منخفض (loopback)، نطاق تأثير معزول، تكلفة موارد أعلى قليلًا. تستخدمه Envoy ext_authz وLinkerd وConsul.
  • Admission webhook (Gatekeeper): يعمل OPA داخل مستوى التحكم في Kubernetes كـ ValidatingAdmissionWebhook. تُقيَّم كل عملية كتابة إلى خادم API بشكل متزامن قبل استمرار المورد. هذه نقطة التطبيق الأساسية في Kubernetes.
  • Daemon / مستقل: خدمة OPA مركزية تُقيِّم استعلامات من مستدعين متعددين. مناسب عندما تريد سجل قرار واحد قابل للتدقيق. يجب التعامل معه كتبعية في المسار الحرج: إن توقف، يفشل كل مستدعٍ إما مفتوحًا أو مغلقًا وفقًا للتهيئة.

Rego: لغة السياسة

Rego لغة تصريحية قائمة على المنطق، مصممة خصيصًا للسياسات. ليست أمرية — لا تكتب أشجار if/else وتُغيِّر الحالة. بدلًا من ذلك، تكتب علاقات منطقية يحلّها مُقيِّم Rego إلى قيمة. النموذج أقرب إلى Datalog (مجموعة فرعية من Prolog)، مما يجعل Rego غريبًا على المهندسين الذين كتبوا كودًا إجرائيًا فحسب. العائد هو أن سياسات Rego متسقة قابلة للإثبات: نفس المدخل يُنتج دائمًا نفس المخرج، ويستطيع المُقيِّم أن يشرح بالضبط سبب الوصول إلى قرار.

أساسيات Rego: القواعد، المراجع، التحليلات

ملف Rego هو وحدة (module) بتصريح package. القواعد هي معادلات. جسم القاعدة مجموعة تعبيرات يجب أن تكون صحيحة جميعًا (منطق عطفي — AND). قواعد متعددة بنفس الرأس تمثّل بدائل (منطق فصلي — OR). تستعلم عن قيمة عبر النقطة على شجرة المستند (input.spec.containers[_]).

# ── أساسيات Rego: package، قواعد، دوال مدمجة ── # الملف: policy/require-labels.rego package kubernetes.admission import future.keywords.if import future.keywords.in # القرار الافتراضي: لا مخالفة (مجموعة فارغة) default deny = false # القاعدة: يجب أن يحمل كل Deployment وسم "team" deny if { input.request.kind.kind == "Deployment" not input.request.object.metadata.labels.team } # القاعدة: يجب أن تأتي كل صورة حاوية من سجل معتمد deny if { some container in input.request.object.spec.containers not startswith(container.image, "registry.corp.internal/") } # قاعدة مركّبة: تُنتج مجموعة رسائل مخالفة (نمط Gatekeeper) violation[{"msg": msg}] if { some container in input.request.object.spec.containers not startswith(container.image, "registry.corp.internal/") msg := sprintf("Container %v uses unapproved registry: %v", [container.name, container.image]) }

مصطلحات Rego الأساسية التي يجب أن يعرفها كل مهندس يعمل مع OPA:

  • المُكرِّر الحرفي [_]: يُكرِّر على جميع عناصر المصفوفة. input.spec.containers[_].image صحيح لأي حاوية تستوفي بقية القاعدة.
  • some x in collection: البديل الاصطلاحي الحديث (يتطلب import future.keywords.in). يُفضَّل على [_] في السياسات الجديدة لوضوحه.
  • النفي not: صحيح عندما يكون التعبير في الجسم غير محدد أو خاطئ. not input.metadata.labels.team صحيح عندما يكون وسم team غائبًا — لكن أيضًا عندما يُضبَط صراحةً على null. افهم هذا قبل كتابة قواعد أمنية.
  • القواعد الافتراضية: يوفر default allow = false قيمة احتياطية آمنة للقواعد الجزئية. أعلن دائمًا عن الافتراضيات في القواعد المتعلقة بالأمان.
  • القواعد الجزئية والتحليلات: violation[msg] { ... } يبني مجموعة من جميع الرسائل المطابقة. نمط Gatekeeper يعتمد على هذا — يجمع كل المخالفات من مورد في استعلام واحد بدلًا من التوقف عند الأولى.

كتابة سياسة بجودة إنتاجية

تُطبِّق السياسة التالية ثلاثة ضوابط Kubernetes شائعة في وحدة واحدة: الوسوم المطلوبة، وسجلات الصور المحظورة، وغياب حاويات المستخدم الجذر. هذا مثل تمثيلي لما يشحنه فريق منصة حقيقي.

# الملف: policy/platform-baseline.rego # يُطبِّق: (1) الوسوم المطلوبة، (2) السجلات المعتمدة، (3) عدم استخدام UID الجذر package platform.admission.baseline import future.keywords.if import future.keywords.in required_labels := {"team", "env", "version"} approved_registry := "registry.corp.internal/" # ── 1. الوسوم المطلوبة ─────────────────────────────────────────────────────── violation[{"msg": msg, "field": "metadata.labels"}] if { some lbl in required_labels not input.review.object.metadata.labels[lbl] msg := sprintf("Missing required label: %v", [lbl]) } # ── 2. سجل الصور المعتمد ────────────────────────────────────────────────────── violation[{"msg": msg, "field": "spec.containers"}] if { some c in input.review.object.spec.containers not startswith(c.image, approved_registry) msg := sprintf("Image %v not from approved registry %v", [c.image, approved_registry]) } # ── 3. لا حاوية تعمل بـ UID 0 ───────────────────────────────────────────────── violation[{"msg": msg, "field": "spec.securityContext"}] if { some c in input.review.object.spec.containers c.securityContext.runAsUser == 0 msg := sprintf("Container %v must not run as UID 0 (root)", [c.name]) } # التحقق أيضًا من سياق أمان Pod المستوى violation[{"msg": msg, "field": "spec.securityContext"}] if { input.review.object.spec.securityContext.runAsUser == 0 msg := "Pod-level securityContext.runAsUser must not be 0" }

اختبار سياسات Rego بـ opa test

سياسة Rego دون اختبارات هي مسؤولية لا أصل. تشحن OPA مشغّل اختبار مدمجًا. تعيش ملفات الاختبار إلى جانب ملفات السياسة وتتبع اصطلاح التسمية *_test.rego. الاختبارات قواعد Rego عادية تبدأ أسماؤها بـ test_. أي اختبار يُقيَّم بـ false أو غير محدد يُعدّ فشلًا.

# الملف: policy/platform-baseline_test.rego package platform.admission.baseline # مساعد: بناء وثيقة مراجعة Pod دنيا make_pod(containers) := { "review": { "object": { "metadata": { "labels": {"team": "platform", "env": "prod", "version": "v1.2"} }, "spec": { "containers": containers, "securityContext": {} } } } } # نجاح: pod متوافق — صفر مخالفات متوقعة test_compliant_pod_passes if { containers := [{"name": "app", "image": "registry.corp.internal/myapp:1.0", "securityContext": {"runAsUser": 1000}}] count(violation) == 0 with input as make_pod(containers) } # فشل: سجل غير معتمد — مخالفة واحدة متوقعة test_unapproved_registry_fails if { containers := [{"name": "app", "image": "docker.io/nginx:latest", "securityContext": {"runAsUser": 1000}}] count(violation) == 1 with input as make_pod(containers) } # فشل: UID الجذر — مخالفة واحدة متوقعة test_root_uid_fails if { containers := [{"name": "app", "image": "registry.corp.internal/myapp:1.0", "securityContext": {"runAsUser": 0}}] count(violation) == 1 with input as make_pod(containers) } # تشغيل: opa test ./policy/ -v
شغِّل opa eval بشكل تفاعلي أثناء تطوير السياسة. استخدم opa eval -d policy/ -i input.json 'data.platform.admission.baseline.violation' لترى بالضبط ما تُعيده سياستك لمدخل معين قبل نشرها. الراية --explain full تطبع تتبع التقييم الكامل — لا غنى عنها عند تصحيح قاعدة تعمل بشكل غير متوقع أو لا تعمل أصلًا. على النطاق الواسع، تُضيف Styra DAS وإضافة OPA لـ VS Code تحليل تغطية على مستوى IDE.

توزيع حزم OPA

في الإنتاج، لا تُدمج السياسات في ثنائي OPA. بل تُوزَّع كـ حزم — أرشيفات tar.gz من ملفات .rego ووثائق data.json — تُدفع إلى سجل OCI أو نقطة نهاية HTTPS. يستطلع OPA نقطة نهاية الحزمة على فترة زمنية مُهيَّأة ويُعيد تحميل السياسات دون إعادة تشغيل. هذا ما يجعل تحديثات السياسة عملية CI: دمج إلى main، يبني CI الحزمة الجديدة ويدفعها، تلتقطها مجموعات OPA في غضون ثوانٍ.

# opa.yaml — ملف تهيئة OPA (sidecar أو مستقل) services: bundle-server: url: https://bundles.corp.internal credentials: bearer: token_path: /var/run/secrets/bundle-token bundles: platform-baseline: service: bundle-server resource: /bundles/platform-baseline.tar.gz polling: min_delay_seconds: 30 max_delay_seconds: 120 decision_logs: console: true # إرسال سجلات القرار المنظمة إلى stdout لـ Fluentd reporting: min_delay_seconds: 5 # دُفعات وإرسال القرارات إلى متلقٍّ مركزي max_delay_seconds: 15
فخ "undefined = allow". في Rego، إن لم تُطابَق قاعدة أبدًا، تكون النتيجة غير محددة، وهو مختلف عن false. المستدعون الذين يعاملون undefined كـ allow (شائع في التكاملات المكتوبة يدويًا) يخلقون ثغرة تجاوز: مدخل مشوّه يفشل في مطابقة أي قاعدة سيُسمح به بصمت. استخدم دائمًا default allow = false أو هيِّئ تكاملك ليعامل القرارات غير المحددة كرفض. تعالج Gatekeeper هذا بشكل صحيح؛ تحقق مضاعفًا من أي مستدعٍ لـ OPA REST API المخصص تكتبه.

OPA في مسار استقبال Kubernetes

عند نشر Gatekeeper (الدرس التالي)، يكون OPA محرك التقييم خلف الكواليس. فهم دورة الاستعلام/الاستجابة الخام لـ OPA أمر جوهري لتصحيح إخفاقات Gatekeeper، وكتابة قوالب قيود مخصصة، وتكامل OPA مع أنظمة غير Kubernetes. يُرسل خادم Kubernetes API كائن AdmissionReview JSON كـ input؛ يجب أن تُنتج السياسة مجموعة violation؛ تُعيّن Gatekeeper تلك المجموعة إلى استجابة قبول. كل استدعاء opa eval تُشغِّله محليًا مع تركيبة AdmissionReview حقيقية يُعيد إنتاج ما ستفعله Gatekeeper في الإنتاج تمامًا.