بيانات الاعتماد قصيرة العمر و Workload Identity Federation في أنابيب CI/CD

مقدمة

إذا قمت بتدقيق مخازن الأسرار في معظم منصات CI/CD اليوم، ستجد مقبرة من بيانات الاعتماد طويلة العمر: مفاتيح وصول AWS أُنشئت منذ سنوات، ومفاتيح JSON لحسابات خدمة GCP مشتركة عبر عشرات الأنابيب، و GitHub Personal Access Tokens بصلاحيات واسعة، وكلمات مرور قواعد بيانات لم يتم تدويرها أبداً. هذه الأسرار الثابتة هي أكثر ناقلات الهجوم شيوعاً في اختراقات CI/CD.

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

يغيّر Workload Identity Federation المعادلة بالكامل. بدلاً من حقن أسرار ثابتة في تشغيلات الأنابيب، تصبح منصة CI نفسها مزوداً للهوية. يتلقى كل تشغيل أنبوب رمزاً مميزاً قصير العمر وموقّعاً تشفيرياً يثبت ما الذي يعمل (أي مستودع، وأي فرع، وأي سير عمل، وأي بيئة). يتحقق مزودو السحابة من صحة هذا الرمز ويصدرون بيانات اعتماد مؤقتة محددة بالصلاحيات المطلوبة بالضبط — بيانات اعتماد تنتهي صلاحيتها في دقائق، وليس أشهر.

يتناول هذا الدليل المشكلة بالتفصيل، ويشرح كيف يعمل Workload Identity Federation على مستوى البروتوكول، ويقدم أمثلة عملية كاملة لـ GitHub Actions و GitLab CI عبر AWS و GCP و Azure، ويغطي الأنماط المتقدمة، ويتضمن دليل ترحيل عملي للفرق المستعدة للتخلص من أسرارها طويلة العمر.

مشكلة بيانات الاعتماد طويلة العمر

قبل الخوض في الحل، من المفيد فهم سبب خطورة بيانات الاعتماد طويلة العمر في سياقات CI/CD تحديداً — وليس بشكل عام فقط.

عدم وجود انتهاء صلاحية أو تدوير تلقائي

مفتاح وصول AWS IAM، بمجرد إنشائه، يظل صالحاً إلى الأبد ما لم يتم إلغاؤه صراحةً. مفتاح JSON لحساب خدمة GCP ليس له تاريخ انتهاء صلاحية. يمكن تعيين GitHub PAT بحيث لا تنتهي صلاحيته أبداً. في الممارسة العملية، تنشئ معظم الفرق بيانات الاعتماد هذه مرة واحدة أثناء الإعداد الأولي ولا تلمسها مرة أخرى. متوسط عمر السر في CI/CD في معظم المؤسسات يُقاس بالسنوات.

هذا يعني أنه حتى لو تم تسريب بيانات اعتماد قبل ستة أشهر، فإنها لا تزال صالحة اليوم. يعرف المهاجمون هذا ويفحصون بشكل روتيني المستودعات العامة وصور Docker وسجلات CI بحثاً عن بيانات اعتماد قد تكون قد تعرضت في أي وقت من التاريخ.

نطاق انفجار واسع

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

صعوبة التدقيق

عندما يُستخدم نفس مفتاح حساب الخدمة عبر 50 مستودعاً و200 أنبوب وثلاث بيئات، يصبح من شبه المستحيل الإجابة على أسئلة الأمان الأساسية:

  • أي أنبوب قام بهذا الاستدعاء لواجهة API الإنتاجية في الساعة 3 صباحاً؟
  • هل تم استخدام بيانات الاعتماد هذه من مُشغّل CI مصرّح به أم من جهاز مهاجم؟
  • أي المستودعات لا تزال تعتمد على هذا المفتاح إذا احتجنا لتدويره؟

لا توفر بيانات الاعتماد طويلة العمر أي معلومات سياقية حول من أو ما الذي يستخدمها. كل استخدام يبدو متطابقاً في سجلات التدقيق.

مخزّنة في مخازن أسرار منصات CI

تخزن منصات CI مثل GitHub Actions و GitLab CI و Jenkins الأسرار في خزائنها الخاصة. هذه أهداف ثمينة. اختراق واحد لمخزن أسرار منصة CI يكشف كل بيانات اعتماد لكل مشروع. اختراق CircleCI في يناير 2023 هو مثال نموذجي: اخترق المهاجمون الأنظمة الداخلية لـ CircleCI واستخرجوا أسرار العملاء، مما أجبر كل عميل CircleCI على تدوير كل سر مخزن في المنصة.

