GitHub Actions OIDC إلى AWS: Keyless

٢٧ مايو ٢٠٢٦

GitHub Actions OIDC to AWS: Keyless Terraform (2026)

ملخص

يمكن لـ GitHub Actions إصدار رمز JSON Web Token (JWT) قصير العمر عند كل تشغيل لبيئة العمل وتبديله مباشرة مع AWS STS للحصول على بيانات اعتماد مؤقتة — دون الحاجة أبدًا إلى وضع AWS_ACCESS_KEY_ID في أسرار المستودع (repository secrets). يشرح هذا البرنامج التعليمي كيفية إعداد نشر Terraform "بدون مفاتيح" (keyless) من البداية إلى النهاية: مزود IAM OpenID Connect، وسياسة ثقة للدور محددة النطاق، وخطوة aws-actions/configure-aws-credentials@v6، مع إجراءات تقوية (hardening) تحصر عمليات النشر في فرع وبيئة عمل واحدة فقط. يستغرق الأمر حوالي 25 دقيقة.

موجز الإجابة: لنشر Terraform على AWS من GitHub Actions بدون أسرار طويلة الأمد، قم بتسجيل مزود IAM OIDC لـ token.actions.githubusercontent.com، وأنشئ دورًا (role) تربط سياسة الثقة الخاصة به مطالبة الـ sub بمستودعك وفرعك، وامنح بيئة العمل صلاحية id-token: write، واستخدم aws-actions/configure-aws-credentials@v6 لتبديل رمز GitHub JWT ببيانات اعتماد STS قصيرة العمر.

ما ستتعلمه

  • مزود هوية IAM OpenID Connect يثق في مصدر رموز GitHub
  • دور IAM بأقل الامتيازات (least-privilege) لا يمكن انتحاله إلا من خلال مستودعك على فرع main أو بيئة production
  • بيئة عمل GitHub Actions تقوم بتشغيل terraform plan و terraform apply مقابل AWS بدون أي أسرار طويلة الأمد
  • سياسة ثقة محصنة تصمد أمام إطلاق مطالبة الموضوع غير القابلة للتغيير (immutable subject claim) للمستودعات الجديدة في 18 يونيو 20261

المتطلبات الأساسية

  • حساب AWS تتوفر فيه صلاحيات iam:CreateOpenIDConnectProvider، و iam:CreateRole، و iam:AttachRolePolicy (مسؤول أو دور تهيئة بنطاق كافٍ)
  • مستودع GitHub (أي خطة — OIDC يعمل على Free و Pro و Team و Enterprise)
  • إصدار Terraform 1.15.2 أو أحدث (قفل الحالة الأصلي عبر S3 باستخدام use_lockfile = true يتطلب 1.11.0+)2
  • واجهة سطر أوامر AWS (AWS CLI) v2.34+
  • حاوية S3 لحالة Terraform عن بُعد (يوضح البرنامج التعليمي لقفل حالة Terraform S3 الأصلية كيفية توفير واحدة مع تمكين القفل وبدون DynamoDB)

الخطوة 1: إنشاء مزود AWS IAM OpenID Connect

هذه عملية تهيئة تتم مرة واحدة لكل حساب. يخبر مزود OIDC خدمة AWS بالوثوق في رموز JWT الموقعة من قِبل token.actions.githubusercontent.com. يمكنك إنشاؤه مرة واحدة باستخدام واجهة سطر الأوامر (CLI) ثم إدارة كل شيء آخر في Terraform.

aws iam create-open-id-connect-provider \
  --url https://token.actions.githubusercontent.com \
  --client-id-list sts.amazonaws.com

هذا هو الأمر بالكامل. هناك ثلاثة أشياء تستحق الملاحظة:

خيار --thumbprint-list اختياري. تنص مرجعيات AWS CLI صراحةً على أنه عند حذفه، يقوم IAM باسترداد بصمة CA الوسيطة العليا بنفسه.3 والأهم من ذلك، منذ يوليو 2023، تتحقق AWS من شهادة TLS لنقطة نهاية JWKS مقابل مكتبتها من مراجع التصديق الجذرية الموثوقة و تتجاهل أي بصمة تمررها لمزود GitHub.4 البرامج التعليمية التي لا تزال تثبت البصمة 6938fd4d98bab03faadb97b34396831e3780aea1 تكرر طقوسًا ما قبل عام 2023 لا تفعل شيئًا اليوم.

sts.amazonaws.com هو الجمهور (مطالبة aud) الذي سيحمله رمز JWT الصادر عن GitHub افتراضيًا. إذا كنت تستهدف قسم AWS China، فستستخدم sts.amazonaws.com.cn هنا وتمرر مدخل audience مطابقًا للأكشن.5

يمكنك تسجيل نفس عنوان URL لـ OIDC مرة واحدة فقط لكل حساب AWS، لذا إذا ظهر خطأ EntityAlreadyExists، فهذا يعني أن المزود موجود بالفعل — انتقل إلى الخطوة 2.

إذا كنت تفضل التصريح عن المزود في Terraform بجانب الدور، فإن المورد يتكون من سطرين:

resource "aws_iam_openid_connect_provider" "GitHub" {
  url            = "https://token.actions.githubusercontent.com"
  client_id_list = ["sts.amazonaws.com"]
}

في إصدار AWS provider 6.x أصبح thumbprint_list اختياريًا. احذفه تمامًا — عندما لا يتم تمرير السمة، يقوم IAM تلقائيًا باسترداد بصمة CA الوسيطة العليا وقت الإنشاء، ثم تتجاهلها AWS في وقت التشغيل لمزود GitHub. لاحظ وجود خطأ برمجي مفتوح (#40509): بمجرد تعيين البصمات في الحالة (state)، فإن إزالتها من التكوين لا يمسحها من IAM، لذا لا تكتب thumbprint_list = [] أيضًا (قد ترفض AWS API القائمة الفارغة الصريحة برسالة Thumbprint list must contain at least one entry).6 إذا ورثت مجموعة موارد (stack) تحتوي بالفعل على بصمات في الحالة، فاتركها كما هي — فهي لا تسبب أي ضرر.

الخطوة 2: تحديد سياسة الثقة لدور IAM

هذا هو الحد الأمني. تحدد سياسة الثقة من يمكنه انتحال الدور؛ بدونها (أو مع شرط فضفاض)، يمكن لأي مستودع على GitHub إصدار JWT وانتحال دورك. سنقوم بربطه بمستودع واحد وفرع واحد.

أنشئ ملف iam.tf:

data "aws_caller_identity" "current" {}

locals {
  github_org  = "your-org"      # قم بتغييري
  github_repo = "your-repo"     # قم بتغييري
  branch      = "main"
  account_id  = data.aws_caller_identity.current.account_id
}

data "aws_iam_policy_document" "github_trust" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRoleWithWebIdentity"]

    principals {
      type        = "Federated"
      identifiers = [
        "arn:aws:iam::${local.account_id}:oidc-provider/token.actions.githubusercontent.com"
      ]
    }

    condition {
      test     = "StringEquals"
      variable = "token.actions.githubusercontent.com:aud"
      values   = ["sts.amazonaws.com"]
    }

    condition {
      test     = "StringEquals"
      variable = "token.actions.githubusercontent.com:sub"
      values   = [
        "repo:${local.github_org}/${local.github_repo}:ref:refs/heads/${local.branch}"
      ]
    }
  }
}

resource "aws_iam_role" "gha_terraform" {
  name               = "gha-terraform-deploy"
  assume_role_policy = data.aws_iam_policy_document.github_trust.json
  description        = "ينتحله GitHub Actions عبر OIDC لنشر Terraform"
  max_session_duration = 3600
}

ثلاثة تفاصيل تكتشفها الفرق لاحقًا:

استخدام StringEquals في مطالبة sub يقوم بمطابقة تامة. إذا كنت تريد أن يتمكن أي فرع (أو أي وسم، أو أي طلب سحب) من النشر، فقم بالتبديل إلى StringLike واستخدم نمط حرف بدل (wildcard) مثل repo:org/repo:ref:refs/heads/*. لعمليات النشر في بيئة الإنتاج، ستحتاج دائمًا تقريبًا إلى StringEquals لفرع واحد.7

تسمية الدور حرفيًا "GitHubActions" تسببت في تقارير قديمة عن فشل بطرق غير متوقعة (المشكلة رقم 953 في مستودع الأكشن).8 استخدم اسمًا وصفيًا مثل gha-terraform-deploy.

القيمة الافتراضية لـ max_session_duration هي 3600 ثانية (ساعة واحدة). إذا كان تنفيذ Terraform plan + apply يستغرق وقتًا أطول من ذلك حقًا، فقم بزيادتها حتى 43200 (اثنتي عشرة ساعة)، ولكن ستحتاج أيضًا إلى تمرير role-duration-seconds للأكشن لطلب جلسة أطول.5

الخطوة 3: إرفاق صلاحيات أقل الامتيازات لحالة Terraform والموارد

يحتاج الدور إلى مجموعتين من الصلاحيات: الوصول إلى الواجهة الخلفية للحالة (state-backend)، وأي شيء يديره Terraform بالفعل. للوصول إلى الواجهة الخلفية فقط، مقابل حاوية حالة S3 تسمى acme-tf-state مع تمكين قفل S3 الأصلي:

data "aws_iam_policy_document" "tf_state" {
  statement {
    sid     = "StateBucketList"
    effect  = "Allow"
    actions = ["s3:ListBucket"]
    resources = ["arn:aws:s3:::acme-tf-state"]
  }

  statement {
    sid     = "StateObjectRW"
    effect  = "Allow"
    actions = ["s3:GetObject", "s3:PutObject"]
    resources = ["arn:aws:s3:::acme-tf-state/prod/terraform.tfstate"]
  }

  statement {
    sid     = "StateLockfileRW"
    effect  = "Allow"
    actions = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"]
    resources = ["arn:aws:s3:::acme-tf-state/prod/terraform.tfstate.tflock"]
  }
}

resource "aws_iam_policy" "tf_state" {
  name   = "gha-terraform-deploy-state"
  policy = data.aws_iam_policy_document.tf_state.json
}

resource "aws_iam_role_policy_attachment" "tf_state" {
  role       = aws_iam_role.gha_terraform.name
  policy_arn = aws_iam_policy.tf_state.arn
}

ملف الحالة نفسه لا يحتاج إلى s3:DeleteObject — يقوم Terraform بالكتابة فوقه في مكانه. يحتاج ملف القفل إلى DeleteObject لأن Terraform يحذف كائن .tflock لتحرير القفل في نهاية عملية apply.9 ثم قم بإرفاق أي سياسات مدارة أو مضمنة تحتاجها مجموعتك لإنشاء الموارد (مثل AmazonEC2FullAccess، أو سياسة مخصصة لخدماتك المحددة، إلخ).

الخطوة 4: ربط بيئة العمل مع configure-aws-credentials v6 و id-token write

الآن سير العمل (workflow). الوسم العائم الحالي aws-actions/configure-aws-credentials@v6 يشير إلى الإصدار v6.1.1 اعتباراً من 5 مايو 2026؛ قم بتثبيته على الإصدار غير القابل للتغيير @v6.1.1 إذا كنت تريد قفل سلسلة التوريد بالكامل.10 أنشئ الملف .GitHub/workflows/terraform.yml:

name: terraform-deploy

on:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./infra
    steps:
      - uses: actions/checkout@v6.0.2

      - name: Configure AWS credentials via OIDC
        uses: aws-actions/configure-aws-credentials@v6.1.1
        with:
          role-to-assume: arn:aws:iam::123456789012:role/gha-terraform-deploy
          role-session-name: gha-${{ GitHub.run_id }}
          aws-region: us-east-1

      - name: Verify identity
        run: aws sts get-caller-identity

      - uses: hashicorp/setup-terraform@v4.0.1
        with:
          terraform_version: 1.15.2

      - run: terraform init
      - run: terraform plan -out=tfplan
      - run: terraform apply -auto-approve tfplan

هناك إذنان يقومان بعمل محدد: id-token: write يسمح للمشغل (runner) بطلب توكن OIDC من موفر الهوية الخاص بـ GitHub (مطلوب لـ OIDC، بدون استثناءات)، و contents: read يسمح لـ actions/checkout بجلب المستودع عبر التوكن المقدم من GitHub. بمجرد التصريح بكتلة permissions:، فإن جميع الأذونات غير المدرجة تصبح "لا شيء" افتراضياً، لذا فإن حذف contents: read هو سبب شائع لفشل عمليات الـ checkout الغامضة في المستودعات الخاصة والداخلية.11 إذا أضفت لاحقاً كتلة permissions: على مستوى الوظيفة (job-level)، تذكر أن أذونات مستوى الوظيفة تحل محل أذونات مستوى سير العمل بالكامل — ولا يتم دمجهما.

role-session-name: gha-${{ GitHub.run_id }} يجعل كل جلسة يتم انتحالها قابلة للتتبع وصولاً إلى تشغيل سير العمل المحدد في CloudTrail. القيمة الافتراضية للإجراء هي مجرد "GitHubActions"، وهي صحيحة ولكنها غير مفيدة عندما تلاحق إدخالاً في سجل التدقيق.5

الخطوة 5: تقوية مطالبة sub — التثبيت ببيئة، وليس بفرع

سياسة الثقة المحددة بفرع جيدة. لكن المحددة ببيئة (environment) أفضل — وهي تسمح لك بطلب موافقة يدوية قبل أن يتم انتحال الدور. يلزم إجراء تغييرين منسقين.

أولاً، في إعدادات مستودع GitHub، أنشئ بيئة production مع مراجعين مطلوبين، ثم أضف مفتاح environment: إلى وظيفة النشر في terraform.yml:

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    # ...rest unchanged

ثانياً، قم بتحديث شرط سياسة الثقة لمطابقة environment:production بدلاً من ref:refs/heads/main. عندما تشير وظيفة إلى بيئة، يقوم GitHub بتجاوز مطالبة الموضوع (subject claim) إلى repo:org/repo:environment:<name> بغض النظر عما إذا كان المحفز هو دفع (push) أو workflow_dispatch، لذا تقوم بتبديل القيمة المطابقة التي تتوقعها سياسة الثقة:

condition {
  test     = "StringEquals"
  variable = "token.actions.githubusercontent.com:sub"
  values   = [
    "repo:${local.github_org}/${local.github_repo}:environment:production"
  ]
}

التأثير المشترك: تتوقف الوظيفة مؤقتاً بحالة "Waiting" حتى يوافق المراجع المطلوب عليها، ولا يمكن انتحال الدور إلا من خلال سير العمل الذي يتم تشغيله داخل تلك البيئة المعتمدة — لا إنسان، لا توكن، لا وصول إلى AWS — حتى لو قام شخص ما بالدفع مباشرة إلى main.12

إذا كنت بحاجة إلى مشاركة دور لكل من عمليات النشر عند الدفع إلى main وإصدار الوسوم (tags)، فانتقل إلى StringLike وادرج كلا النمطين:

condition {
  test     = "StringLike"
  variable = "token.actions.githubusercontent.com:sub"
  values   = [
    "repo:${local.github_org}/${local.github_repo}:ref:refs/heads/main",
    "repo:${local.github_org}/${local.github_repo}:ref:refs/tags/v*"
  ]
}

ما لا يجب عليك فعله أبداً هو ترك سياسة الثقة بشرط aud فقط، أو تحديد نطاق sub إلى repo:${org}/*:*. أي منهما يعني أن أي فرع في أي من مستودعاتك — بما في ذلك فرع ميزة قام مقاول بدفعه بالأمس — يمكنه انتحال الدور.

الخطوة 6: الاستعداد لمطالبات الموضوع غير القابلة للتغيير (بدءاً من 18 يونيو 2026)

هذه هي المشكلة الخاصة بعام 2026 التي لا تغطيها معظم البرامج التعليمية الحالية. في 23 أبريل 2026، أعلن GitHub أن مطالبات موضوع OIDC ستلحق معرفات المالك والمستودع غير القابلة للتغيير للدفاع ضد هجمات إعادة استخدام الأسماء. المستودعات التي تم إنشاؤها بعد 18 يونيو 2026 ستصدر تلقائياً توكنات بمواضيع مثل:1

repo:octocat-12345/my-repo-67890:ref:refs/heads/main

تستمر المستودعات الحالية في إصدار تنسيق الاسم القابل للتغيير فقط ما لم يوافق المالك على التغيير مبكراً. هناك استنتاجان:

إذا كانت سياسة الثقة الخاصة بك تستخدم StringEquals على سلسلة الموضوع الكاملة، فيجب أن تتضمن سياسة الثقة الخاصة بك لمستودع جديد تم إنشاؤه بعد 18 يونيو معرف المالك ومعرف المستودع. يمكنك اكتشافهما باستخدام gh API repos/<owner>/<repo> --jq '{id, owner_id: .owner.id}' (يرفض واجهة سطر أوامر GitHub وجود شرطة مائلة بادئة في مسار نقطة النهاية). بالنسبة للمستودعات الحالية التي لم تشترك بعد، لا يزال التنسيق القديم يعمل.

إذا لم تتمكن من التنبؤ بالمعرفات مسبقاً، فإن النمط المتوافق مع المستقبل هو StringLike مع مصطلحي glob — أحدهما يطابق التنسيق القديم، والآخر يطابق التنسيق غير القابل للتغيير:

values = [
  "repo:${local.github_org}/${local.github_repo}:ref:refs/heads/main",
  "repo:${local.github_org}-*/${local.github_repo}-*:ref:refs/heads/main"
]

