نظرة عامة
أصبح GitHub Actions منصة CI/CD الأكثر استخدامًا على نطاق واسع للبرمجيات مفتوحة المصدر والتجارية على حد سواء. هذه الشعبية تجعله سطح الهجوم الأول في بيئة CI/CD. تقوم سير العمل المُعدَّة بشكل خاطئ بتسريب الأسرار بشكل منتظم، ومنح صلاحيات مفرطة، وسحب شفرات طرف ثالث يمكن التلاعب بها بصمت.
في هذا المختبر العملي ستقوم بتعزيز أمان سير عمل GitHub Actions غير آمن عمدًا باستخدام أكثر ثلاث تقنيات تأثيرًا متاحة اليوم:
- الصلاحيات الدنيا — تقييد
GITHUB_TOKENليشمل فقط النطاقات التي يحتاجها كل job فعليًا. - تثبيت SHA — الإشارة إلى كل action تابع لطرف ثالث بواسطة SHA الخاص بالـ commit الثابت بدلاً من tag قابل للتغيير.
- حماية الأسرار — تحديد نطاق الأسرار ضمن بيئات مع بوابات موافقة ومنع التسريب من خلال pull requests القادمة من الـ forks.
بنهاية المختبر سيكون لديك قالب سير عمل جاهز للإنتاج يمكنك استخدامه في أي مستودع.
المتطلبات الأساسية
- حساب GitHub مع صلاحية إنشاء المستودعات.
- معرفة أساسية بصياغة YAML في GitHub Actions (المحفزات، الوظائف، الخطوات).
- تثبيت
ghCLI (اختياري لكنه مفيد للاستعلام عن SHA الخاص بالـ actions).
إعداد البيئة
إنشاء مستودع تجريبي
أنشئ مستودعًا عامًا جديدًا على GitHub باسم gha-hardening-lab. يمكنك القيام بذلك من خلال واجهة المستخدم أو باستخدام سطر الأوامر:
gh repo create gha-hardening-lab --public --clone
cd gha-hardening-lab
قم بتهيئة مشروع Node.js بسيط ليكون لسير العمل شيء يبنيه:
npm init -y
cat <<'EOF' > index.js
console.log("Hello from the hardening lab");
EOF
git add -A && git commit -m "Initial commit" && git push
سير العمل الأولي (غير الآمن)
أنشئ الملف .github/workflows/build.yml بالمحتوى التالي. سير العمل هذا غير آمن عمدًا — لا يحتوي على كتلة permissions، ويستخدم tags قابلة للتغيير، ويكشف الأسرار بشكل واسع:
# .github/workflows/build.yml — نقطة البداية غير الآمنة
name: Build
on:
push:
pull_request_target:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm install
- run: npm test
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
- uses: actions/upload-artifact@v4
with:
name: build-output
path: .
قم بعمل commit ودفع هذا الملف. سيعمل بنجاح، لكنه يحتوي على خمس مشاكل أمنية على الأقل ستقوم بإصلاحها في التمارين أدناه.
التمرين 1: الصلاحيات الدنيا
مشكلة الصلاحيات الافتراضية
عندما لا يُعلن سير العمل عن كتلة permissions، يحصل GITHUB_TOKEN على الصلاحيات الافتراضية للمستودع. بالنسبة لمعظم المستودعات يعني ذلك صلاحيات القراءة والكتابة لكل نطاق — contents وpackages وissues وpull requests وdeployments وغيرها. إذا اخترق مهاجم أي خطوة في سير العمل هذا، فسيرث جميع تلك الصلاحيات.
يتطلب مبدأ الحد الأدنى من الامتيازات أن تمنح فقط الصلاحيات التي يحتاجها كل job فعليًا، ولا شيء غير ذلك.
الخطوة 1 — تعيين قيمة افتراضية مقيّدة على المستوى الأعلى
أضف مفتاح permissions على المستوى الأعلى مباشرة بعد كتلة on:. هذا يحدد القيمة الافتراضية لكل job في سير العمل:
permissions:
contents: read
إذا أردت البدء بأكثر قيمة افتراضية تقييدًا ثم منح الصلاحيات لكل job على حدة، يمكنك استخدام خريطة فارغة:
permissions: {}
الخطوة 2 — إضافة صلاحيات لكل Job
يمكن لكل job تجاوز القيمة الافتراضية على مستوى سير العمل. امنح فقط ما يحتاجه الـ job:
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read # سحب الشفرة
actions: read # قراءة بيانات سير العمل الوصفية
steps:
- uses: actions/checkout@v4
# ...
إذا كان job ثانٍ يحتاج لرفع أصل إصدار، ستمنحه contents: write على ذلك الـ job فقط — وليس أبدًا على مستوى سير العمل.
قبل وبعد
قبل (غير آمن):
name: Build
on:
push:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install
بعد (مُعزَّز):
name: Build
on:
push:
branches: [main]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
steps:
- uses: actions/checkout@v4
- run: npm install
التحقق من الصلاحيات الفعلية
بعد تشغيل سير العمل، افتح الـ job في تبويب Actions. انقر على أيقونة الترس في أعلى يمين سجل الـ job واختر “Set up job”. وسّع هذا القسم لرؤية صلاحيات GITHUB_TOKEN الدقيقة التي تم منحها. تأكد أن contents: read وactions: read فقط تظهران.
يمكنك أيضًا الاستعلام عن الصلاحيات برمجيًا داخل خطوة:
- name: Print token permissions
run: |
curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
https://api.github.com/repos/${{ github.repository }} \
| jq '.permissions'
التمرين 2: تثبيت Actions بواسطة SHA
لماذا تُعد Tags خطيرة
عندما تكتب uses: actions/checkout@v4، فأنت تشير إلى Git tag. الـ Tags قابلة للتغيير — يمكن لمشرف الـ action (أو مهاجم يخترق حسابه) حذف وإعادة إنشاء الـ tag ليشير إلى شفرة مختلفة تمامًا. سيقوم سير العمل الخاص بك بعد ذلك بتنفيذ الشفرة الجديدة بصمت في التشغيل التالي. يزيل تثبيت SHA هذا الخطر لأن SHA الخاص بالـ commit ثابت ولا يمكن تغييره.
الخطوة 1 — إيجاد SHA الخاص بـ Action
استخدم gh CLI لتحويل tag إلى SHA الخاص بالـ commit:
# تحويل actions/checkout@v4 إلى SHA الخاص بالـ commit
gh api repos/actions/checkout/git/ref/tags/v4 --jq '.object.sha'
إذا كان الـ tag مُشروحًا (معظمها كذلك)، يُرجع الأمر أعلاه SHA الخاص بكائن الـ tag. تحتاج لإلغاء مرجعيته للحصول على الـ commit:
TAG_SHA=$(gh api repos/actions/checkout/git/ref/tags/v4 --jq '.object.sha')
gh api repos/actions/checkout/git/tags/$TAG_SHA --jq '.object.sha'
بدلاً من ذلك، قم بزيارة مستودع الـ action على GitHub، انقر على الـ tag، وانسخ SHA الكامل للـ commit من الرابط أو رأس الـ commit.
الخطوة 2 — تثبيت Actions الشائعة
استبدل كل tag قابل للتغيير بـ SHA الكامل المكون من 40 حرفًا. أضف دائمًا تعليقًا في النهاية يحتوي الإصدار لسهولة القراءة:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: build-output
path: dist/
الخطوة 3 — أتمتة تحديثات SHA باستخدام Dependabot
التثبيت بواسطة SHA يعني أنك لن تحصل على تحديثات تلقائية قائمة على الـ tags. يحل Dependabot هذه المشكلة بفتح pull requests عندما ينشر action مثبت إصدارًا جديدًا.
أنشئ الملف .github/dependabot.yml:
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "ci"
بعد دفع هذا الملف، سيقوم Dependabot بفحص سير عملك أسبوعيًا وفتح PRs لتحديث SHA المثبتة. يُظهر كل PR الفرق في شفرة الـ action، مما يمنحك فرصة للمراجعة قبل الدمج.
إذا كنت تفضل Renovate على Dependabot، أضف ملف renovate.json في جذر المستودع:
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"github-actions": {
"enabled": true
}
}
التمرين 3: حماية الأسرار
أسرار المستودع مقابل أسرار البيئة
يوفر GitHub مستويين لتخزين الأسرار:
- أسرار المستودع — متاحة لكل سير عمل وكل job في المستودع. مريحة لكنها واسعة النطاق بشكل مفرط.
- أسرار البيئة — متاحة فقط للـ jobs التي تُعلن صراحة عن
environment: <name>. هذا هو النهج الموصى به لبيانات الاعتماد الحساسة.
الخطوة 1 — إنشاء بيئة مع قواعد حماية
في مستودعك، انتقل إلى Settings → Environments وأنشئ بيئة تسمى production. قم بتمكين قواعد الحماية التالية:
- المراجعون المطلوبون — أضف عضوًا واحدًا على الأقل من الفريق يجب عليه الموافقة على عمليات النشر.
- مؤقت الانتظار — أضف اختياريًا تأخيرًا (مثلاً 5 دقائق) لمنح المراجعين وقتًا.
- فروع النشر — قيّد بفرع
mainفقط.
الآن أضف DEPLOY_TOKEN الخاص بك كسر داخل هذه البيئة، وليس على مستوى المستودع.
الخطوة 2 — الإشارة إلى البيئة في سير العمل
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
permissions:
contents: read
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Deploy
run: ./deploy.sh
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
إعلان environment: production يعني أن هذا الـ job سيتوقف وينتظر موافقة مراجع قبل تشغيل أي خطوة. سر DEPLOY_TOKEN متاح فقط داخل هذه البيئة — لا يمكن الوصول إليه من jobs أو سير عمل أخرى لا تُعلن عن هذه البيئة.
الخطوة 3 — فهم سلوك الـ Forks
الأسرار غير متاحة لسير العمل المُحفَّز بأحداث pull_request من الـ forks. هذا حد أمني حاسم. إذا أنشأت سير عمل يعتمد على الأسرار أثناء فحوصات PR، سيفشل للمساهمين الخارجيين:
# هذه الخطوة ستفشل لـ PRs من الـ forks لأن DEPLOY_TOKEN فارغ
- name: Authenticated API call
run: |
curl -H "Authorization: Bearer $DEPLOY_TOKEN" https://api.example.com/health
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
هذا بالتصميم — إنه يمنع الـ forks الخبيثة من تسريب أسرارك.
الخطوة 4 — خطر pull_request_target
يعمل محفز pull_request_target في سياق المستودع الأساسي، مما يعني أنه يملك حق الوصول إلى الأسرار. هذا خطير للغاية إذا قمت أيضًا بسحب شفرة رأس الـ PR:
# خطير — لا تفعل هذا
on:
pull_request_target:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }} # يسحب شفرة غير موثوقة
- run: npm install # ينفذ شفرة يتحكم بها المهاجم مع الوصول إلى الأسرار
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
يمكن للمهاجم تعديل package.json ليتضمن سكريبت postinstall يسرّب DEPLOY_TOKEN. لا تجمع أبدًا بين pull_request_target وسحب رأس الـ PR إلا إذا قمت بالتحقق من الشفرة وعزلها صراحة.
البديل الآمن: استخدم محفز pull_request القياسي لسير عمل البناء والاختبار. احتفظ بـ pull_request_target فقط لسير عمل التصنيف أو التعليق التي لا تنفذ أبدًا شفرة الـ PR.
ملخص أفضل الممارسات
- خزّن الأسرار الحساسة في البيئات، وليس على مستوى المستودع.
- أضف مراجعين مطلوبين وقيود فروع لكل بيئة تحتوي على بيانات اعتماد الإنتاج.
- استخدم محفز
pull_requestلـ CI. تجنبpull_request_targetإلا إذا كنت تفهم تمامًا آثار الثقة. - صمّم سير العمل بحيث تكون الـ jobs التي تحتاج أسرارًا منفصلة عن الـ jobs التي تشغل شفرة غير موثوقة.
التمرين 4: تعزيز إضافي
منع التشغيلات المكررة باستخدام Concurrency
بدون سياسة concurrency، يؤدي دفع عدة commits بتتابع سريع إلى تشغيل عدة عمليات سير عمل تهدر الموارد ويمكن أن تسبب حالات سباق أثناء النشر. أضف كتلة concurrency على مستوى سير العمل:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
هذا يلغي أي تشغيل جارٍ لنفس سير العمل والفرع عند دفع commit جديد.
تعيين حدود المهلة الزمنية
يمكن لـ job معلّق أن يستهلك دقائق المشغّل إلى أجل غير مسمى. عيّن دائمًا مهلة زمنية صريحة:
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 15
اختر قيمة تمنح بناءك هامشًا كافيًا لكن تمنع العمليات الجامحة. لمعظم بناءات Node.js أو Go، تعد 10 إلى 20 دقيقة كافية.
تقييد محفزات سير العمل
تجنب المحفزات المجردة التي تعمل على كل فرع:
# واسع جدًا — يعمل على كل push لكل فرع
on:
push:
بدلاً من ذلك، حدد نطاق المحفزات للفروع المهمة:
on:
push:
branches: [main]
pull_request:
branches: [main]
هذا يقلل التشغيلات غير الضرورية ويحد من سطح الهجوم لهجمات الحقن القائمة على الفروع.
التنفيذ المشروط للخطوات الحساسة
استخدم شروط if: لمنع الخطوات الحساسة من التشغيل في سياقات لا ينبغي أن تعمل فيها:
- name: Deploy to production
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: ./deploy.sh
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
هذا يضمن أن خطوة النشر تعمل فقط عند الدفع إلى main، وليس أبدًا على pull requests أو فروع أخرى، حتى لو تم تشغيل الـ job نفسه.
سير العمل المُعزَّز النهائي
فيما يلي سير العمل المُعزَّز الكامل إلى جانب الأصلي. كل تحسين أمني مُشروح بتعليق.
الأصلي (غير الآمن)
name: Build
on:
push:
pull_request_target:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm install
- run: npm test
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
- uses: actions/upload-artifact@v4
with:
name: build-output
path: .
المُعزَّز
name: Build
# مُعزَّز: محفزات محددة النطاق — فرع main فقط، محفز PR آمن
on:
push:
branches: [main]
pull_request:
branches: [main]
# مُعزَّز: صلاحيات افتراضية مقيّدة لجميع الـ jobs
permissions:
contents: read
# مُعزَّز: إلغاء التشغيلات المكررة
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
# مُعزَّز: مهلة زمنية صريحة
timeout-minutes: 15
# مُعزَّز: صلاحيات لكل job (الحد الأدنى من الامتيازات)
permissions:
contents: read
actions: read
steps:
# مُعزَّز: جميع الـ actions مثبتة بواسطة SHA
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20
- run: npm install
- run: npm test
# مُعزَّز: لا أسرار مكشوفة في job البناء/الاختبار
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: build-output
path: dist/
deploy:
needs: build
runs-on: ubuntu-latest
# مُعزَّز: يعمل فقط عند الدفع إلى main
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
# مُعزَّز: الأسرار محمية خلف بيئة مع مراجعين مطلوبين
environment: production
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Deploy
run: ./deploy.sh
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
كسر العمل (الفشل المتعمد)
لترسيخ فهمك، قم بكسر سير العمل المُعزَّز عمدًا وراقب العواقب.
الاختبار 1 — إزالة كتلة الصلاحيات
احذف مفتاح permissions: على المستوى الأعلى وصلاحيات كل job. ادفع وشغّل سير العمل. سيستمر بالنجاح، لكن إذا فحصت خطوة إعداد الـ job، سترى أن الرمز المميز لديه الآن صلاحيات القراءة والكتابة لكل نطاق. يمكن لخطوة مخترقة دفع شفرة أو حذف فروع أو تعديل الإصدارات.
الاختبار 2 — استخدام Action غير مثبت
غيّر أحد الـ actions مرة أخرى إلى مرجع tag:
- uses: actions/checkout@v4
سير العمل لا يزال يعمل. لكن إذا تم نقل tag v4 إلى commit خبيث، سيقوم سير العمل بتنفيذ تلك الشفرة دون تحذير. لا يوجد سجل تدقيق — الـ tag ببساطة يشير إلى SHA مختلف. أعد تثبيته إلى SHA بعد هذا الاختبار.
الاختبار 3 — الوصول إلى أسرار الإنتاج من PR
أنشئ فرع ميزة وافتح pull request. لن يعمل job deploy بسبب شرط if:. حتى لو أزلت الشرط، فإن سر البيئة DEPLOY_TOKEN محمي خلف بيئة production، التي تقيّد النشر بفرع main وتتطلب موافقة المراجع. ستكون قيمة السر فارغة في سياق PR.
هذا هو بالضبط السلوك الذي تريده — الأسرار لا تكون متاحة أبدًا في سياقات غير موثوقة.
التنظيف
عند الانتهاء من المختبر، احذف المستودع التجريبي لتجنب ازدحام حسابك:
gh repo delete gha-hardening-lab --yes
إذا استخدمت fork لمشروع موجود، يمكنك إعادة تعيينه بدلاً من ذلك:
git checkout main
git reset --hard origin/main
git push --force
النقاط الرئيسية
- أعلن دائمًا عن كتلة
permissions. عيّن قيمة افتراضية مقيّدة على مستوى سير العمل وامنح نطاقات إضافية لكل job فقط حسب الحاجة. - ثبّت كل action تابع لطرف ثالث بواسطة SHA الكامل. الـ Tags قابلة للتغيير ويمكن إعادة توجيهها بصمت إلى شفرة خبيثة.
- استخدم Dependabot أو Renovate لإبقاء SHA المثبتة محدثة تلقائيًا.
- خزّن الأسرار الحساسة في البيئات مع مراجعين مطلوبين وقيود فروع — وليس أبدًا على مستوى المستودع.
- استخدم
pull_requestوليسpull_request_targetلسير العمل التي تبني أو تختبر شفرة PR. محفزpull_request_targetيمنح الوصول إلى الأسرار لشفرة قد تكون غير موثوقة. - أضف
concurrencyوtimeout-minutesومحفزات محددة الفروع لتقليل هدر الموارد وتقليص سطح الهجوم.
الخطوات التالية
واصل بناء معرفتك بأمان CI/CD مع هذه الأدلة ذات الصلة:
- نماذج تنفيذ CI/CD وافتراضات الثقة — فهم كيف تُنمذج منصات CI/CD المختلفة الثقة وأين تنهار الحدود.
- فصل المهام والحد الأدنى من الامتيازات في خطوط أنابيب CI/CD — تصميم خطوط أنابيب حيث لا يملك أي فاعل أو بيانات اعتماد منفردة وصولاً أكثر من اللازم.