اختراقات واقعية

يتكرر النمط عبر الصناعة:

  • Codecov (2021): عدّل المهاجمون Codecov Bash Uploader لاستخراج متغيرات البيئة — بما في ذلك أسرار CI/CD — من أنابيب آلاف العملاء. تم إرسال بيانات الاعتماد طويلة العمر المخزنة كمتغيرات بيئة إلى خوادم يتحكم فيها المهاجمون.
  • CircleCI (2023): أدى اختراق حاسوب محمول لموظف إلى استخراج أسرار العملاء من مخزن أسرار CircleCI. نُصح كل عميل بتدوير جميع الأسرار فوراً.
  • Travis CI (2021): كشفت ثغرة أمنية عن أسرار من بناءات المستودعات العامة، بما في ذلك مفاتيح AWS ورموز GitHub وبيانات اعتماد Docker Hub.
  • Uber (2022): حصل مهاجم على وصول إلى الأنظمة الداخلية من خلال أنبوب CI/CD مخترق، مستفيداً من بيانات اعتماد مشفرة في نصوص PowerShell.

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

كيف يعمل Workload Identity Federation

يستبدل Workload Identity Federation بيانات الاعتماد الثابتة بتدفق مصادقة ديناميكي قائم على الرموز المميزة مبني على OpenID Connect (OIDC). إليك كيف يعمل على مستوى البروتوكول.

تدفق OIDC

يتضمن تدفق المصادقة ثلاثة أطراف: منصة CI (مزود الهوية)، ومزود السحابة (الطرف المعتمد)، وتشغيل الأنبوب (حمل العمل).

  1. إصدار الرمز: عندما تبدأ مهمة أنبوب، تولّد منصة CI رمز JWT (JSON Web Token) موقّعاً لهذا التشغيل المحدد. يحتوي هذا الرمز على مطالبات حول حمل العمل: أي مستودع أطلقه، وأي فرع، وأي سير عمل، وأي فاعل، والبيئة.
  2. تبادل الرمز: يقدم الأنبوب هذا الـ JWT إلى خدمة رمز الأمان (STS) لمزود السحابة ويطلب بيانات اعتماد مؤقتة.
  3. التحقق: يجلب مزود السحابة وثيقة اكتشاف OIDC العامة ومفاتيح التوقيع لمنصة CI. يتحقق من توقيع JWT، ويتحقق من عدم انتهاء صلاحية الرمز، ويتحقق من تطابق المطالبات مع سياسة الثقة المكوّنة.
  4. إصدار بيانات الاعتماد: إذا نجح التحقق، يصدر مزود السحابة بيانات اعتماد قصيرة العمر (عادةً صالحة لمدة ساعة واحدة أو أقل) محددة بدور IAM أو حساب الخدمة المكوّن في سياسة الثقة.

علاقة الثقة

أساس Workload Identity Federation هو علاقة ثقة بين نظام IAM لمزود السحابة ومزود OIDC لمنصة CI. يتم تكوين هذا مرة واحدة:

  • AWS: مورد IAM OIDC Identity Provider يثق بـ token.actions.githubusercontent.com (لـ GitHub Actions) أو gitlab.com (لـ GitLab).
  • GCP: Workload Identity Pool و Provider مكوّنان للثقة بمُصدر OIDC لمنصة CI.
  • Azure: Federated Identity Credential على App Registration أو Managed Identity.

التحديد القائم على المطالبات

تأتي القوة الأمنية الحقيقية لـ OIDC federation من التحديد القائم على المطالبات. يحتوي JWT الصادر من منصة CI على مطالبات سياقية غنية. تكوّن مزود السحابة لقبول الرموز التي تتطابق مطالباتها مع شروط محددة فقط:

  • المستودع: قبول الرموز من my-org/my-repo فقط
  • الفرع: قبول الرموز من فرع main فقط
  • البيئة: قبول الرموز من بيئة production فقط
  • سير العمل: قبول الرموز من ملف سير عمل محدد فقط

هذا يعني أنه حتى لو اخترق مهاجم مستودعاً آخر في نفس مؤسسة GitHub، فلن يتمكن من الحصول على بيانات اعتماد محددة بدور نشر الإنتاج — لن تتطابق المطالبات.

تصوّر التدفق

