قيود الشبكة ونظام الملفات لبيئات بناء CI/CD

تُعد خطوط أنابيب CI/CD من أكثر الأحمال امتيازاً في أي مؤسسة. فهي تسحب الشيفرة المصدرية، وتنزّل التبعيات، وتصل إلى الأسرار، وتدفع الـ artifacts إلى سجلات الإنتاج. ومع ذلك، في كثير من البيئات، تعمل عمليات الـ build القائمة خلف هذه الخطوط بوصول شبكي غير مقيّد وأذونات كاملة على نظام الملفات — وهو مزيج يمثّل واحدة من أكثر الثغرات قابليةً للاستغلال في تسليم البرمجيات الحديث.

عندما تستطيع بيئة الـ build الوصول إلى أي عنوان IP والكتابة في أي مسار على القرص، يمكن لتبعية واحدة مخترَقة أو طلب دمج (pull request) خبيث أن يسرّب الأسرار، أو يعبث بالـ artifacts، أو ينشئ أبواباً خلفية دائمة. يغطّي هذا الدليل تقنيات عملية لإحكام قيود الوصول الشبكي والوصول إلى نظام الملفات في بيئات build الخاصة بـ CI/CD، من NetworkPolicies في Kubernetes إلى أنظمة الـ build المُحكَمة (hermetic).

لماذا تُعد بيئات الـ build غير المقيّدة خطيرة

قبل الخوض في الحلول، يجدر فهم التهديدات المحدّدة التي تخلقها بيئات الـ build غير المقيّدة. وهذه المخاطر ليست نظرية — فقد جرى استغلالها في هجمات سلسلة توريد واقعية.

تسريب البيانات

كثيراً ما تملك بيئات الـ build وصولاً إلى الأسرار: مفاتيح API، وبيانات اعتماد السجلات، ومفاتيح التوقيع، ورموز النشر. وإذا كان لعملية الـ build وصول شبكي صادر غير مقيّد، فيمكن لتبعية مخترَقة أن ترسل تلك الأسرار إلى خادم يتحكم فيه المهاجم. وقد يحدث ذلك عبر سكربت postinstall خبيث في حزمة npm، أو تبعية PyPI مخترَقة، أو حتى هدف Makefile مُصاغ بعناية. فبدون قيود شبكية، لا يوجد حاجز بين السرّ ونقطة نهاية المهاجم.

هجمات سلسلة التوريد

المهاجم القادر على تنفيذ شيفرة عشوائية أثناء الـ build يمكنه تعديل artifacts المخرجات. فإذا كان نظام الملفات قابلاً للكتابة دون قيود، أمكن العبث بالملفات الثنائية المُصرَّفة، أو صور الحاويات، أو بيانات النشر (manifests)، بعد خطوة الـ build الشرعية لكن قبل دفع الـ artifact. وهذا جوهر كثير من هجمات سلسلة التوريد — تبدو الشيفرة المصدرية نظيفة، لكن الـ artifact المُسلَّم مسموم.

الحركة الجانبية

بيئات الـ build التي تتشارك شبكة مع بنية تحتية أخرى (قواعد بيانات، واجهات برمجة تطبيقات داخلية، خدمات بيانات وصفية سحابية) تمنح المهاجم نقطة ارتكاز. فيمكن لمهمة build مخترَقة أن تمسح الشبكات الداخلية، وأن تصل إلى نقاط نهاية البيانات الوصفية لنسخة السحابة (مثل 169.254.169.254)، وأن تتصاعد من سياق CI/CD إلى وصول أوسع للبنية التحتية.

القيود الشبكية

أكثر ضابط يمكنك تطبيقه تأثيراً هو تقييد الوصول الشبكي الصادر من بيئات الـ build. فعمليات الـ build تحتاج إلى سحب التبعيات ودفع الـ artifacts، لكنها نادراً ما تحتاج إلى وصول غير مقيّد إلى الإنترنت.

NetworkPolicy في Kubernetes لبودات الـ Runner