إذا اعتمدت السياسة المحددة بالبيئة من الخطوة 5، فاستبدل :ref:refs/heads/main بـ :environment:production في كلا مصطلحي glob. هذا هو أنظف مسار للهجرة حتى تشترك في المستودع وتلتقط المعرفات.

التحقق

قم بدفع التزام (commit) لا يقوم بأي عملية إلى main وراقب تشغيل سير العمل. يجب أن تطبع خطوة "Verify identity" شيئاً مثل:

{
    "UserId": "AROAEXAMPLE:gha-1234567890",
    "Account": "123456789012",
    "Arn": "arn:aws:sts::123456789012:assumed-role/gha-terraform-deploy/gha-1234567890"
}

في CloudTrail، ابحث عن حدث AssumeRoleWithWebIdentity المطابق. سيكون requestParameters.roleSessionName هو gha-${run_id} وسيكون responseElements.subjectFromWebIdentityToken هو القيمة الدقيقة التي حملها توكن JWT الخاص بـ GitHub في مطالبة sub الخاصة به — repo:<org>/<repo>:ref:refs/heads/main إذا توقفت عند الخطوة 4، أو repo:<org>/<repo>:environment:production إذا قمت بالتقوية إلى البيئة في الخطوة 5. هذا الحقل هو مرساة التدقيق الخاصة بك.

