نظرة عامة
تُعد GitHub-hosted runners مشتركة ومؤقتة بشكل افتراضي — حيث يحصل كل مهمة (job) على آلة افتراضية جديدة يتم تدميرها بعد اكتمال المهمة. أما Self-hosted runners، فهي دائمة ومشتركة عبر عمليات تشغيل سير العمل المختلفة. يُشكّل هذا خطراً أمنياً كبيراً: حيث يمكن أن تتسرب الأسرار (secrets) والرموز (tokens) ومخرجات البناء (build artifacts) من مهمة إلى أخرى. يمكن لسير عمل مخترق أن يُلوّث بيئة التشغيل لجميع المهام المستقبلية.
يحل Actions Runner Controller (ARC) هذه المشكلة. ARC هو مشغّل أصلي لـ Kubernetes يمنحك runners مؤقتة، قابلة للتوسع التلقائي، ومبنية على الحاويات. تحصل كل مهمة على pod جديد يتم تدميره عند اكتمال المهمة — تماماً مثل GitHub-hosted runners، ولكنها تعمل على بنيتك التحتية الخاصة مع أدواتك وسياسات الشبكة الخاصة بك.
في هذا المختبر العملي، ستقوم بـ:
- نشر ARC على مجموعة Kubernetes محلية
- تكوين مجموعات runners مؤقتة قابلة للتوسع
- إثبات العزل بين المهام (الميزة الأمنية الأساسية)
- بناء صور runners مخصصة
- تطبيق عزل مجموعات runners لفصل المهام
- تكوين التوسع التلقائي
- تطبيق سياسات الشبكة لتقييد وصول runners إلى الشبكة
المتطلبات الأساسية
قبل بدء هذا المختبر، تأكد من توفر ما يلي:
- مجموعة Kubernetes — kind أو minikube أو مجموعة سحابية مُدارة (EKS أو GKE أو AKS)
- Helm 3 — التثبيت من helm.sh
- kubectl — مُهيأ للتواصل مع مجموعتك
- حساب GitHub — مع صلاحيات المسؤول على مستودع أو منظمة
- GitHub App أو Personal Access Token (PAT) — بصلاحيات
repoوadmin:org(PAT) أو صلاحيات GitHub App المناسبة - Docker — لبناء صور runners المخصصة (التمرين 4)
إعداد البيئة
سنستخدم kind (Kubernetes in Docker) لإنشاء مجموعة محلية. هذا يجعل المختبر مستقلاً وسهل التنظيف.
إنشاء مجموعة kind
kind create cluster --name arc-lab
تحقق من أن المجموعة تعمل:
kubectl cluster-info --context kind-arc-lab
إنشاء مستودع GitHub للاختبار
أنشئ مستودعاً جديداً (مثل arc-lab-test) في حسابك على GitHub. أضف ملف سير عمل بسيط في .github/workflows/test.yml:
name: ARC Test Workflow
on:
push:
branches: [main]
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Hello from GitHub-hosted runner
run: echo "This runs on a GitHub-hosted runner"
ادفع هذا إلى مستودعك. سنعدّله لاحقاً ليستهدف runners الخاصة بـ ARC.
التمرين 1: تثبيت ARC باستخدام Helm
يستخدم Actions Runner Controller v2 مخططات Helm لنشر مكونين: وحدة تحكم (controller) تدير دورة حياة pods الـ runner، ومجموعة واحدة أو أكثر من runner scale sets التي تسجل مع GitHub وتقبل المهام.
الخطوة 1: إضافة مستودع Helm
helm repo add actions-runner-controller \
https://actions-runner-controller.github.io/actions-runner-controller
helm repo update
الخطوة 2: تكوين المصادقة
يحتاج ARC إلى المصادقة مع GitHub API. لديك خياران:
الخيار أ: GitHub App (مُوصى به للإنتاج)
أنشئ GitHub App في إعدادات منظمتك أو حسابك:
- انتقل إلى Settings → Developer settings → GitHub Apps → New GitHub App
- عيّن الصلاحيات التالية:
- Repository:
Actions(قراءة)،Administration(قراءة/كتابة)،Metadata(قراءة) - Organization:
Self-hosted runners(قراءة/كتابة)
- Repository:
- أنشئ مفتاحاً خاصاً وقم بتنزيله
- ثبّت التطبيق على منظمتك أو مستودعك
- سجّل App ID و Installation ID
الخيار ب: Personal Access Token (أبسط للمختبرات)
أنشئ PAT (كلاسيكي) بصلاحيات repo و admin:org، أو PAT دقيق الصلاحيات مع أذونات Actions و Administration. لهذا المختبر، سنستخدم PAT للبساطة.
الخطوة 3: تثبيت وحدة تحكم ARC
helm install arc \
actions-runner-controller/gha-runner-scale-set-controller \
--namespace arc-systems \
--create-namespace
تحقق من أن وحدة التحكم تعمل:
kubectl get pods -n arc-systems
يجب أن ترى مخرجات مشابهة لـ:
NAME READY STATUS RESTARTS AGE
arc-gha-runner-scale-set-controller-xxx 1/1 Running 0 30s
الخطوة 4: تثبيت Runner Scale Set
الآن انشر runner scale set التي تسجل مع مستودعك على GitHub:
helm install arc-runner-set \
actions-runner-controller/gha-runner-scale-set \
--namespace arc-runners \
--create-namespace \
--set githubConfigUrl="https://github.com/<org>/<repo>" \
--set githubConfigSecret.github_token="<PAT>"
استبدل <org>/<repo> بمسار مستودعك و <PAT> برمز الوصول الشخصي الخاص بك.
تحقق من runner scale set:
kubectl get pods -n arc-runners
في هذه المرحلة، قد لا توجد pods للـ runner بعد — يستخدم ARC نموذج التوسع إلى الصفر. يتم إنشاء Pods فقط عند وضع المهام في قائمة الانتظار.
الخطوة 5: التحقق في GitHub
انتقل إلى مستودعك على GitHub: Settings → Actions → Runners. يجب أن ترى runner scale set مدرجة باسم arc-runner-set. تُظهر الحالة أنها جاهزة لقبول المهام.
التمرين 2: تشغيل سير عمل على ARC Runners
الآن حدّث سير العمل التجريبي ليستهدف runner scale set الخاصة بـ ARC بدلاً من GitHub-hosted runners.
الخطوة 1: تحديث سير العمل
عدّل .github/workflows/test.yml لاستخدام تسمية ARC runner:
name: ARC Test Workflow
on:
push:
branches: [main]
workflow_dispatch:
jobs:
test:
runs-on: arc-runner-set
steps:
- name: Hello from ARC runner
run: |
echo "This runs on an ephemeral ARC runner!"
echo "Hostname: $(hostname)"
echo "Runner OS: $(uname -a)"
- name: Show environment
run: env | sort
التغيير الرئيسي هو runs-on: arc-runner-set — والذي يتطابق مع اسم إصدار Helm لـ runner scale set.
الخطوة 2: تشغيل سير العمل
ادفع ملف سير العمل المحدّث أو استخدم زر “Run workflow” (workflow_dispatch) في واجهة GitHub Actions.
الخطوة 3: مراقبة Pod الـ Runner
راقب مساحة الأسماء arc-runners أثناء تشغيل سير العمل:
kubectl get pods -n arc-runners -w
سترى pod يتم إنشاؤه للمهمة:
NAME READY STATUS RESTARTS AGE
arc-runner-set-xxxxx-runner 1/1 Running 0 5s
بعد اكتمال المهمة، يتم إنهاء Pod وإزالته:
NAME READY STATUS RESTARTS AGE
arc-runner-set-xxxxx-runner 0/1 Completed 0 45s
شغّل kubectl get pods -n arc-runners مرة أخرى — اختفى Pod. هذا هو النموذج المؤقت: كل مهمة تحصل على حاوية جديدة، ويتم تدمير الحاوية عند انتهاء المهمة. لا يوجد استمرارية للحالة بين المهام.
التمرين 3: إثبات الأمان المؤقت
يوضح هذا التمرين الميزة الأمنية الأساسية للـ runners المؤقتة: عدم وجود تلوث بين المهام.
الخطوة 1: إنشاء سير عمل يكتب بيانات حساسة
أنشئ .github/workflows/ephemeral-test.yml:
name: Ephemeral Security Test
on: workflow_dispatch
jobs:
write-secret:
runs-on: arc-runner-set
steps:
- name: Write sensitive data
run: |
echo "SECRET_API_KEY=sk-prod-abc123xyz" > /tmp/secret-data
echo "DB_PASSWORD=super-secret-password" >> /tmp/secret-data
echo "Written sensitive data to /tmp/secret-data"
cat /tmp/secret-data
read-secret:
runs-on: arc-runner-set
needs: write-secret
steps:
- name: Attempt to read previous job data
run: |
echo "Checking if /tmp/secret-data exists from previous job..."
if [ -f /tmp/secret-data ]; then
echo "SECURITY RISK: Found data from previous job!"
cat /tmp/secret-data
else
echo "SECURE: /tmp/secret-data does not exist."
echo "Each job gets a fresh container — no cross-job contamination."
fi
الخطوة 2: تشغيل سير العمل
شغّل سير العمل عبر workflow_dispatch. تكتب المهمة الأولى (write-secret) بيانات حساسة إلى /tmp/secret-data. تعمل المهمة الثانية (read-secret) في pod جديد وتحاول قراءة ذلك الملف.
الخطوة 3: التحقق من النتائج
في سجلات GitHub Actions، سترى:
- مهمة write-secret: تكتب الملف بنجاح وتطبع المحتويات
- مهمة read-secret: الملف غير موجود — المخرجات تُظهر
SECURE: /tmp/secret-data does not exist.
كل مهمة عملت في pod منفصل تم إنشاؤه حديثاً. عندما تم تدمير pod write-secret، تم تدمير جميع البيانات — بما في ذلك الملف الحساس — معه.
لماذا هذا مهم
على runner ذاتي الاستضافة دائم، سيبقى ملف /tmp/secret-data على القرص عند تشغيل المهمة الثانية. يمكن لسير عمل خبيث في طلب سحب (pull request) قراءة الأسرار والرموز وبيانات الاعتماد التي تركتها المهام السابقة. مع runners المؤقتة، يتم القضاء على هذا المتجه الهجومي.
التمرين 4: صور Runner المخصصة
تستخدم ARC runners صورة حاوية أساسية. للاستخدام في العالم الحقيقي، تحتاج إلى تخصيص هذه الصورة لتضمين أدوات البناء الخاصة بك.
الخطوة 1: إنشاء Dockerfile مخصص
أنشئ Dockerfile لـ runner المخصص الخاص بك:
FROM ghcr.io/actions/actions-runner:latest
USER root
# Install build tools
RUN apt-get update && apt-get install -y \
curl \
wget \
git \
jq \
unzip \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Install Go
RUN wget -q https://go.dev/dl/go1.22.4.linux-amd64.tar.gz \
&& tar -C /usr/local -xzf go1.22.4.linux-amd64.tar.gz \
&& rm go1.22.4.linux-amd64.tar.gz
ENV PATH="$PATH:/usr/local/go/bin"
# Install cosign
RUN curl -sSL -o /usr/local/bin/cosign \
https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64 \
&& chmod +x /usr/local/bin/cosign
# Install Docker CLI (for Docker-in-Docker workflows)
RUN curl -fsSL https://get.docker.com | sh
USER runner
الخطوة 2: بناء الصورة ودفعها
# Build the image
docker build -t ghcr.io/<org>/custom-runner:latest .
# Authenticate to GitHub Container Registry
echo "<PAT>" | docker login ghcr.io -u <username> --password-stdin
# Push the image
docker push ghcr.io/<org>/custom-runner:latest
الخطوة 3: تكوين ARC لاستخدام الصورة المخصصة
أنشئ ملف قيم custom-runner-values.yaml:
githubConfigUrl: "https://github.com/<org>/<repo>"
githubConfigSecret:
github_token: "<PAT>"
template:
spec:
containers:
- name: runner
image: ghcr.io/<org>/custom-runner:latest
command: ["/home/runner/run.sh"]
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2"
memory: "2Gi"
قم بترقية runner scale set بالصورة المخصصة:
helm upgrade arc-runner-set \
actions-runner-controller/gha-runner-scale-set \
--namespace arc-runners \
-f custom-runner-values.yaml
الخطوة 4: التحقق من الأدوات المخصصة
أنشئ سير عمل يستخدم الأدوات المخصصة:
name: Custom Runner Tools Test
on: workflow_dispatch
jobs:
verify-tools:
runs-on: arc-runner-set
steps:
- name: Verify Go
run: go version
- name: Verify cosign
run: cosign version
- name: Verify Docker CLI
run: docker --version
الفائدة الأمنية: من خلال بناء صورة runner الخاصة بك، تتحكم بالضبط في الأدوات والاعتماديات الموجودة في بيئة البناء. لا توجد ملفات ثنائية غير متوقعة، ولا برامج مثبتة مسبقاً لم توافق عليها، ويمكنك تثبيت كل أداة على إصدار محدد. يمكنك أيضاً فحص الصورة بحثاً عن الثغرات الأمنية قبل نشرها.
التمرين 5: عزل مجموعات Runner
تتمتع سير العمل المختلفة بمستويات ثقة مختلفة. لا ينبغي أن يكون لتحقق طلبات السحب (pull request) وصول إلى أسرار الإنتاج. تحتاج سير عمل النشر إلى الأسرار ولكن يجب أن تعمل فقط من الفرع الرئيسي. يتيح لك ARC تنفيذ هذا الفصل من خلال إنشاء مجموعات runner scale sets متميزة بتسميات وتكوينات مختلفة.
الخطوة 1: إنشاء Runner Scale Set للتحقق من طلبات السحب
أنشئ pr-runner-values.yaml:
githubConfigUrl: "https://github.com/<org>/<repo>"
githubConfigSecret:
github_token: "<PAT>"
template:
spec:
containers:
- name: runner
image: ghcr.io/<org>/custom-runner:latest
command: ["/home/runner/run.sh"]
env:
- name: RUNNER_GROUP
value: "pr-validation"
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "1"
memory: "1Gi"
helm install arc-runner-pr \
actions-runner-controller/gha-runner-scale-set \
--namespace arc-runners \
-f pr-runner-values.yaml
الخطوة 2: إنشاء Runner Scale Set للنشر
أنشئ deploy-runner-values.yaml:
githubConfigUrl: "https://github.com/<org>/<repo>"
githubConfigSecret:
github_token: "<PAT>"
template:
spec:
containers:
- name: runner
image: ghcr.io/<org>/custom-runner:latest
command: ["/home/runner/run.sh"]
env:
- name: RUNNER_GROUP
value: "deployment"
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2"
memory: "2Gi"
serviceAccountName: deploy-runner-sa
nodeSelector:
runner-type: deployment
helm install arc-runner-deploy \
actions-runner-controller/gha-runner-scale-set \
--namespace arc-runners \
-f deploy-runner-values.yaml
الخطوة 3: تكوين سير العمل للعزل
استخدم تسميات runner مختلفة بناءً على محفّز سير العمل:
name: CI/CD Pipeline
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
validate:
if: github.event_name == 'pull_request'
runs-on: arc-runner-pr
steps:
- uses: actions/checkout@v4
- name: Run tests
run: make test
- name: Run linter
run: make lint
deploy:
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: arc-runner-deploy
steps:
- uses: actions/checkout@v4
- name: Deploy to production
run: make deploy
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
ينفذ هذا فصل المهام على مستوى الـ runner. تعمل مهام التحقق من طلبات السحب على runners ليس لها وصول إلى أسرار النشر أو شرائح الشبكة المتميزة. تعمل مهام النشر على مجموعة منفصلة من runners التي تمتلك بيانات الاعتماد اللازمة والوصول إلى الشبكة، ولكنها تعمل فقط عند الدفع إلى main.
التمرين 6: التوسع التلقائي
يدعم ARC التوسع التلقائي بشكل أصلي. يتم إنشاء pods الـ runner عند الطلب وتدميرها عند عدم النشاط. يمكنك تكوين الحد الأدنى والأقصى للنسخ للتحكم في التكلفة والاستجابة.
الخطوة 1: تكوين معلمات التوسع التلقائي
حدّث ملف قيم runner scale set الخاص بك ليشمل معلمات التوسع:
githubConfigUrl: "https://github.com/<org>/<repo>"
githubConfigSecret:
github_token: "<PAT>"
minRunners: 0
maxRunners: 10
template:
spec:
containers:
- name: runner
image: ghcr.io/actions/actions-runner:latest
command: ["/home/runner/run.sh"]
helm upgrade arc-runner-set \
actions-runner-controller/gha-runner-scale-set \
--namespace arc-runners \
-f autoscale-values.yaml
الخطوة 2: توليد الحمل
أنشئ سير عمل يُطلق عدة مهام متوازية:
name: Autoscale Test
on: workflow_dispatch
jobs:
parallel-job:
runs-on: arc-runner-set
strategy:
matrix:
id: [1, 2, 3, 4, 5]
steps:
- name: Simulate work
run: |
echo "Job ${{ matrix.id }} running on $(hostname)"
sleep 60
شغّل سير العمل هذا وراقب توسع الـ pods:
kubectl get pods -n arc-runners -w
سترى خمس pods يتم إنشاؤها — واحدة لكل مهمة matrix:
NAME READY STATUS RESTARTS AGE
arc-runner-set-abcde-runner 1/1 Running 0 5s
arc-runner-set-fghij-runner 1/1 Running 0 5s
arc-runner-set-klmno-runner 1/1 Running 0 5s
arc-runner-set-pqrst-runner 1/1 Running 0 5s
arc-runner-set-uvwxy-runner 1/1 Running 0 5s
بعد اكتمال المهام (60 ثانية)، يتم إنهاء جميع الـ pods. تعود مساحة الأسماء إلى صفر pods.
الخطوة 3: تكوين تأخير تقليص الحجم
لتحسين التكلفة، قد ترغب في بقاء الـ pods جاهزة لفترة قصيرة بعد اكتمال المهمة. هذا يتجنب تأخر البدء البارد لأحمال العمل المتقطعة. سلوك التوسع إلى الصفر في ARC هو الخيار الافتراضي والأكثر أماناً. إذا كنت تحتاج runners جاهزة، اجعل النافذة قصيرة (أقل من 5 دقائق) وتأكد من أن الوضع المؤقت لا يزال مفروضاً.
التمرين 7: سياسات الشبكة لـ Runners
تتيح لك Kubernetes NetworkPolicies تقييد وصول pods الـ runner إلى الشبكة. هذا دفاع حاسم ضد تسريب البيانات من عمليات البناء المخترقة.
الخطوة 1: إنشاء NetworkPolicy
طبّق NetworkPolicy التالية على مساحة الأسماء arc-runners:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: runner-egress-policy
namespace: arc-runners
spec:
podSelector: {}
policyTypes:
- Egress
egress:
# Allow DNS resolution
- to:
- namespaceSelector: {}
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# Allow GitHub API and Actions services
- to:
- ipBlock:
cidr: 140.82.112.0/20
- ipBlock:
cidr: 143.55.64.0/20
- ipBlock:
cidr: 185.199.108.0/22
- ipBlock:
cidr: 4.0.0.0/8
ports:
- protocol: TCP
port: 443
# Allow your container registry (example: ghcr.io)
- to:
- ipBlock:
cidr: 140.82.112.0/20
ports:
- protocol: TCP
port: 443
# Allow your artifact storage (replace with your CIDR)
# - to:
# - ipBlock:
# cidr: 10.0.0.0/8
# ports:
# - protocol: TCP
# port: 443
kubectl apply -f runner-network-policy.yaml
ملاحظة: تنشر GitHub نطاقات IP الخاصة بها على https://api.github.com/meta. استخدم نطاقات actions و api. النطاقات CIDR أعلاه هي أمثلة — تحقق من النطاقات الحالية وحدّثها وفقاً لذلك.
الخطوة 2: اختبار NetworkPolicy
أنشئ سير عمل يحاول الوصول إلى عنوان URL خارجي:
name: Network Policy Test
on: workflow_dispatch
jobs:
test-network:
runs-on: arc-runner-set
steps:
- name: Test GitHub API (should work)
run: curl -s -o /dev/null -w "%{http_code}" https://api.github.com
- name: Test external URL (should be blocked)
run: |
if curl -s --connect-timeout 5 https://evil-exfiltration-server.example.com; then
echo "FAIL: External access was allowed"
exit 1
else
echo "PASS: External access was blocked by NetworkPolicy"
fi
عند تشغيل سير العمل هذا:
- ينجح طلب GitHub API (HTTP 200) لأن NetworkPolicy تسمح بحركة المرور إلى نطاقات IP الخاصة بـ GitHub.
- تنتهي مهلة طلب URL الخارجي ويفشل لأنه ليس في قائمة الخروج المسموح بها.
يمنع هذا عملية بناء مخترقة من تسريب الكود المصدري أو الأسرار أو مخرجات البناء إلى خادم يسيطر عليه المهاجم. حتى لو قامت اعتمادية خبيثة بتشغيل كود عشوائي أثناء البناء، فلا يمكنها الاتصال بالخارج.
التنظيف
أزل جميع الموارد التي تم إنشاؤها خلال هذا المختبر:
# Delete Helm releases
helm uninstall arc-runner-set -n arc-runners
helm uninstall arc-runner-pr -n arc-runners
helm uninstall arc-runner-deploy -n arc-runners
helm uninstall arc -n arc-systems
# Delete namespaces
kubectl delete namespace arc-runners
kubectl delete namespace arc-systems
# Delete the kind cluster
kind delete cluster --name arc-lab
إذا أنشأت GitHub App لهذا المختبر، يمكنك حذفه من Settings → Developer settings → GitHub Apps. قم بإلغاء أي PATs أنشأتها.
النقاط الرئيسية
- تقضي Runners المؤقتة على التلوث بين المهام. تحصل كل مهمة على حاوية جديدة — يتم تدمير الأسرار والرموز ومخرجات البناء عند اكتمال المهمة.
- يوفر ARC فوائد runners ذاتية الاستضافة دون المخاطر الأمنية. تحصل على أدوات مخصصة ووصول إلى شبكة خاصة والتحكم في التكاليف مع الحفاظ على نموذج الأمان المؤقت.
- تمنحك صور runner المخصصة تحكماً كاملاً في بيئة البناء. ثبّت إصدارات الأدوات، وافحص الثغرات الأمنية، وتخلص من مخاطر سلسلة التوريد من البرامج المثبتة مسبقاً.
- يطبّق عزل مجموعات runner فصل المهام. تعمل سير عمل التحقق من طلبات السحب والنشر على مجموعات runner منفصلة بصلاحيات ووصول شبكي مختلف.
- سياسات الشبكة هي طبقة دفاع حاسمة. يمنع تقييد خروج runner تسريب البيانات حتى لو تم اختراق خطوة بناء.
- يقلل التوسع التلقائي إلى الصفر من التكلفة وسطح الهجوم. توجد pods الـ runner فقط لمدة المهمة — لا توجد بنية تحتية دائمة للصيانة أو التأمين.
الخطوات التالية
واصل تعزيز وضعك الأمني لـ CI/CD مع هذه الأدلة ذات الصلة:
- تأمين GitHub Actions Runners — تعمق في أفضل ممارسات أمان runner وإدارة الرموز والمراقبة لكل من GitHub-hosted و self-hosted runners.
- فصل المهام ومبدأ الصلاحيات الأقل في خطوط أنابيب CI/CD — دليل شامل لتطبيق مبادئ الصلاحيات الأقل عبر خط أنابيب CI/CD بالكامل، من التحكم في المصدر إلى نشر الإنتاج.