linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
Intro
Lessons
Footer
linuxlab-УчебникиЦеныО платформеКонфиденциальность и куки
Copyright © 2026 LinuxLab. Все права защищены.
linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
  • Введение
  • Уроки
  • How it works
  • База знаний
  • Шпаргалка
  • Capstone
  • Собеседование
home/terraform/kb/CI/CD/tf-oidc-aws

kb/cicd ── CI/CD ── intermediate

OIDC между GitHub Actions и AWS, без access keys

Раньше 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`.

view as markdownaka: github-oidc-aws, terraform-oidc, aws-iam-oidc-github

Зачем

До 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:prodWorkflow с 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-claimPermissions
github-tf-runner-dev:ref:refs/heads/*dev-account или scoped IAM
github-tf-runner-stage:environment:stagestage-resources
github-tf-runner-prod:environment:prodprod-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 доверяет.

§ см. также

  • tf-plan-apply-ciPlan-as-artifact и automation mode в CIВ CI plan-job создаёт `plan.tfplan`, грузит как артефакт. Apply-job скачивает артефакт и применяет ровно его, никакого нового plan на apply-стадии. Это гарантирует: то что показали reviewer'у в PR ровно то, что применилось. Все команды с `-input=false`, `-no-color`, `-lock-timeout` чтобы автоматизация не зависала. Plan-файл содержит sensitive, обращайся с ним как с секретом.
  • tf-secrets-in-stateСекреты и Terraform state: где хранить и как читатьState содержит всё что прошло через apply, пароли, ключи, токены в открытом виде. Решения: хранить секреты в Secrets Manager / Vault / KMS, читать через data-source, шифровать backend (S3 SSE-KMS), OIDC вместо access-keys для CI. «sensitive=true», про логи, не про шифрование.
  • tf-policy-as-codeOPA + Rego, policy as code для Terraform planPolicy-as-code = правила («все S3 шифрованы», «никаких IAM с *») написаны кодом, гонятся в CI, fail'ят PR. OPA, стандарт, Rego, язык. conftest, обёртка с CLI-friendly выводом; читает plan.json, прогоняет правила, exit 0/1. Зрелее и дороже на старте, чем Checkov, но позволяет cross-resource правила любой сложности.
Footer
linuxlab-
Copyright © 2026 LinuxLab. Все права защищены.
Учебники
Цены
О платформе
Конфиденциальность и куки