AWS_ACCESS_KEY_ID в CI-переменных — стандарт 2018-го, anti-pattern 2026-го. Федеративная идентичность через OIDC даёт CI/CD короткоживущие STS-credentials по sub-клейму токена, а в CloudTrail оседает не безымянный machine-user, а конкретный workflow с веткой и репозиторием. Единый паттерн работает и для GitHub Actions, и для GitLab CI, и для self-hosted Atlantis.
Что не так с долгоживущим ключом
aws_iam_access_key живёт месяцами и хранится в secrets репозитория или CI-переменной. Это даёт сразу три проблемы, которые не лечатся «лучшей политикой паролей»:
- Ручная ротация. Перевыпустить ключ — это PR в SaaS, новое значение в каждый секрет каждого репозитория, и надежда, что никто не положил его в
~/.aws/credentialsна ноутбуке. - Blast radius при leak. Ключ с правом
terraform applyвалидно работает хоть из CI, хоть из Slack-чата атакующего. Сетевая граница —0.0.0.0/0. - CloudTrail без атрибуции.
eventSource=ec2.amazonaws.com,userIdentity.userName=ci-deployer-prod, и никакой связи с тем, какой PR и чьей кнопкой это запустил.
OIDC как замена: три действующих лица
Федеративный сценарий заменяет «передать ключ» на «обменять токен на короткую сессию». В нём три действующих лица:
- CI-провайдер — выпускает подписанный JWT (
id_token) с клеймами: репозиторий, ветка, environment, project_id. - IAM OIDC Identity Provider на AWS — один на провайдера, ARN формы
arn:aws:iam::<acc>:oidc-provider/<issuer-host>. Audience всегдаsts.amazonaws.com. - IAM Role с trust policy —
Federated principalуказывает на OIDC-провайдера;Conditionотсеивает запросы поsub-клейму.
Когда job начинается, CI кладёт JWT во временную переменную и вызывает sts:AssumeRoleWithWebIdentity. AWS проверяет подпись токена, сверяет aud и sub с trust policy и возвращает AccessKeyId/SecretAccessKey/SessionToken, действующие час или меньше. В CloudTrail остаётся не iam-user/ci-prod, а arn:aws:sts::…:assumed-role/<role>/<run-id> плюс sub в webIdFederationData.
Минимальный setup на трёх провайдерах
GitHub Actions — issuer token.actions.githubusercontent.com, sub-формат repo:org/repo:ref:refs/heads/main (или :environment:prod, или :pull_request):
permissions:
id-token: write # обязательно
contents: read
jobs:
deploy:
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::<acct>:role/gha-deploy
aws-region: eu-central-1
- run: terraform apply -auto-approve
GitLab CI (SaaS) — issuer https://gitlab.com, sub-формат project_path:group/project:ref_type:branch:ref:main. JWT приходит как id_tokens, обмен — aws sts assume-role-with-web-identity явным шагом.
Self-hosted Atlantis — issuer берётся либо у самого Atlantis, либо у IDP за ним; scope — per-project IAM role, не общая «atlantis-runner».
Trust policy одинакова с точностью до подстановки issuer и sub-pattern:
resource "aws_iam_role" "ci_deploy" {
assume_role_policy = jsonencode({
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRoleWithWebIdentity"
Principal = { Federated = "arn:aws:iam::${var.acc}:oidc-provider/${var.issuer}" }
Condition = {
StringEquals = { "${var.issuer}:aud" = "sts.amazonaws.com" }
StringLike = { "${var.issuer}:sub" = var.sub_pattern }
}
}]
})
}
Чего нельзя делать, даже когда «работает»
Три анти-паттерна, которые регулярно встречаются в публичных репозиториях и обнуляют выигрыш от OIDC:
AdministratorAccessна OIDC-роли «для демо». Federated-роль обязана быть least-privilege. Иначе при компрометации build-скрипта (зависимость от npm-пакета, supply-chain) злоумышленник делает то же, что давал вашiam-user, только с лучшей атрибуцией — никакого выигрыша.- Один job делает
npm buildиterraform apply. Build исполняется на default-runner'е, может тянуть произвольные зависимости, а потом этот же процесс получает временный токен с правами в проде. Правильно — build ассумит ноль ролей, артефакт уезжает в registry, отдельный apply job вызываетconfigure-aws-credentials. Радиус взрыва компрометации зависимости сжимается до прав на pull. AWS_ACCESS_KEY_IDв env CI-runner'а живёт «для совместимости». Переменные окружения перебиваютAWS_PROFILEв aws-CLI; одна забытаяAWS_ACCESS_KEY_ID=…в groups-level variables — и OIDC-сессия игнорируется. В CI-shell передterraformвсегдаunset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN.
Отдельная ловушка для Terraform: module { source = "s3::…" } не уважает AWS_PROFILE — модули скачаются под дефолтными credentials. Лечится двумя способами: aws configure export-credentials --format env перед terraform init, или хранить модули в Git/registry, а не в S3.
Что вы перестанете делать
После миграции на OIDC исчезает рутинный поток задач: ротация ключей, ревью, кто и когда добавил AWS_* в secrets, аудит покинувших команду людей с ключами на ноутбуках. Постоянных секретов в CI остаётся ноль; периметр атаки CI/CD сжимается до возможности подделать JWT-подпись GitHub или GitLab — что в текущей модели угроз эквивалентно компрометации самого провайдера.
Это та редкая security-миграция, которая не добавляет процедур, а удаляет их.