شبكات Kubernetes والتخزين

سياسات الشبكة في Kubernetes

18 دقيقة الدرس 5 من 31

سياسات الشبكة في Kubernetes

بشكل افتراضي، يستطيع كل Pod في مجموعة Kubernetes الوصول إلى أي Pod آخر — في أي namespace وعلى أي منفذ. هذا هو نموذج السماح الافتراضي: الشبكة مسطحة ومفتوحة بالكامل. هذا مقبول في بيئة تجريبية، لكنه ثغرة أمنية خطيرة في بيئة إنتاج تشغّل خدمات أو فرق أو مستأجرين متعددين. Pod واجهة أمامية مُخترق يمكنه فحص قاعدة البيانات بحرية. خطأ في namespace يمكنه إغراق نقاط النهاية في namespace آخر.

NetworkPolicy هو المورد الأصلي في Kubernetes الذي يتيح لك تقييد هذه الحركة. فكّر فيه كقاعدة جدار حماية محددة لـ Pod: يختار مجموعة من الـ Pods، ثم يحدد الأقران — بتسمية Pod أو namespace — المسموح لهم بالوصول إليها (ingress) أو التي يُسمح لها بالوصول إليهم (egress). يُنفّذ إضافة CNI القواعد في مستوى البيانات؛ يقتصر دور kube-apiserver على تخزين الكائنات فقط.

دعم CNI إلزامي. تُطبَّق كائنات NetworkPolicy فقط إذا كانت إضافة CNI الخاصة بك تدعمها. Flannel (الافتراضي في كثير من تثبيتات kubeadm) لا يُطبّق NetworkPolicy. Cilium وCalico وWeave جميعها تدعمها. تحقق دائماً من CNI قبل الاعتماد على NetworkPolicy للأمان.

من السماح الافتراضي إلى الرفض الافتراضي

التحول من السماح الافتراضي إلى الرفض الافتراضي هو أكثر تغيير مؤثر يمكنك إجراؤه على وضعية الأمان في مجموعتك. يعكس مبدأ انعدام الثقة: لا شيء موثوق افتراضياً؛ يجب منح الوصول صراحةً.

تختار NetworkPolicy الـ Pods عبر podSelector. الفكرة المحورية هي: سياسة بـ podSelector فارغ ({}) تختار جميع Pods في الـ namespace. وسياسة بدون قواعد ingress أو egress تعني عدم السماح بأي حركة في تلك الاتجاهات. الجمع بينهما يُعطيك رفضاً افتراضياً لكامل الـ namespace:

# deny-all.yaml — طبّقها على كل namespace تريد عزله apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny-all namespace: payments # كرر لكل namespace spec: podSelector: {} # يختار جميع pods في الـ namespace policyTypes: - Ingress - Egress

طبّق هذه السياسة ويتوقف كل Pod في payments فوراً عن استقبال وإرسال الحركة — بما في ذلك استعلامات DNS. لا تطبّقها على kube-system إلا إذا فهمت التداعيات جيداً.

ابدأ بالرفض الافتراضي، ثم بالقوائم البيضاء. النمط التشغيلي المستخدم على نطاق واسع: طبّق default-deny-all على namespace في بيئة التجربة، راقب ما ينكسر (تحقق من kubectl describe pod وسجلات CNI)، ثم اكتب سياسات السماح لكل تدفق مشروع. هذا يكشف التبعيات غير الموثقة التي لم تكن تعرفها.

السماح بحركة محددة

بعد تفعيل الرفض الافتراضي، تعيد فتح ما هو ضروري فقط. ثلاثة أنواع من المحددات متاحة في كتل from / to:

  • podSelector — يطابق Pods في نفس الـ namespace بالتسمية.
  • namespaceSelector — يطابق جميع Pods في namespaces تطابق تسمياتها.
  • ipBlock — يطابق نطاق CIDR (مفيد للخدمات الخارجية أو شبكات داخلية).

المحددات داخل نفس عنصر القائمة تُطبَّق بمنطق AND؛ العناصر المنفصلة تُطبَّق بمنطق OR. هذا مصدر شائع للارتباك.

سياسة سماح واقعية لـ deployment اسمه api-server يجب أن يقبل الحركة من Pod الـ frontend فقط، وعلى المنفذ 8080 فقط:

apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-frontend-to-api namespace: payments spec: podSelector: matchLabels: app: api-server # الهدف: pods بهذه التسمية policyTypes: - Ingress ingress: - from: - podSelector: matchLabels: app: frontend # فقط pods بهذه التسمية يمكنها الاتصال ports: - protocol: TCP port: 8080

لاحظ أن egress من api-server غير مذكور — لأن سياسة default-deny-all تغطي egress بالفعل، نحتاج إلى سياسة egress منفصلة (مثلاً للسماح لـ api-server بالتحدث إلى postgres على المنفذ 5432 وإلى kube-dns على UDP 53).

عزل الـ Namespace مع محددات عبر الـ Namespaces