إذا شغّلت runners الخاصة بـ CI/CD على Kubernetes (مثلاً باستخدام Actions Runner Controller أو مُنفِّذ GitLab لـ Kubernetes)، فإن موارد NetworkPolicy تمنحك تحكماً دقيقاً في الوصول الشبكي على مستوى البود. وتحرم السياسة المصمَّمة جيداً كل حركة صادرة (egress) افتراضياً، ثم تسمح فقط بنقاط النهاية المحدّدة التي يحتاجها الـ build.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: ci-runner-netpol
  namespace: ci-runners
spec:
  podSelector:
    matchLabels:
      app: ci-runner
  policyTypes:
    - Egress
  egress:
    # Allow DNS resolution
    - to:
        - namespaceSelector: {}
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53
    # Allow access to container registry
    - to:
        - ipBlock:
            cidr: 10.0.50.0/24
      ports:
        - protocol: TCP
          port: 443
    # Allow access to artifact storage
    - to:
        - ipBlock:
            cidr: 10.0.60.0/24
      ports:
        - protocol: TCP
          port: 443
    # Deny everything else by omission

تسمح هذه السياسة لبودات الـ runner بتحليل DNS، والوصول إلى سجل الحاويات، والوصول إلى تخزين الـ artifacts — لا شيء غير ذلك. وكل اتصال صادر آخر يُسقَط. وإذا كنت تستخدم إضافة CNI تدعم NetworkPolicy (Calico أو Cilium أو Weave Net)، فإن هذا يسري فوراً عند تطبيقه.

لتحكم أدقّ، تدعم CiliumNetworkPolicy من Cilium قواعد قائمة على DNS، ما يتيح لك تحديد أسماء النطاقات بدلاً من كتل IP:

apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: ci-runner-cilium-policy
  namespace: ci-runners
spec:
  endpointSelector:
    matchLabels:
      app: ci-runner
  egress:
    - toEndpoints:
        - matchLabels:
            io.kubernetes.pod.namespace: kube-system
            k8s-app: kube-dns
      toPorts:
        - ports:
            - port: "53"
              protocol: ANY
    - toFQDNs:
        - matchName: "ghcr.io"
        - matchName: "registry.npmjs.org"
        - matchName: "pypi.org"
      toPorts:
        - ports:
            - port: "443"
              protocol: TCP

Docker –network=none

بالنسبة لخطوات الـ build القائمة على Docker والتي لا ينبغي أن تحتاج إلى أي وصول شبكي (التصريف، والتحليل الساكن، واختبارات الوحدة)، يمكنك إزالة الوصول الشبكي بالكامل بتشغيل الحاوية بـ --network=none:

docker run --network=none \
  --rm \
  -v "$(pwd)/src:/workspace:ro" \
  -v "$(pwd)/output:/output" \
  my-build-image:latest \
  make build

مع --network=none، لا تملك الحاوية أي واجهات شبكية على الإطلاق — ولا حتى loopback في بعض التهيئات. وهذا أقوى عزل شبكي يمكنك تحقيقه لخطوة build. والمفتاح هو أن تبني خط أنابيبك بحيث يحدث جلب التبعيات في مرحلة واحدة (بوصول شبكي محدود) ويحدث الـ build الفعلي في مرحلة منفصلة بلا شبكة.

قواعد جدار الحماية للـ runners ذاتية الاستضافة

إذا كنت تستخدم runners ذاتية الاستضافة على أجهزة افتراضية بدلاً من الحاويات، فإن قواعد جدار الحماية على مستوى المضيف توفّر حماية مكافئة. على Linux، يمكن لقواعد iptables أو nftables أن تقيّد الحركة الصادرة من حساب المستخدم الذي يشغّل مهام الـ CI:

# Allow DNS
iptables -A OUTPUT -m owner --uid-owner ci-runner -p udp --dport 53 -j ACCEPT
iptables -A OUTPUT -m owner --uid-owner ci-runner -p tcp --dport 53 -j ACCEPT

# Allow HTTPS to specific registries
iptables -A OUTPUT -m owner --uid-owner ci-runner -p tcp --dport 443 \
  -d registry.example.com -j ACCEPT
iptables -A OUTPUT -m owner --uid-owner ci-runner -p tcp --dport 443 \
  -d ghcr.io -j ACCEPT

