أساسيات التكامل المستمر

الأسرار والأمان في التكامل المستمر

22 دقيقة الدرس 8 من 28

الأسرار والأمان في التكامل المستمر

تعمل خطوط أنابيب CI مع وصول إلى حسابات سحابتك الإنتاجية، وسجلات الحاويات، ومفاتيح التوقيع، وبيانات اعتماد النشر. هذا يجعلها واحدة من أكثر الأهداف قيمة في بنيتك الهندسية بأكملها. اختراق Solar Winds لسلسلة التوريد، وسرقة بيانات اعتماد Codecov، وعشرات تقارير ما بعد الحوادث المنشورة، تشترك جميعها في خيط واحد: مهاجم وصل إلى نظام البناء ووجد طريقه مباشرة إلى الإنتاج. يغطي هذا الدرس كيف تحمي الفرق المتقدمة الأسرار وتُحصّن خطوط أنابيب CI حتى لا يتحول Runner مخترق إلى حادثة على مستوى الشركة.

لماذا الأسرار المُدوَّنة في الكود تُشكّل فئة ثغرة قائمة بذاتها

النهج الساذج — لصق رمز مميز في ملف سير العمل أو في .env مُودَع في المستودع — شائع جداً لدرجة أن المسح الآلي (GitHub Secret Scanning، TruffleHog، Gitleaks) يجعل منه وظيفة متفرغة. حتى لو حذفت السر من الملف، يحتفظ تاريخ Git به إلى الأبد ما لم تُعيد كتابة التاريخ وتُدوّر بيانات الاعتماد. في شركات التقنية الكبرى السياسة مطلقة: لا يلمس أي سر مستودع المصدر، حتى في مستودع خاص، حتى مشفراً، حتى مُرمَّزاً بـ base64. التعتيم ليس أماناً.

خطأ شائع في الإنتاج — هجوم PR من Fork: في GitHub Actions، سير العمل الذي يُشغَّل عبر pull_request من Fork يعمل في سياق الـ Fork — ولا يمكنه الوصول إلى أسرار المستودع بشكل افتراضي. لكن إذا استخدمت pull_request_target (الذي يعمل في سياق المستودع الأساسي) ثم سحبت كود PR، فقد منحت المساهم وصولاً كاملاً لقراءة كل سر في مؤسستك. هذا أحد أكثر ناقلات تصعيد الصلاحيات شيوعاً في CI.

مخازن الأسرار: أين تعيش الأسرار فعلياً

كل منصة CI كبرى تأتي بمخزن أسرار مدمج. تُشفّر هذه المخازن الأسرار عند الراحة، وتحقنها في المهام كمتغيرات بيئة أو ملفات، والأهم — تُخفي قيمها في مخرجات السجل حتى لا يمكن سرقتها عبر خطوط تجميع السجلات.

أسرار GitHub Actions هي خط الأساس. أسرار المستودع مقيّدة بمستودع واحد؛ أسرار البيئة تُضيف بوابة موافقة (تتطلب مراجعاً قبل النشر على production)؛ أسرار المؤسسة يمكن مشاركتها عبر مستودعات بقوائم سماح صريحة.

# .github/workflows/deploy.yml name: Deploy on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest environment: production # بوابة على قواعد حماية البيئة steps: - uses: actions/checkout@v4 - name: Configure AWS credentials via OIDC uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy aws-region: us-east-1 - name: Log in to ECR run: | aws ecr get-login-password --region us-east-1 \ | docker login --username AWS --password-stdin \ 123456789012.dkr.ecr.us-east-1.amazonaws.com - name: Deploy env: # السر مُحقَن من GitHub Secrets — لا يُطبع في السجلات DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }} run: ./scripts/deploy.sh

للفرق التي تحتاج إدارة مركزية للأسرار عبر منصات CI متعددة وموفري السحابة وبيئات وقت التشغيل، يُعدّ مدير أسرار مخصص المعيار الإنتاجي: HashiCorp Vault، أو AWS Secrets Manager، أو GCP Secret Manager، أو Azure Key Vault. تُجلب الأسرار عند وقت تشغيل المهمة بتوكن قصير الصلاحية؛ ولا تُخزَّن أبداً في منصة CI نفسها.

# جلب سر من HashiCorp Vault داخل مهمة GitHub Actions # المتطلبات: hashicorp/vault-action، طريقة مصادقة Vault JWT مُكوَّنة - name: Import secrets from Vault uses: hashicorp/vault-action@v3 with: url: https://vault.internal.company.com method: jwt role: github-actions secrets: | secret/data/production/db password | DB_PASSWORD ; secret/data/production/api key | API_KEY - name: Use secrets run: | echo "DB_PASSWORD is available as an env var (masked in logs)" ./migrate --db-password "$DB_PASSWORD"
ممارسة احترافية — تدوير الأسرار: تعامل مع أسرار CI ككلمات مرور: دوّرها بجدول منتظم (ربع سنوي كحد أدنى) وفوراً بعد مغادرة أي عضو فريق يملك وصولاً. الأتمتة تجعل هذا بدون جهد: Vault يدعم التدوير التلقائي لمفاتيح AWS IAM، وبيانات اعتماد قواعد البيانات، وشهادات PKI. AWS Secrets Manager يمكنه تدوير كلمات مرور RDS تلقائياً بجدول قابل للتكوين عبر Lambda function.