مجموعات الإنتاج غالباً تشغّل namespace لـ monitoring (Prometheus) يحتاج إلى جمع مقاييس من pods في namespaces أخرى. تريد السماح بهذا المسار العابر للـ namespace تحديداً دون فتح كل شيء.

أولاً، ضع تسمية على namespace الـ monitoring:

kubectl label namespace monitoring kubernetes.io/metadata.name=monitoring

ثم أضف قاعدة ingress تجمع namespaceSelector وpodSelector — كلاهما في نفس عنصر قائمة from لتطبيق AND (فقط pods Prometheus من namespace الـ monitoring، لا أي pod من monitoring ولا pods Prometheus من أي namespace):

apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-prometheus-scrape namespace: payments spec: podSelector: matchLabels: app: api-server policyTypes: - Ingress ingress: - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: monitoring podSelector: matchLabels: app: prometheus # AND: namespace AND تسمية Pod ports: - protocol: TCP port: 9090

مخطط: عزل الـ Namespace بـ NetworkPolicy

Namespace isolation via NetworkPolicy Namespace: payments (default-deny-all applied) frontend app=frontend api-server app=api-server postgres app=postgres NetworkPolicy: allow-frontend-to-api Namespace: monitoring prometheus app=prometheus rogue-pod app=attacker ALLOW :8080 ALLOW :5432 ALLOW :9090 (namespaceSelector+podSelector) DENY (no matching policy) تدفق مسموح تدفق مرفوض
عزل الـ Namespace: default-deny-all في "payments" مع قواعد سماح صريحة. يُسمح بجمع مقاييس Prometheus عبر الـ namespace؛ يُرفض pod المخترق في "monitoring".

DNS: الضحية الصامتة للرفض الافتراضي

الخطأ الأكثر شيوعاً بعد تطبيق الرفض الافتراضي هو نسيان السماح بـ egress إلى kube-dns (المنفذ 53، UDP وTCP). كل استعلام DNS من Pod يمر عبر kube-dns في kube-system. عندما يُحجب، ينكسر كل اكتشاف للخدمات — لا تستطيع Pods تحليل postgres.payments.svc.cluster.local، ورسالة الخطأ هي انتهاء مهلة الاتصال العامة، لا خطأ DNS. أضف دائماً سياسة egress هذه جنباً إلى جنب مع الرفض الافتراضي:

apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-dns-egress namespace: payments spec: podSelector: {} # جميع pods في الـ namespace policyTypes: - Egress egress: - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: kube-system podSelector: matchLabels: k8s-app: kube-dns ports: - protocol: UDP port: 53 - protocol: TCP port: 53

تشخيص أخطاء NetworkPolicies

عندما تُحجب الحركة بشكل غير متوقع، أسرع طريق لتحديد السبب هو:

  1. شغّل kubectl get networkpolicies -n <namespace> -o yaml لإخراج جميع السياسات والتحقق من أن المحددات تطابق تسميات Pod تماماً (خطأ إملائي واحد يعني عدم تطبيق أي قاعدة).
  2. استخدم kubectl exec للدخول إلى pod تشخيصي وشغّل curl أو nc -zv <target> <port> لتأكيد ما يمكن الوصول إليه.
  3. تحقق من سجلات CNI. Cilium يوفر cilium-dbg monitor --type drop الذي يُظهر قرار السياسة الدقيق على كل حزمة مرفوضة — لا غنى عنه للتحقيقات الإنتاجية.
  4. تحقق من تسميات Pod بـ kubectl get pod <name> -o jsonpath='{.metadata.labels}' — هذه هي التي يطابقها podSelector في NetworkPolicy.
NetworkPolicy تراكمية: سياسات متعددة تستهدف نفس Pod تُجمع (union). لا يوجد إجراء رفض صريح في قاعدة NetworkPolicy — فقط السماح. الرفض يأتي من غياب أي قاعدة سماح مطابقة عندما تكون سياسة الرفض الافتراضي فعّالة. هذا يعني أنك تستطيع إضافة سياسات السماح تدريجياً بأمان دون خطر التقييد المفرط غير المقصود.

أفضل الممارسات الإنتاجية

  • عزل Namespace لكل فريق: كل فريق يمتلك namespace، وكل namespace يحصل على default-deny-all كخط أساسي. الحركة العابرة للـ namespace موثقة صراحةً في YAML وتخضع لمراجعة الكود مثل كود التطبيق.
  • حوكمة التسميات مهمة: NetworkPolicy موثوقة بقدر انضباطك في التسميات. طبّق معايير التسمية عبر webhooks للقبول (OPA/Gatekeeper) حتى لا يمكن نشر pods بدون التسميات التي تعتمد عليها السياسات.
  • استخدم Cilium Network Policy أو Calico GlobalNetworkPolicy للإعدادات الافتراضية على مستوى المجموعة التي تنطبق عبر جميع namespaces — NetworkPolicy القياسية محدودة بالـ namespace.
  • اختبر في CI: أدوات مثل netassert أو k8s-netpol-verify يمكنها تشغيل تأكيدات الاتصال على مجموعة وإفشال pipeline إذا سمحت NetworkPolicy بحركة غير متوقعة.