# Deny all other outbound traffic from the CI runner
iptables -A OUTPUT -m owner --uid-owner ci-runner -j DROP

يعمل هذا النهج جيداً حين تشغّل وكيل الـ CI تحت حساب مستخدم مخصّص وتحتاج إلى السماح لنظام المضيف نفسه بالحفاظ على اتصال أوسع لأغراض الإدارة والتحديثات.

وضع السجلات وواجهات API في قائمة السماح

بصرف النظر عن آلية الفرض، المبدأ واحد: امنع الحركة الصادرة افتراضياً، ثم اسمح فقط بما يحتاجه الـ build فعلاً. وعادةً ما تشمل قائمة السماح النموذجية سجل الحزم (npm، PyPI، Maven Central)، وسجل الحاويات (Docker Hub، GHCR، ECR)، وواجهة API لمنصة الـ CI/CD (لتحديثات الحالة ورفع الـ artifacts)، وربما وكيلاً (proxy) أو مرآة (mirror) تتحكم فيها. وينبغي حجب كل ما عدا ذلك. استخدم وكيلاً أو مرآة داخلية للتبعيات كلما أمكن — فهذا يقلّص قائمة السماح إلى نقطة نهاية واحدة، ويمنحك التخزين المؤقت وتسجيل التدقيق مجاناً.

قيود نظام الملفات

تمنع القيود الشبكية البيانات من مغادرة بيئة الـ build. وتمنع قيود نظام الملفات التعديلات غير المصرّح بها داخلها. ومعاً، تشكّلان وضعية قوية للدفاع في العمق (defense-in-depth).

نظام ملفات جذري للقراءة فقط

تشغيل حاويات الـ build بنظام ملفات جذري للقراءة فقط يمنع أي عملية من تعديل الصورة الأساسية. ويحجب ذلك فئة من الهجمات تعدّل فيها الشيفرة الخبيثة الملفات الثنائية للنظام، أو تثبّت أبواباً خلفية، أو تغيّر تهيئة أدوات الـ build على مستوى النظام.

في Docker، استخدم الراية --read-only:

docker run --read-only \
  --tmpfs /tmp:rw,noexec,nosuid,size=512m \
  --tmpfs /workspace/build:rw,size=2g \
  -v "$(pwd)/src:/workspace/src:ro" \
  my-build-image:latest \
  make build

في Kubernetes، اضبط سياق الأمان (security context) في مواصفات البود:

apiVersion: v1
kind: Pod
metadata:
  name: ci-build-pod
spec:
  containers:
    - name: build
      image: my-build-image:latest
      securityContext:
        readOnlyRootFilesystem: true
        runAsNonRoot: true
        allowPrivilegeEscalation: false
      volumeMounts:
        - name: build-tmp
          mountPath: /tmp
        - name: build-output
          mountPath: /workspace/build
        - name: source
          mountPath: /workspace/src
          readOnly: true
  volumes:
    - name: build-tmp
      emptyDir:
        medium: Memory
        sizeLimit: 512Mi
    - name: build-output
      emptyDir:
        sizeLimit: 2Gi
    - name: source
      configMap:
        name: source-code

tmpfs لـ artifacts الـ build

عندما يكون نظام الملفات الجذري للقراءة فقط، تحتاج عمليات الـ build إلى مساحة قابلة للكتابة للملفات المؤقتة، وذواكر التخزين المؤقت، وartifacts المخرجات. استخدم عمليات تركيب tmpfs (المدعومة بذاكرة RAM) أو أحجام emptyDir (في Kubernetes) لهذه المسارات. ولهذا فائدة إضافية هي أن جميع artifacts الـ build تُنظَّف تلقائياً عند خروج الحاوية — فلا تبقى بيانات قديمة بين عمليات الـ build.

ركّب tmpfs بخيارات مقيّدة كلما أمكن: noexec يمنع تنفيذ الملفات الثنائية المكتوبة في المجلدات المؤقتة (ما يحجب متجه هجوم شائعاً)، وnosuid يمنع هجمات بِتّ SUID، وحدود size تمنع build جامحاً من استنزاف ذاكرة المضيف.

منع الكتابة في المسارات الحساسة