بيانات الاعتماد الأقل صلاحية في CI

يجب أن تكون كل بيانات اعتماد CI مقيّدة بالحد الأدنى من الصلاحيات اللازمة لتلك المهمة المحددة — لا أكثر. هذا هو مبدأ الصلاحية الأقل مطبّقاً على خطوط الأنابيب. من الناحية العملية:

  • بيانات اعتماد منفصلة لكل مرحلة: المهمة التي تُشغّل الاختبارات تحتاج صلاحية قراءة لسجل الحزم. المهمة التي تدفع صورة Docker تحتاج صلاحية كتابة على ECR. المهمة التي تنشر تحتاج AssumeRole على دور النشر فقط. يجب أن تكون هذه ثلاثة بيانات اعتماد مختلفة، وليس توكناً واحداً ذا صلاحيات شاملة.
  • تقييد النطاق بالفروع والبيئات: بيانات الاعتماد التي يمكنها الكتابة على الإنتاج يجب أن تكون متاحة فقط في المهام المُشغَّلة من main وفقط في بيئة production (مع بوابات الموافقة).
  • التوكنات قصيرة الصلاحية تفضّل على مفاتيح API طويلة الصلاحية: مفتاح API لا تنتهي صلاحيته أبداً هو التزام يتفاقم مع الوقت. توكنات OIDC تنتهي في دقائق. توكنات AWS STS AssumeRole تنتهي في 1-12 ساعة. فضّلها.

OIDC: الطريقة الصحيحة لمصادقة CI على السحابة

يُزيل OpenID Connect (OIDC) بيانات الاعتماد طويلة الصلاحية كلياً. بدلاً من تخزين مفتاح AWS في GitHub Secrets، تُكوّن AWS لـتثق بموفر هوية GitHub. عندما تعمل مهمة ما، يُصدر GitHub للـ Runner توكن JWT موقّعاً يحتوي على ادعاءات مُتحقَّق منها: اسم المستودع، والفرع، والبيئة، واسم سير العمل. تتبادل AWS هذا JWT بتوكن STS مؤقت. لا يُخزَّن أي سر في أي مكان — يُثبت خط الأنابيب هويته تشفيرياً.

OIDC Token Exchange Flow for CI to Cloud Auth CI Runner GitHub Actions Job context GitHub OIDC Identity Provider /.well-known/jwks AWS STS AssumeRoleWithWebIdentity Validates JWT signature IAM Role github-actions-deploy Least-privilege policy 1. طلب OIDC token 2. JWT موقّع (ادعاءات المستودع) 3. بيانات STS مؤقتة (تنتهي خلال ساعة) 4. Runner يستخدم بيانات STS لاستدعاءات AWS API لا سر دائم مُخزَّن في أي مكان على GitHub
تبادل توكن OIDC — يُثبت Runner هويته بـ JWT موقّع؛ تُصدر AWS بيانات اعتماد مؤقتة مقيّدة بدور IAM. لا يُخزَّن أي سر ثابت.

يتطلب إعداد OIDC بين GitHub Actions وAWS ثلاث خطوات لمرة واحدة:

# 1. إنشاء موفر GitHub OIDC في AWS (مثال Terraform) resource "aws_iam_openid_connect_provider" "github" { url = "https://token.actions.githubusercontent.com" client_id_list = ["sts.amazonaws.com"] # بصمة إبهام GitHub OIDC thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"] } # 2. إنشاء دور IAM بأقل صلاحية مع سياسة ثقة مقيّدة بمستودعك resource "aws_iam_role" "github_actions_deploy" { name = "github-actions-deploy" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [{ Effect = "Allow" Principal = { Federated = aws_iam_openid_connect_provider.github.arn } Action = "sts:AssumeRoleWithWebIdentity" Condition = { StringEquals = { "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com" } StringLike = { # تقييد بالفرع الرئيسي لهذا المستودع المحدد فقط "token.actions.githubusercontent.com:sub" = "repo:myorg/myrepo:ref:refs/heads/main" } } }] }) } # 3. إرفاق سياسة بأقل صلاحية — ECR push + ECS deploy فقط resource "aws_iam_role_policy" "deploy_policy" { name = "deploy-policy" role = aws_iam_role.github_actions_deploy.id policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = [ "ecr:GetAuthorizationToken", "ecr:BatchCheckLayerAvailability", "ecr:PutImage", "ecr:InitiateLayerUpload", "ecr:UploadLayerPart", "ecr:CompleteLayerUpload" ] Resource = "*" }, { Effect = "Allow" Action = ["ecs:UpdateService", "ecs:DescribeServices"] Resource = "arn:aws:ecs:us-east-1:123456789012:service/prod/*" } ] }) }
فكرة أساسية — تحديد نطاق sub claim: يُرمّز ادعاء sub في JWT الـ OIDC السياق الدقيق: repo:org/repo:ref:refs/heads/main للدفعات إلى main، وrepo:org/repo:environment:production للمهام المقيّدة بالبيئة. قيّد دائماً سياسة ثقة IAM بأكثر نمط sub تقييداً ممكناً. حرف بدل مثل repo:myorg/* يعني أن أي مستودع في مؤسستك يمكنه افتراض دور النشر — تكوين خاطئ بالغ الخطورة.

المتغيرات المُخفية ونظافة السجلات

تُخفي منصات CI تلقائياً قيم الأسرار في مخرجات السجل: إذا ظهرت قيمة سر في stdout أو stderr، تستبدلها المنصة بـ ***. هذا هو خط الدفاع الأخير، وليس الأول. عدة أنماط فشل تتجاوزه:

  • حيل الترميز: ترميز سر بـ base64 أو URL قبل طباعته لن يُخفى. المهاجمون الذين يمكنهم تنفيذ خطوات تعسفية في خط أنابيبك يعرفون ذلك.
  • التقسيم عبر أسطر: بعض منصات CI تُطابق النص الحرفي فقط. سر مُقسَّم عبر عدة تعليمات طباعة قد لا يُكتشف.
  • الإجراءات من طرف ثالث: إجراء يملك وصولاً إلى GITHUB_TOKEN أو أي متغير بيئة مُحقَن يمكنه سرقته عبر الشبكة، متجاوزاً إخفاء السجل كلياً. دائماً قيّد الإجراءات من طرف ثالث بـ SHA كامل للإيداع، وليس بوسم — الوسوم قابلة للتغيير ويمكن اختطافها.
# سيء: استخدام وسم قابل للتغيير — يمكن استبدال الإجراء من تحتك - uses: some-org/some-action@v3 # جيد: تثبيت على SHA محدد للإيداع — ثابت - uses: some-org/some-action@a81bbbf8298c0fa03ea29cdc473d45769f953675 # جيد أيضاً: استخدام Dependabot لإبقاء تبعيات SHA المُثبَّتة محدّثة # .github/dependabot.yml version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly"

اكتشاف تسريبات الأسرار قبل وصولها إلى المستودع

أرخص إصلاح هو اكتشاف سر مُسرَّب قبل إيداعه على الإطلاق. أداتان متكاملتان:

  • خطافات pre-commit مع Gitleaks أو detect-secrets: تفحص التغييرات المرحلية قبل إنشاء الإيداع. تعمل في ملي ثواني. تكتشف الأسرار محلياً قبل لمسها للبُعيد.
  • خطوة مسح أسرار في CI: Gitleaks كخطوة مهمة في CI يفحص كل فرق طلب السحب. يعمل Secret Scanning المدمج في GitHub على كل دفعة إلى المستودع ويمكنه حجب الدفعات عبر حماية الدفع.
# .github/workflows/security.yml — مسح أسرار Gitleaks على كل PR name: Secret Scan on: pull_request: jobs: gitleaks: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # التاريخ الكامل — مسح جميع الإيداعات في PR - name: Run Gitleaks uses: gitleaks/gitleaks-action@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
ممارسة احترافية — خطافات pre-commit على نطاق واسع: طبّق خطافات pre-commit عبر مؤسسة هندسية كبيرة باستخدام إطار pre-commit (Python) مع تكوين مشترك في مستودع مركزي. اطلب gitleaks وdetect-secrets كخطافات. اجعل تثبيت الخطاف إلزامياً في سكريبت إعداد المطوّر. هذا ينقل الاكتشاف يساراً بأيام — قبل PR، قبل مراجعة الكود، قبل تشغيل أي CI.

أنماط الفشل الشائعة على نطاق الإنتاج

  • الأسرار في وسيطات البناء: docker build --build-arg API_KEY=$API_KEY يخبّئ القيمة في تاريخ طبقة الصورة. أي شخص يسحب الصورة يمكنه تشغيل docker history --no-trunc وقراءتها. استخدم بنيات متعددة المراحل واحقن الأسرار عبر BuildKit: RUN --mount=type=secret,id=api_key ...
  • ثقة OIDC ذات نطاق واسع: نسيان تحديد نطاق ادعاء sub يعني أن أي فرع أو PR يمكنه افتراض دور نشر الإنتاج. قيّده بـ ref:refs/heads/main واستخدم قواعد حماية البيئة.
  • تسجيل متغيرات البيئة الحساسة: خطوة تشخيص مثل env | sort ستطبع كل متغير بيئة — بما فيها الأسرار المُحقَنة — في السجل. أزل خطوات التشخيص قبل الدمج؛ والأفضل ألا تطبع البيئة بأكملها أبداً.
  • مجموعات Runners المشتركة: على Runners عامة في GitHub Actions، تحصل كل مهمة على آلة افتراضية مؤقتة جديدة. على Runners ذاتية الاستضافة، يمكن لمهمة خبيثة ترك ملفات أو تعديلات بيئية تتسرب إلى المهمة التالية على نفس Runner. استخدم Runners مؤقتة ذاتية الاستضافة (يسجّل Runner، يُنفّذ مهمة واحدة، ثم ينتهي) أو اعزل Runners لكل بيئة.