# OIDC между GitHub Actions и AWS, без access keys _CI/CD · TerraformLab Knowledge Base_ **TL;DR:** Раньше CI-runner ходил в AWS с долгоживущим access key. OIDC переворачивает это: GitHub выдаёт workflow signed JWT, STS обменивает на временные credentials через AssumeRoleWithWebIdentity. Никаких secrets, scope роли сужается до repo+branch+env. Три артефакта: IAM OIDC-провайдер, IAM-роль с trust policy, workflow с `id-token: write`. ## Зачем До OIDC у каждой команды в CI был долгоживущий AWS IAM User с двумя секретами в GitHub: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`. Все knows: - Утечка через лог = катастрофа. - Ротация ручная, делается раз в год вместо раз в 90 дней. - Один ключ, один CI; невозможно ограничить «можно только из main». - Forensics: «кто это сделал?», невозможно, потому что любой CI-action шёл от одного и того же IAM user. OIDC: AWS доверяет GitHub как identity provider. GitHub при запуске workflow выдаёт runner'у короткий JWT (5 минут). Runner идёт в STS AssumeRoleWithWebIdentity → получает временные credentials (1 час). Никаких долгих секретов; trust policy ограничивает trust до конкретного repo/branch/environment. ## Шаг 1: IAM OIDC Provider Создаётся один раз per AWS-account. Через CLI: ```bash aws iam create-open-id-connect-provider \ --url https://token.actions.githubusercontent.com \ --client-id-list sts.amazonaws.com \ --thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1 ``` Через Terraform: ```hcl resource "aws_iam_openid_connect_provider" "github" { url = "https://token.actions.githubusercontent.com" client_id_list = ["sts.amazonaws.com"] thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"] } ``` Thumbprint, отпечаток SSL-сертификата GitHub. AWS его проверяет. Иногда GitHub меняет cert и thumbprint становится stale (старая проблема). Сейчас рекомендуется опускать thumbprint, AWS делает верификацию через PKI бренд (с лета 2023). ## Шаг 2: IAM Role с trust policy ```hcl resource "aws_iam_role" "github_tf_runner" { name = "github-tf-runner" 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:linuxlab/terraform-infra:ref:refs/heads/main" } } }] }) } resource "aws_iam_role_policy_attachment" "github_tf_runner" { role = aws_iam_role.github_tf_runner.name policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess" # или Custom } ``` Sub-claim, самое важное: | Значение `sub` | Что разрешает | |---|---| | `repo:org/repo:ref:refs/heads/main` | Только workflow на main | | `repo:org/repo:ref:refs/tags/*` | Любой тег | | `repo:org/repo:pull_request` | Только PR-context (read-only обычно) | | `repo:org/repo:environment:prod` | Workflow с `environment: prod` | | `repo:org/*:*` | **Слишком широко**, любой workflow орга | Конкретнее = безопаснее. Для prod-роли: `:environment:prod` и pin'нуть одно environment, требующее approval. ## Шаг 3: Workflow ```yaml # .github/workflows/tf-apply.yml permissions: id-token: write # обязательно для OIDC contents: read jobs: apply: runs-on: ubuntu-latest environment: prod # привязка к prod-роли steps: - uses: actions/checkout@v4 - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::ACCOUNT:role/github-tf-runner aws-region: us-east-1 role-session-name: gha-${{ github.run_id }} - uses: hashicorp/setup-terraform@v3 - run: terraform init - run: terraform apply -auto-approve plan.tfplan ``` `permissions: id-token: write`, без неё GitHub не выдаст OIDC-токен, и action provider'а падает с «No OIDC token available». `role-session-name: gha-${{ github.run_id }}`, название сессии в STS. В CloudTrail видно, какая run-id создала ресурс. Forensics-friendly. ## Несколько ролей: dev / stage / prod Антипаттерн: одна роль для всех env. Если CI на dev-ветке взломан, он ходит и в prod. Правильно: | Роль | Sub-claim | Permissions | |---|---|---| | `github-tf-runner-dev` | `:ref:refs/heads/*` | dev-account или scoped IAM | | `github-tf-runner-stage` | `:environment:stage` | stage-resources | | `github-tf-runner-prod` | `:environment:prod` | prod-resources | В каждом workflow указываешь свою `role-to-assume` в зависимости от целевой среды. ## Read-only plan-роль Plan-job нужен только AWS-read (для refresh). Apply-job, read+write. Split: ```hcl resource "aws_iam_role" "github_tf_plan" { name = "github-tf-plan" assume_role_policy = jsonencode({ Statement = [{ Effect = "Allow" Principal = { Federated = aws_iam_openid_connect_provider.github.arn } Action = "sts:AssumeRoleWithWebIdentity" Condition = { StringLike = { "token.actions.githubusercontent.com:sub" = "repo:linuxlab/terraform-infra:pull_request" } } }] Version = "2012-10-17" }) } resource "aws_iam_role_policy_attachment" "plan_readonly" { role = aws_iam_role.github_tf_plan.name policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess" } ``` Плана не нужно `s3:CreateBucket`. ReadOnlyAccess (или собственный custom с view-permissions для покрываемых сервисов) хватает. Compromised PR не может ничего создать. ## Подводные камни - **Sub-claim ошибка, vulnerability.** `repo:org/*:*` означает «любой repo в org может assume». Доверяешь всем team-mate'ам, всем fork'ам, всем pull_request'ам. Всегда конкретный repo + конкретный ref/environment. - **`permissions: id-token: write` забывают на subworkflow.** Если выполняется через `uses: ./.github/workflows/tf.yml` (reusable workflow), он не наследует permissions парента. Прописывать в каждом workflow или в `defaults`. - **Sessions длятся 1 час.** Долгий apply (1.5h) умрёт на отказе в refresh credentials. Решение: бить apply на куски (`-target` не рекомендую, лучше split-modules), или поднять `DurationSeconds` в role (`max-session-duration` до 12h) + явный `role-duration-seconds` в action. - **OIDC не работает с self-hosted runners по default.** Self-hosted нуждается в правильно настроенном `ACTIONS_ID_TOKEN_REQUEST_URL` окружении (GitHub его выставляет на GitHub-hosted, на self-hosted нет). Для self-hosted: либо use GitHub-hosted для apply, либо отдельная конфигурация. - **Thumbprint от старой документации.** Старые туторы говорят «обязателен thumbprint`. С 2023 AWS делает PKI-валидацию, thumbprint игнорируется. Прописал стейл значение, провайдер всё равно работает, но не доверяй ему как security-control. - **OIDC ≠ ноль секретов.** Сам role-arn, это «полу-секрет». Если публикуешь в public-репо, atttacker знает, какую роль assume'ить (хотя без OIDC-токена из его workflow ничего не сделает). Trust-policy всё равно держит. - **CloudTrail для OIDC-roles тяжелее читать.** `Principal` в логах federated identity, ID сессии, gha-run_id. Сделай query-saver в Athena/CloudWatch чтобы быстро находить «кто apply'ил». ## Команды ```bash aws iam create-open-id-connect-provider --url https://token.actions.githubusercontent.com --client-id-list sts.amazonaws.com ``` Создать IAM OIDC-провайдера. Per-account один раз. ```bash aws sts assume-role-with-web-identity --role-arn ROLE_ARN --web-identity-token "$TOKEN" --role-session-name dbg ``` Ручной obmen JWT на credentials. Так делает aws-actions/configure-aws-credentials. ```bash aws sts get-caller-identity ``` После configure-aws-credentials, проверить, какая роль реально assumed. ```bash aws iam list-role-tags --role-name github-tf-runner ``` Прочитать теги роли, useful чтобы помнить, какой repo'у этот role доверяет. ## См. также - [Plan-as-artifact и automation mode в CI](/terraform/kb/tf-plan-apply-ci.md) - [Секреты и Terraform state: где хранить и как читать](/terraform/kb/tf-secrets-in-state.md) - [OPA + Rego, policy as code для Terraform plan](/terraform/kb/tf-policy-as-code.md)