إلى جانب نظام الملفات الجذري، تستحق مسارات محدّدة حماية إضافية. ركّب الشيفرة المصدرية للقراءة فقط لمنع الـ build من تعديل مدخلاته الخاصة. وتأكّد من أن /etc و/usr و/var غير قابلة للكتابة. وإذا كان الـ build يحتاج إلى الكتابة في مجلد المنزل (home directory) لتهيئة الأدوات، فوفّر عملية تركيب مخصّصة قابلة للكتابة بدلاً من جعل مجلد المنزل بأكمله قابلاً للكتابة. واحجب الوصول إلى مقابس Docker (Docker sockets)، ورموز حسابات خدمة Kubernetes، وملفات بيانات الاعتماد السحابية بعدم تركيبها في حاويات الـ build على الإطلاق.

عمليات الـ build المُحكَمة (Hermetic)

المعيار الذهبي لأمن بيئة الـ build هو الـ build المُحكَم (hermetic): بناء لا يملك أي وصول شبكي على الإطلاق ويستخدم فقط مدخلات مُعلَنة صراحةً ومُجلوبة مسبقاً. وتقضي عمليات الـ build المُحكَمة على فئات كاملة من هجمات سلسلة التوريد، لأن عملية الـ build لا تستطيع تنزيل شيفرة لم تُحدَّد وتُتحقَّق منها صراحةً.

نمط الـ build المُحكَم

يتألف خط أنابيب الـ build المُحكَم عادةً من مرحلتين. في المرحلة الأولى (مرحلة التحليل/الجلب)، تُنزَّل التبعيات من مصادر معتمَدة، ويُتحقَّق من قيم اختبارها (checksums) مقابل ملف قفل (lockfile)، وتُخزَّن في ذاكرة تخزين مؤقت محلية أو مجلد مُورَّد (vendored). وتتطلب هذه المرحلة وصولاً شبكياً محدوداً. وفي المرحلة الثانية (مرحلة الـ build)، يحدث التصريف أو التجميع الفعلي بوصول شبكي صفري. وتأتي جميع المدخلات من نظام الملفات المحلي — الشيفرة المصدرية والتبعيات المُجلوبة مسبقاً.

# Phase 1: Fetch dependencies (limited network)
docker run --network=ci-restricted \
  -v "$(pwd):/workspace" \
  my-build-image:latest \
  sh -c "cd /workspace && npm ci --ignore-scripts"

# Phase 2: Build (no network)
docker run --network=none \
  --read-only \
  --tmpfs /tmp:rw,noexec,size=512m \
  -v "$(pwd):/workspace:ro" \
  -v "$(pwd)/dist:/dist" \
  my-build-image:latest \
  sh -c "cd /workspace && npm run build && cp -r build/* /dist/"

Bazel وعمليات الـ build المُحكَمة

صُمِّم Bazel حول مبدأ الإحكام (hermeticity). فمع --sandbox_default_allow_network=false، يحجب Bazel الوصول الشبكي أثناء إجراءات الـ build افتراضياً. وتُعلَن التبعيات في ملفات WORKSPACE أو MODULE.bazel بقيم تجزئة SHA-256 صريحة، ويجلبها Bazel في مرحلة منفصلة قبل بدء الـ build. وإذا لم تطابق تبعية قيمة تجزئتها المُعلَنة، فشل الـ build.

# In .bazelrc
build --sandbox_default_allow_network=false
build --incompatible_strict_action_env
fetch --repository_cache=/shared/bazel-cache/repos

وهذا يجعل عمليات build الخاصة بـ Bazel قابلة لإعادة الإنتاج ومقاوِمة لهجمات الخلط بين التبعيات (dependency confusion). فكل مدخل مُعنوَن بمحتواه (content-addressed) ومُتحقَّق منه.

Nix وعمليات الـ build القابلة لإعادة الإنتاج