┌─────────────┐     1. Request JWT      ┌──────────────────┐
│  CI Runner   │ ──────────────────────▶ │  CI OIDC Provider │
│  (Pipeline)  │ ◀────────────────────── │  (GitHub/GitLab)  │
└─────┬───────┘     2. Signed JWT        └──────────────────┘
      │                                          │
      │  3. Present JWT +                        │
      │     Request credentials                  │
      ▼                                          │
┌─────────────┐     4. Validate JWT      ┌──────┴───────────┐
│  Cloud IAM  │ ──────────────────────▶  │  OIDC Discovery   │
│  (STS)      │     (fetch public keys)  │  + JWKS Endpoint  │
└─────┬───────┘                          └──────────────────┘
      │
      │  5. Issue short-lived
      │     credentials (1hr)
      ▼
┌─────────────┐
│  Cloud APIs │
│  (S3, GCS,  │
│   etc.)     │
└─────────────┘

GitHub Actions OIDC Federation

يتمتع GitHub Actions بدعم OIDC أصلي. يمكن لكل تشغيل سير عمل طلب JWT موقّع من مزود OIDC الخاص بـ GitHub على token.actions.githubusercontent.com. إليك كيفية إعداد الربط مع كل مزود سحابة رئيسي.

AWS: IAM OIDC Provider + IAM Role

أولاً، أنشئ مزود OIDC ودور IAM في AWS. إليك تكوين Terraform:

# Terraform: AWS OIDC Provider for GitHub Actions
resource "aws_iam_openid_connect_provider" "github_actions" {
  url             = "https://token.actions.githubusercontent.com"
  client_id_list  = ["sts.amazonaws.com"]
  thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}

# IAM Role that GitHub Actions can assume
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_actions.arn
        }
        Action = "sts:AssumeRoleWithWebIdentity"
        Condition = {
          StringEquals = {
            "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
          }
          StringLike = {
            "token.actions.githubusercontent.com:sub" = "repo:my-org/my-repo:ref:refs/heads/main"
          }
        }
      }
    ]
  })
}

# Attach permissions to the role
resource "aws_iam_role_policy_attachment" "deploy_policy" {
  role       = aws_iam_role.github_actions_deploy.name
  policy_arn = "arn:aws:iam::policy/my-deploy-policy"
}

ثم استخدم الدور في سير عمل GitHub Actions الخاص بك:

# .github/workflows/deploy.yml
name: Deploy to AWS
on:
  push:
    branches: [main]

permissions:
  id-token: write   # Required for OIDC
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
          aws-region: us-east-1
          role-session-name: github-actions-deploy-${{ github.run_id }}

      - name: Deploy
        run: |
          aws s3 sync ./dist s3://my-bucket/
          aws cloudfront create-invalidation --distribution-id E12345 --paths "/*"

GCP: Workload Identity Pool + Provider

يستخدم GCP خاصية Workload Identity Federation مع pools و providers. إليك الإعداد باستخدام gcloud CLI:

# Create the Workload Identity Pool
gcloud iam workload-identity-pools create "github-actions-pool" \
  --project="my-project" \
  --location="global" \
  --display-name="GitHub Actions Pool"

# Create the OIDC Provider
gcloud iam workload-identity-pools providers create-oidc "github-provider" \
  --project="my-project" \
  --location="global" \
  --workload-identity-pool="github-actions-pool" \
  --display-name="GitHub OIDC" \
  --attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.ref=assertion.ref" \
  --issuer-uri="https://token.actions.githubusercontent.com" \
  --attribute-condition="assertion.repository_owner=='my-org'"

# Grant the pool access to a service account
gcloud iam service-accounts add-iam-policy-binding \
  deploy-sa@my-project.iam.gserviceaccount.com \
  --project="my-project" \
  --role="roles/iam.workloadIdentityUser" \
  --member="principalSet://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/github-actions-pool/attribute.repository/my-org/my-repo"

سير عمل GitHub Actions لـ GCP:

# .github/workflows/deploy-gcp.yml
name: Deploy to GCP
on:
  push:
    branches: [main]

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Authenticate to GCP
        uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: "projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/github-actions-pool/providers/github-provider"
          service_account: "deploy-sa@my-project.iam.gserviceaccount.com"

      - name: Set up Cloud SDK
        uses: google-github-actions/setup-gcloud@v2

      - name: Deploy to Cloud Run
        run: |
          gcloud run deploy my-service \
            --image gcr.io/my-project/my-app:${{ github.sha }} \
            --region us-central1