الأخطاء الشائعة

Error: Could not load credentials from any providers — غالباً ما يكون بسبب فقدان permissions: id-token: write سواء على مستوى سير العمل أو مستوى الوظيفة. إضافته على مستوى الوظيفة هي التي تسود.11

Not authorized to perform sts:AssumeRoleWithWebIdentity — شرط sub في سياسة الثقة لا يطابق ما يرسله سير العمل فعلياً. ترسل عمليات تشغيل PR القيمة repo:org/repo:pull_request، وليس ref:refs/heads/.... ترسل عمليات دفع الوسوم (tags) القيمة ref:refs/tags/<tag>. أضف actions-oidc-debugger لطباعة المطالبة الفعلية، ثم قم بتحديث السياسة.5

InvalidIdentityToken: Provider not found — موفر IAM OIDC غير موجود في الحساب، أو قمت بتسجيله بعنوان URL مختلف. يجب أن يكون عنوان URL هو https://token.actions.githubusercontent.com تماماً بدون شرطة مائلة في النهاية وبدون مسار.

InvalidIdentityToken: Couldn't retrieve verification key — عادةً ما يتم تعيين audience: مخصص في الإجراء ولكن لم يتم إضافته إلى client_id_list الخاص بموفر IAM. يجب أن تطابق مطالبة aud في JWT معرف عميل مسجلاً لدى الموفر.