يتبع Nix نهجاً مماثلاً. فكل اشتقاق build (derivation) يحدّد مدخلاته بقيمة تجزئة المحتوى، وصندوق build الرملي في Nix يحجب الوصول الشبكي افتراضياً. ويجلب أمر nix-build جميع المصادر إلى مخزن Nix (متحقّقاً من قيم التجزئة)، ثم يشغّل الـ build في بيئة معزولة بلا شبكة وبنظام ملفات مُقلَّص. ويضمن ذلك أن تكون عمليات الـ build قابلة لإعادة الإنتاج — فالمدخلات نفسها تنتج المخرجات نفسها دائماً.

التنفيذ العملي

لننظر في كيفية تطبيق هذه القيود في منصات CI/CD محدّدة.

GitHub Actions مع Actions Runner Controller (ARC) + NetworkPolicy

إذا كنت تستخدم Actions Runner Controller لتشغيل GitHub Actions على Kubernetes، فيمكنك تطبيق NetworkPolicies مباشرةً على بودات الـ runner. فـ ARC ينشئ بودات بوسوم (labels) متوقّعة، ما يجعلها سهلة الاستهداف بالسياسات.

apiVersion: actions.summerwind.dev/v1alpha1
kind: RunnerDeployment
metadata:
  name: secure-runner
  namespace: ci-runners
spec:
  replicas: 3
  template:
    metadata:
      labels:
        app: ci-runner
        security-tier: restricted
    spec:
      containers:
        - name: runner
          securityContext:
            readOnlyRootFilesystem: true
            runAsNonRoot: true
            allowPrivilegeEscalation: false
            capabilities:
              drop:
                - ALL
          volumeMounts:
            - name: work
              mountPath: /runner/_work
            - name: tmp
              mountPath: /tmp
      volumes:
        - name: work
          emptyDir:
            sizeLimit: 10Gi
        - name: tmp
          emptyDir:
            medium: Memory
            sizeLimit: 1Gi
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: secure-runner-netpol
  namespace: ci-runners
spec:
  podSelector:
    matchLabels:
      app: ci-runner
  policyTypes:
    - Egress
    - Ingress
  ingress: []
  egress:
    - to:
        - namespaceSelector: {}
      ports:
        - protocol: UDP
          port: 53
    - to:
        - ipBlock:
            cidr: 0.0.0.0/0
      ports:
        - protocol: TCP
          port: 443

تحرم هذه التهيئة كل حركة واردة (ingress) — فالـ runners لا ينبغي أن تقبل اتصالات واردة — وتقصر الحركة الصادرة على DNS وHTTPS. وللاستخدام في الإنتاج، استبدل نطاق 0.0.0.0/0 بنطاقات IP محدّدة لواجهة API الخاصة بـ GitHub، وسجل الحاويات لديك، ومخزن الـ artifacts.

GitLab CI مع تهيئة الـ Runner

يدعم مُنفِّذ GitLab لـ Kubernetes تهيئة سياق الأمان في ملف config.toml الخاص بالـ runner. ويمكنك ضبط نظام الملفات للقراءة فقط وقيود أخرى مباشرةً:

# config.toml for GitLab Runner (Kubernetes executor)
[[runners]]
  name = "secure-k8s-runner"
  executor = "kubernetes"
  [runners.kubernetes]
    namespace = "ci-runners"
    image = "alpine:latest"
    privileged = false
    allow_privilege_escalation = false
    [runners.kubernetes.pod_security_context]
      run_as_non_root = true
      run_as_user = 1000
    [runners.kubernetes.build_container_security_context]
      read_only_root_filesystem = true
      allow_privilege_escalation = false
      [runners.kubernetes.build_container_security_context.capabilities]
        drop = ["ALL"]
    [runners.kubernetes.volumes]
      [[runners.kubernetes.volumes.empty_dir]]
        name = "build-tmp"
        mount_path = "/tmp"
        medium = "Memory"
        size_limit = "512Mi"
      [[runners.kubernetes.volumes.empty_dir]]
        name = "build-workspace"
        mount_path = "/builds"
        size_limit = "5Gi"

اجمع هذا مع NetworkPolicy مطبَّقة على مساحة الأسماء ci-runners فتحصل على قيود لنظام الملفات وللشبكة معاً.

قيود Docker-in-Docker