Azure: Federated Credentials على App Registration

يدعم Azure بيانات اعتماد الهوية الفيدرالية على App Registrations و Managed Identities. أنشئ الربط باستخدام Azure CLI:

# Create an App Registration
az ad app create --display-name "github-actions-deploy"

# Create a federated credential
az ad app federated-credential create \
  --id <APP_OBJECT_ID> \
  --parameters '{
    "name": "github-actions-main",
    "issuer": "https://token.actions.githubusercontent.com",
    "subject": "repo:my-org/my-repo:ref:refs/heads/main",
    "audiences": ["api://AzureADTokenExchange"],
    "description": "GitHub Actions deploy from main branch"
  }'

# Create a service principal and assign roles
az ad sp create --id <APP_CLIENT_ID>
az role assignment create \
  --assignee <APP_CLIENT_ID> \
  --role "Contributor" \
  --scope /subscriptions/<SUBSCRIPTION_ID>/resourceGroups/my-rg

سير عمل GitHub Actions لـ Azure:

# .github/workflows/deploy-azure.yml
name: Deploy to Azure
on:
  push:
    branches: [main]

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Azure Login
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Deploy to Azure Web App
        uses: azure/webapps-deploy@v3
        with:
          app-name: my-web-app
          package: ./dist

ملاحظة: مع Azure OIDC، لا تزال تخزن client ID و tenant ID و subscription ID كأسرار — لكنها معرّفات غير حساسة، وليست بيانات اعتماد للمصادقة. لا حاجة لـ client secret.

شروط المطالبات: تقييد الوصول

يتبع مطالبة subject في رموز OIDC لـ GitHub Actions تنسيقاً يمكن التنبؤ به. استخدم هذه الأنماط في سياسات الثقة الخاصة بك:

  • repo:my-org/my-repo:ref:refs/heads/main — فرع main فقط
  • repo:my-org/my-repo:environment:production — بيئة production فقط
  • repo:my-org/my-repo:pull_request — سير عمل pull request فقط
  • repo:my-org/my-repo:ref:refs/tags/v* — وسوم الإصدارات فقط (استخدم StringLike في AWS)