ينجح الـ Plan، ويتوقف الـ apply عند الحصول على القفل (lock) — لقد انتقلت من قفل DynamoDB ونسيت حذف وسيط dynamodb_table القديم من تكوين الواجهة الخلفية (backend)، أو يفتقر الدور إلى إذن s3:DeleteObject على مفتاح .tflock. راجع برنامج تعليمي لقفل حالة Terraform S3 لمعرفة مسار الهجرة للقفل المزدوج.

الخطوات التالية

قم بتطبيق نفس نمط سياسة الثقة على أدوار إضافية لبيئات staging مقابل production، مع تحديد نطاقها لبيئاتها الخاصة. أضف inline-session-policy في الإجراء لتشديد ما يمكن للدور المنتحل القيام به في وقت التشغيل — وهو أمر مفيد عندما يخدم دور واحد عدة سير عمل. وقم بإلغاء أي مفاتيح AWS_ACCESS_KEY_ID طويلة الأمد لا تزال موجودة في أسرار المستودع: فهي الآن خطيرة بشكل نشط، لأن الشيء الوحيد الذي يمنع تسريب البيانات هو طبقة لم تعد بحاجة إليها.

Footnotes

  1. سجل تغييرات GitHub، "مطالبات الموضوع غير القابلة للتغيير لرموز OIDC الخاصة بـ GitHub Actions"، 23 أبريل 2026 — GitHub.blog 2

  2. إصدارات HashiCorp Terraform — releases.hashicorp.com/terraform/

  3. مرجع أوامر AWS CLI، iam create-open-id-connect-providerdocs.aws.amazon.com

  4. سجل تغييرات GitHub، "تكامل OIDC مع AWS لم يعد يتطلب تثبيت شهادات TLS الوسيطة"، 13 يوليو 2023 — GitHub.blog

  5. ملف README الخاص بـ aws-actions/configure-aws-credentials (الإصدار v6.1.1، 5 مايو 2026) — GitHub.com 2 3 4

  6. مشكلة hashicorp/terraform-provider-aws رقم 40509: "aws_iam_openid_connect_provider لا يمكنه مسح thumbprint_list بمجرد تعيينه" — GitHub.com

  7. وثائق GitHub، "تكوين OpenID Connect في Amazon Web Services" — docs.GitHub.com

  8. مشكلة aws-actions/configure-aws-credentials رقم 953، "إذا كان اسم الدور المفترض هو GitHubActions فسيفشل الإجراء بخطأ غير محدد"؛ يربطه ملف README تحت عنوان "تم الإبلاغ عن أن تسمية دورك 'GitHubActions' لا تعمل" — GitHub.com

  9. HashiCorp، "نوع الواجهة الخلفية: s3" (مرجع use_lockfile) — developer.hashicorp.com

  10. إصدار aws-actions/configure-aws-credentials v6.1.1، نُشر في 05-05-2026 — GitHub.com

  11. وثائق GitHub، "OpenID Connect" — docs.GitHub.com 2

  12. مدونة AWS الأمنية، "استخدام أدوار IAM لربط GitHub Actions بالإجراءات في AWS" — aws.amazon.com


نشرة أسبوعية مجانية

ابقَ على مسار النيرد

بريد واحد أسبوعياً — دورات، مقالات معمّقة، أدوات، وتجارب ذكاء اصطناعي.

بدون إزعاج. إلغاء الاشتراك في أي وقت.