يُستخدَم Docker-in-Docker (DinD) عادةً لبناء صور الحاويات في الـ CI. وهو أيضاً أحد أكثر الأنماط خطورة لأنه يتطلب عادةً الوضع المميّز (privileged mode). وإذا كان لا بد من استخدام DinD، فطبّق هذه القيود:

# Use rootless DinD instead of privileged mode
services:
  dind:
    image: docker:24-dind-rootless
    environment:
      - DOCKER_TLS_CERTDIR=/certs
    volumes:
      - dind-certs:/certs/client
      - dind-data:/var/lib/docker

# When running builds inside DinD, pass network and filesystem restrictions
docker --host tcp://dind:2376 --tlsverify \
  run --network=none --read-only \
  --tmpfs /tmp:rw,noexec,size=256m \
  --security-opt=no-new-privileges \
  my-build-image:latest make build

والأفضل من ذلك أن تستبدل بـ DinD أدوات لا تحتاج إلى خفيّ Docker (Docker daemon) على الإطلاق. فأدوات مثل kaniko وbuildah وko يمكنها بناء صور الحاويات دون وصول مميّز، وتعمل جيداً مع أنظمة الملفات للقراءة فقط والشبكات المقيّدة.

المراقبة والتدقيق

القيود مفيدة فقط إذا كنت تعرف متى يجري اختبارها أو تجاوزها. فالمراقبة تُكمل الصورة الأمنية.

كشف الاتصالات الشبكية غير المتوقعة

استخدم Hubble من Cilium، أو سجلات التدفق (flow logs) من Calico، أو Falco لكشف الاتصالات الشبكية التي كان ينبغي لسياستك حجبها (أو الاتصالات بوجهات غير معتادة على المنافذ المسموح بها). أعدّ تنبيهات لأي استعلامات DNS إلى نطاقات ليست في قائمة السماح لديك، والاتصالات الصادرة إلى منافذ غير قياسية، والاتصالات بنطاقات IP معروفة بخبثها، وأي حركة صادرة من بودات ينبغي أن تكون بـ --network=none.

# Falco rule: detect unexpected outbound connections from CI runners
- rule: CI Runner Unexpected Outbound Connection
  desc: Detect network connections from CI runner pods to non-approved destinations
  condition: >
    evt.type in (connect, sendto) and
    container and
    k8s.ns.name = "ci-runners" and
    not (fd.sip in (approved_registry_ips) or fd.sport = 53)
  output: >
    Unexpected outbound connection from CI runner
    (command=%proc.cmdline connection=%fd.name container=%container.name
    pod=%k8s.pod.name namespace=%k8s.ns.name)
  priority: WARNING
  tags: [network, ci-cd, supply-chain]

تدقيق الوصول إلى نظام الملفات

راقب عمليات الكتابة في نظام الملفات داخل حاويات الـ build لكشف التعديلات غير المتوقعة. فبإمكان auditd في Linux مراقبة مسارات محدّدة، وبإمكان Falco كشف الكتابة في المواضع الحساسة. وتشمل المسارات الرئيسية الواجب مراقبتها /etc و/usr (لا ينبغي الكتابة فيهما أبداً أثناء build)، ومسار مقبس Docker، ومسارات رموز حسابات خدمة Kubernetes، وأي مسار يحتوي على بيانات اعتماد أو مفاتيح توقيع.

إذا كنت تستخدم أنظمة ملفات جذرية للقراءة فقط، فإن أي محاولة كتابة في مسار محمي تولّد خطأً — سجّل هذه الأخطاء ونبّه عليها. فهي تشير إما إلى build سيّئ التهيئة أو إلى هجوم محتمل.

المفاضلات وتجربة المطوّر

القيود الصارمة على الشبكة ونظام الملفات تخلق احتكاكاً لا محالة. وفهم هذه المفاضلات وإدارتها أمر جوهري لنجاح التبنّي.

سرعة الـ build