استخدم دائماً شرط المطالبة الأكثر تقييداً. تجنب أحرف البدل مثل repo:my-org/* ما لم تكن بحاجة فعلية للوصول على مستوى المؤسسة بأكملها.

GitLab CI OIDC Federation

قدّم GitLab CI دعم OIDC أصلياً مع كلمة id_tokens المفتاحية. التدفق مشابه لـ GitHub Actions ولكن مع بعض الاختلافات في بنية المطالبات والتكوين.

طلب رموز OIDC في GitLab CI

في GitLab CI، تعلن عن رموز OIDC في تكوين المهمة باستخدام كلمة id_tokens المفتاحية:

# .gitlab-ci.yml
deploy_to_aws:
  stage: deploy
  image: amazon/aws-cli:latest
  id_tokens:
    AWS_OIDC_TOKEN:
      aud: https://sts.amazonaws.com
  script:
    - |
      CREDENTIALS=$(aws sts assume-role-with-web-identity \
        --role-arn arn:aws:iam::123456789012:role/gitlab-deploy \
        --role-session-name "gitlab-ci-${CI_PIPELINE_ID}" \
        --web-identity-token "${AWS_OIDC_TOKEN}" \
        --duration-seconds 3600 \
        --query 'Credentials')
      export AWS_ACCESS_KEY_ID=$(echo $CREDENTIALS | jq -r '.AccessKeyId')
      export AWS_SECRET_ACCESS_KEY=$(echo $CREDENTIALS | jq -r '.SecretAccessKey')
      export AWS_SESSION_TOKEN=$(echo $CREDENTIALS | jq -r '.SessionToken')
      aws s3 sync ./dist s3://my-bucket/
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

تكوين AWS لـ GitLab

يستخدم مزود IAM OIDC لـ GitLab عنوان URL مُصدر مختلفاً:

# Terraform: AWS OIDC Provider for GitLab
resource "aws_iam_openid_connect_provider" "gitlab" {
  url             = "https://gitlab.com"
  client_id_list  = ["https://sts.amazonaws.com"]
  thumbprint_list = ["b3dd7606d2b5a8b4a13771dbecc9ee1cecafa38a"]
}

resource "aws_iam_role" "gitlab_deploy" {
  name = "gitlab-deploy"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Federated = aws_iam_openid_connect_provider.gitlab.arn
        }
        Action = "sts:AssumeRoleWithWebIdentity"
        Condition = {
          StringEquals = {
            "gitlab.com:aud" = "https://sts.amazonaws.com"
          }
          StringLike = {
            "gitlab.com:sub" = "project_path:my-group/my-project:ref_type:branch:ref:main"
          }
        }
      }
    ]
  })
}

GCP Workload Identity Federation لـ GitLab

# Create the OIDC Provider for GitLab
gcloud iam workload-identity-pools providers create-oidc "gitlab-provider" \
  --project="my-project" \
  --location="global" \
  --workload-identity-pool="cicd-pool" \
  --display-name="GitLab OIDC" \
  --attribute-mapping="google.subject=assertion.sub,attribute.project_path=assertion.project_path,attribute.ref=assertion.ref" \
  --issuer-uri="https://gitlab.com" \
  --attribute-condition="assertion.namespace_path=='my-group'"

مهمة GitLab CI المقابلة لـ GCP:

# .gitlab-ci.yml
deploy_to_gcp:
  stage: deploy
  image: google/cloud-sdk:slim
  id_tokens:
    GCP_OIDC_TOKEN:
      aud: https://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/cicd-pool/providers/gitlab-provider
  script:
    - |
      echo "${GCP_OIDC_TOKEN}" > /tmp/oidc_token.txt
      gcloud iam workload-identity-pools create-cred-config \
        "projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/cicd-pool/providers/gitlab-provider" \
        --service-account="deploy-sa@my-project.iam.gserviceaccount.com" \
        --output-file=/tmp/cred_config.json \
        --credential-source-file=/tmp/oidc_token.txt
      gcloud auth login --cred-file=/tmp/cred_config.json
      gcloud run deploy my-service --image gcr.io/my-project/my-app:${CI_COMMIT_SHA} --region us-central1
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

اختلافات تصفية المطالبات مقارنة بـ GitHub Actions

تستخدم رموز OIDC لـ GitLab تنسيق مطالبة subject مختلفاً عن GitHub Actions:

  • GitLab: project_path:my-group/my-project:ref_type:branch:ref:main
  • GitHub: repo:my-org/my-repo:ref:refs/heads/main

يتضمن GitLab أيضاً مطالبات إضافية يمكن استخدامها للتصفية:

  • namespace_id و namespace_path — مساحة اسم المجموعة أو المستخدم
  • project_id و project_path — المشروع المحدد
  • pipeline_source — كيف تم تشغيل الأنبوب (push، merge_request، schedule، إلخ.)
  • environment — بيئة النشر، إذا تم تعيينها
  • ref_protected — ما إذا كان المرجع فرعاً محمياً

مطالبة ref_protected مفيدة بشكل خاص: يمكنك تكوين سياسات الثقة لقبول الرموز من الفروع المحمية فقط، مما يضيف طبقة أمان إضافية.

أنماط متقدمة

تسلسل OIDC: من CI إلى Vault إلى السحابة

يمكن لـ HashiCorp Vault أن يعمل كوسيط هوية بين منصة CI ومزودي السحابة. هذا مفيد عندما تحتاج إلى إدارة مركزية للأسرار، أو بيانات اعتماد ديناميكية لقواعد البيانات أو خدمات أخرى غير سحابية، أو سجل تدقيق موحّد عبر جميع منصات CI.

# Configure Vault JWT auth backend for GitHub Actions
vault auth enable jwt

vault write auth/jwt/config \
  oidc_discovery_url="https://token.actions.githubusercontent.com" \
  bound_issuer="https://token.actions.githubusercontent.com"

# Create a role that maps GitHub Actions claims to Vault policies
vault write auth/jwt/role/deploy \
  role_type="jwt" \
  bound_audiences="https://vault.mycompany.com" \
  bound_claims_type="glob" \
  bound_claims='{"repository":"my-org/my-repo","ref":"refs/heads/main"}' \
  user_claim="repository" \
  policies="deploy-policy" \
  ttl="15m"

سير عمل GitHub Actions باستخدام Vault كوسيط:

# .github/workflows/deploy-via-vault.yml
name: Deploy via Vault
on:
  push:
    branches: [main]

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Import Secrets from Vault
        uses: hashicorp/vault-action@v3
        with:
          url: https://vault.mycompany.com
          method: jwt
          role: deploy
          jwtGithubAudience: https://vault.mycompany.com
          secrets: |
            aws/creds/deploy access_key | AWS_ACCESS_KEY_ID ;
            aws/creds/deploy secret_key | AWS_SECRET_ACCESS_KEY ;
            aws/creds/deploy security_token | AWS_SESSION_TOKEN

      - name: Deploy
        run: aws s3 sync ./dist s3://my-bucket/

هويات لكل بيئة

أنشئ أدوار IAM منفصلة لكل بيئة، كل منها بسياسات ثقة أكثر صرامة تدريجياً:

# Terraform: Per-environment roles
locals {
  environments = {
    dev = {
      branch    = "*"
      condition = "StringLike"
    }
    staging = {
      branch    = "refs/heads/main"
      condition = "StringEquals"
    }
    production = {
      branch    = "refs/heads/main"
      condition = "StringEquals"
    }
  }
}

resource "aws_iam_role" "deploy" {
  for_each = local.environments
  name     = "github-actions-deploy-${each.key}"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Federated = aws_iam_openid_connect_provider.github_actions.arn
        }
        Action = "sts:AssumeRoleWithWebIdentity"
        Condition = {
          StringEquals = {
            "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
          }
          (each.value.condition) = {
            "token.actions.githubusercontent.com:sub" = "repo:my-org/my-repo:environment:${each.key}"
          }
        }
      }
    ]
  })
}

في GitHub Actions، استخدم البيئات لتحديد نطاق رمز OIDC تلقائياً:

jobs:
  deploy-prod:
    runs-on: ubuntu-latest
    environment: production  # This changes the OIDC subject claim
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy-production
          aws-region: us-east-1

Kubernetes Workload Identity لخطوات النشر

إذا كان أنبوب CI الخاص بك ينشر إلى Kubernetes، يمكنك تسلسل الهويات: يصادق مُشغّل CI على الكتلة باستخدام OIDC، وتستخدم pods في الكتلة Kubernetes Workload Identity للوصول إلى موارد السحابة:

# GKE Workload Identity: Annotate the Kubernetes service account
apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-deploy-sa
  namespace: production
  annotations:
    iam.gke.io/gcp-service-account: app-sa@my-project.iam.gserviceaccount.com

أنماط الوصول عبر الحسابات

للمؤسسات التي لديها حسابات AWS متعددة، يمكنك تسلسل افتراضات الأدوار. يفترض مُشغّل CI دوراً في حساب “CI” مركزي عبر OIDC، ثم يفترض أدواراً في الحسابات المستهدفة:

# Step 1: Assume the hub role via OIDC
- uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::HUB_ACCOUNT:role/github-actions-hub
    aws-region: us-east-1

# Step 2: Assume a role in the target account
- uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::TARGET_ACCOUNT:role/deploy-role
    aws-region: us-east-1
    role-chaining: true

دمج OIDC مع Terraform

تستفيد عمليات نشر Terraform بشكل كبير من OIDC لأنها تتطلب عادةً صلاحيات بنية تحتية واسعة. كوّن مزود AWS لاستخدام الدور المفترض عبر OIDC:

# terraform/providers.tf
provider "aws" {
  region = "us-east-1"

  # No credentials configured here — they come from
  # the environment variables set by the OIDC step
  default_tags {
    tags = {
      ManagedBy   = "terraform"
      Repository  = "my-org/my-infra"
      Environment = var.environment
    }
  }
}

# terraform/backend.tf
terraform {
  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "infra/terraform.tfstate"
    region         = "us-east-1"
    # State backend also uses the OIDC credentials
    # No access_key or secret_key needed
  }
}

اعتبارات أمنية

يُعد Workload Identity Federation تحسيناً هائلاً مقارنة ببيانات الاعتماد طويلة العمر، لكنه ليس مضموناً بالكامل. إليك المخاطر التي يجب الانتباه إليها والتخفيف منها.

سياسات الثقة الواسعة بشكل مفرط

الخطأ الأكثر شيوعاً هو تكوين سياسات ثقة متساهلة جداً. أمثلة على التكوينات الخطرة:

  • الثقة بمؤسسة بأكملها: repo:my-org/* يعني أن أي مستودع في المؤسسة يمكنه افتراض الدور. مستودع مخترق أو خبيث يحصل على وصول الإنتاج.
  • الثقة بجميع الفروع: repo:my-org/my-repo:* يعني أن فرعاً بكود غير مختبر يمكنه الوصول إلى بيانات اعتماد الإنتاج.
  • عدم وجود تقييد للجمهور: بدون التحقق من مطالبة aud، يمكن إعادة تشغيل الرموز المخصصة لخدمة واحدة ضد خدمة أخرى.

اتبع دائماً مبدأ أقل الامتيازات: قيّد الوصول إلى المستودع والفرع والبيئة المحددة المطلوبة.

خطأ تكوين جمهور الرمز

تحدد مطالبة الجمهور (aud) المستلم المقصود للرمز. إذا لم تتحقق سياسة الثقة الخاصة بك من الجمهور، يمكن للمهاجم الذي يحصل على رمز مخصص لخدمة مختلفة استخدامه لافتراض دور IAM الخاص بك. تحقق دائماً من مطالبة الجمهور في سياسات الثقة الخاصة بك.

اختراق مزود OIDC

إذا تم اختراق مزود OIDC لمنصة CI (مثلاً، يمكن للمهاجم تزوير JWTs)، فإن جميع علاقات الثقة المبنية على هذا المزود تُخترق. تشمل التخفيفات:

  • مراقبة إرشادات الأمان لمنصة CI
  • استخدام قيود مطالبات إضافية تتجاوز مجرد الموضوع
  • تنفيذ قيود قائمة على IP حيثما كان ذلك مدعوماً
  • تعيين فترات جلسات قصيرة (15 دقيقة بدلاً من ساعة)
  • تمكين CloudTrail أو GCP Audit Logs أو Azure Activity Logs للكشف عن الوصول الفيدرالي غير الطبيعي

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

يجب مراقبة الوصول الفيدرالي مثل أي مصادقة أخرى. أعدّ تنبيهات لـ:

  • افتراضات الأدوار من مستودعات أو فروع غير متوقعة
  • أنماط وصول غير عادية (مثلاً، افتراض دور الإنتاج في الساعة 3 صباحاً عندما لا يكون هناك نشر مجدول)
  • محاولات افتراض أدوار فاشلة (قد تشير إلى مهاجم يستكشف سياسات الثقة)
  • تغييرات في تكوينات مزود OIDC أو سياسات الثقة
# AWS CloudTrail filter for OIDC role assumptions
{
  "eventName": "AssumeRoleWithWebIdentity",
  "requestParameters": {
    "roleArn": "arn:aws:iam::123456789012:role/github-actions-deploy-production"
  }
}

استراتيجيات الطوارئ

يعتمد OIDC federation على خدمات خارجية (نقطة نهاية OIDC لمنصة CI، و STS السحابي). خطط لحالات الانقطاع:

  • احتفظ بمجموعة واحدة من بيانات الاعتماد طويلة العمر للطوارئ (“break-glass”) في خزنة آمنة (مثل AWS Secrets Manager أو 1Password)، متاحة فقط لكبار المهندسين
  • وثّق إجراء الطوارئ: متى يُستخدم، ومن يمكنه التصريح به، وكيفية تدقيق استخدامه
  • أعدّ تنبيهات عند استخدام بيانات اعتماد الطوارئ
  • قم بتدوير بيانات اعتماد الطوارئ بعد كل استخدام

دليل الترحيل: من بيانات الاعتماد طويلة العمر إلى قصيرة العمر

الترحيل من بيانات الاعتماد طويلة العمر إلى Workload Identity Federation هو تغيير عالي القيمة ومنخفض المخاطر عند تنفيذه بشكل منهجي. إليك نهجاً مرحلياً.

المرحلة 1: الجرد

ابدأ بفهرسة كل سر في منصة CI/CD الخاصة بك:

# GitHub: List all secrets for a repository
gh secret list --repo my-org/my-repo

# GitHub: List organization secrets
gh secret list --org my-org

# GitLab: List project variables
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
  "https://gitlab.com/api/v4/projects/$PROJECT_ID/variables"

لكل سر، وثّق:

  • ما هو (مفتاح AWS، مفتاح GCP، كلمة مرور قاعدة بيانات، رمز API)
  • ما الذي يصل إليه (أي حساب سحابي، أي خدمة)
  • أي الأنابيب تستخدمه
  • متى تم تدويره آخر مرة
  • ما إذا كان يمكن استبداله بـ OIDC federation

المرحلة 2: تحديد المرشحين

مرشحون جيدون لاستبدال OIDC:

  • مفاتيح وصول AWS IAM المستخدمة في CI/CD
  • مفاتيح JSON لحسابات خدمة GCP
  • أسرار client لـ Azure service principal
  • أي بيانات اعتماد تُستخدم للمصادقة على مزود سحابة يدعم OIDC

ليسوا مرشحين لـ OIDC (تتطلب نهجاً مختلفاً):

  • كلمات مرور قواعد البيانات (استخدم بيانات اعتماد Vault الديناميكية بدلاً من ذلك)
  • مفاتيح API للجهات الخارجية (استخدم Vault أو مدير أسرار)
  • مفاتيح SSH لعمليات Git (استخدم deploy keys أو رموز GitHub App)
  • كلمات مرور سجل الحاويات (استخدم مصادقة السجل الأصلية للسحابة عبر OIDC)

المرحلة 3: الطرح المرحلي

  1. ابدأ بغير الإنتاج: أعدّ OIDC federation لأنبوب واحد غير إنتاجي. تحقق من أنه يعمل بشكل موثوق خلال أسبوع.
  2. التوسع لمزيد من أنابيب غير الإنتاج: حوّل أنابيب التطوير والاختبار المتبقية. ابنِ الثقة والتوثيق.
  3. تجربة الإنتاج: اختر أنبوب إنتاج واحداً بمراقبة جيدة. شغّل OIDC بجانب بيانات الاعتماد الحالية لمدة أسبوع (يستخدم الأنبوب OIDC، لكن بيانات الاعتماد القديمة لا تزال موجودة كاحتياطي).
  4. الطرح الكامل للإنتاج: حوّل أنابيب الإنتاج المتبقية. احتفظ ببيانات الاعتماد القديمة للطوارئ فقط.

المرحلة 4: إيقاف بيانات الاعتماد القديمة

بعد أن يعمل OIDC بشكل موثوق:

  1. عطّل، لا تحذف: عطّل بيانات الاعتماد القديمة أولاً. انتظر أسبوعين للتأكد من عدم حدوث أعطال.
  2. راقب الاستخدام: تحقق من CloudTrail أو GCP Audit Logs أو ما يعادلها بحثاً عن أي استخدام لبيانات الاعتماد القديمة.
  3. احذف: بمجرد التأكد من عدم استخدام بيانات الاعتماد، احذفها.
  4. أزل من أسرار CI: احذف السر من مخزن أسرار منصة CI الخاصة بك.
# AWS: Deactivate an old access key
aws iam update-access-key \
  --user-name ci-deploy \
  --access-key-id AKIAIOSFODNN7EXAMPLE \
  --status Inactive

# After validation period, delete it
aws iam delete-access-key \
  --user-name ci-deploy \
  --access-key-id AKIAIOSFODNN7EXAMPLE

التحقق

تأكد من اكتمال الترحيل:

  • جميع أنابيب CI/CD تصادق عبر OIDC (تحقق من سجلات سير العمل)
  • لا توجد بيانات اعتماد سحابية طويلة العمر في مخازن أسرار CI (باستثناء بيانات اعتماد الطوارئ الموثقة)
  • مستخدمو IAM أو حسابات الخدمة التي كانت تستخدمها CI محذوفة أو ليس لديها مفاتيح وصول
  • المراقبة والتنبيه مفعّلة للوصول الفيدرالي
  • إجراءات الطوارئ موثقة ومختبرة

الخلاصة

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

مسار الترحيل واضح ومباشر. ابدأ بأنبوب واحد ومزود سحابة واحد. أعدّ علاقة ثقة OIDC، وحدّث سير العمل لاستخدام المصادقة الفيدرالية، وتحقق من أنه يعمل، وانتقل إلى الأنبوب التالي. في غضون أسابيع قليلة، يمكنك التخلص من كل بيانات اعتماد السحابة طويلة العمر من منصة CI/CD الخاصة بك.

الأدوات ناضجة ومدعومة جيداً. يتمتع GitHub Actions و GitLab CI وجميع مزودي السحابة الرئيسيين الثلاثة بدعم OIDC federation جاهز للإنتاج. تتعامل إجراءات GitHub Actions الرسمية (aws-actions/configure-aws-credentials و google-github-actions/auth و azure/login) مع تبادل الرموز تلقائياً. موارد Terraform موجودة لإعداد البنية التحتية ككود.

لا يوجد سبب للاحتفاظ ببيانات الاعتماد طويلة العمر في أنابيب CI/CD الخاصة بك. المخاطر عالية، وتكلفة الترحيل منخفضة، والتحسين الأمني فوري. ابدأ اليوم.