تتطلب عمليات الـ build المُحكَمة جلب جميع التبعيات مسبقاً، ما يضيف مرحلة إلى خط الأنابيب. لكن هذا يعني أيضاً إمكانية تخزين التبعيات مؤقتاً بقوة. وعملياً، تجد فرق كثيرة أن عمليات الـ build المُحكَمة أسرع فعلاً، لأن معدّل إصابة ذاكرة التخزين المؤقت أعلى بكثير حين يكون تحليل التبعيات حتمياً. استخدم ذاكرة تخزين مؤقت مشتركة (ذاكرة Bazel البعيدة، أو ذاكرة Nix الثنائية، أو ذاكرة HTTP بسيطة للتبعيات المُورَّدة) لتوزيع التكلفة على عمليات الـ build.

تجربة المطوّر

سيواجه المطوّرون إخفاقات حين تحاول عمليات الـ build الوصول إلى نقاط نهاية شبكية محجوبة أو الكتابة في مسارات للقراءة فقط. ورسائل الخطأ الجيدة ضرورية. غلّف خطوات الـ build لديك بسكربتات تلتقط أخطاء الأذونات والإخفاقات الشبكية، ثم تُخرج رسائل قابلة للتنفيذ توضّح سبب حجب الوصول وكيفية إصلاح المشكلة (عادةً بإضافة تبعية إلى ملف القفل أو بتغيير مسار المخرجات).

فكّر في تنفيذ طرح تدريجي: ابدأ بوضع المراقبة (سجّل المخالفات دون حجبها)، ثم انتقل إلى الفرض. فهذا يمنح الفرق وقتاً لتحديث تهيئات الـ build لديها دون كسر كل خط أنابيب دفعةً واحدة.

تصحيح الأخطاء

يكون تصحيح إخفاقات الـ build في بيئة مقيّدة أصعب حين لا تستطيع تثبيت أدوات إضافية أو الوصول إلى خدمات خارجية. وفّر “وضع تصحيح” (debug mode) يخفّف القيود لتشغيل خط أنابيب محدّد يُطلَق يدوياً (وليس أبداً لعمليات التشغيل الآلية على الفرع الرئيسي). سجّل أن وضع التصحيح قد استُخدم ومَن أطلقه. ولا تسمح أبداً لوضع التصحيح بتجاوز القيود على عمليات build الخاصة بـ artifacts الإنتاج.

الجمع بين كل ذلك

فيما يلي ملخّص للنهج الطبقي لتأمين بيئات build الخاصة بـ CI/CD:

الطبقة 1 — القيود الشبكية: امنع الحركة الصادرة افتراضياً مع قوائم سماح للسجلات وواجهات API. استخدم NetworkPolicy في Kubernetes، أو --network=none في Docker، أو قواعد جدار الحماية على مستوى المضيف بحسب بنية الـ runner لديك.

الطبقة 2 — قيود نظام الملفات: نظام ملفات جذري للقراءة فقط، وtmpfs للمسارات القابلة للكتابة بحدود حجم وبخيار noexec، وشيفرة مصدرية مُركَّبة للقراءة فقط.

الطبقة 3 — عمليات الـ build المُحكَمة: افصِل تحليل التبعيات عن الـ build. وشغّل مرحلة الـ build بوصول شبكي صفري وبمدخلات مُجلوبة مسبقاً ومُتحقَّق من تجزئتها فقط.

الطبقة 4 — المراقبة: اكشف مخالفات السياسة والاتصالات غير المتوقعة ومحاولات تعديل نظام الملفات ونبّه عليها.

لا تكفي أي طبقة وحدها. فالقيود الشبكية بلا ضوابط لنظام الملفات لا تزال تتيح العبث بالـ artifacts. وقيود نظام الملفات بلا ضوابط شبكية لا تزال تتيح التسريب. وعمليات الـ build المُحكَمة بلا مراقبة تتركك أعمى عن محاولات الهجوم. فالطبقات يعزّز بعضها بعضاً.

أدلة ذات صلة

لمزيد حول تأمين خط أنابيب CI/CD لديك، راجع هذه الأدلة ذات الصلة:

ابدأ بالقيود الشبكية — فهي تقدّم أعلى تأثير بأقل جهد تنفيذ. ثم أضف قيود نظام الملفات واعمل باتجاه عمليات الـ build المُحكَمة كلما زاد نضج خط أنابيبك. فكل طبقة تضيفها تجعل هجمات سلسلة التوريد أصعب تنفيذاً على نحو ملموس.