Add solidago-dev-github-actions-terraform-plan: AWS ReadOnlyAccess plus
state read, the S3-native lockfile (env/dev/terraform.tfstate.tflock),
the tfstate CMK, and this stack's secret values (ReadOnlyAccess omits
GetSecretValue, which refresh of the secret versions needs). Trusted only
for repo:lentago/solidago:pull_request.
Widen the existing (apply) role's trust to ref:refs/heads/main alongside
the transitional environment:terraform sub. terraform.yml still assumes
the existing role from both jobs, so this PR's own plan and its merge
apply are unaffected; step 2 switches the jobs and drops the legacy sub.
Both jobs now log `aws sts get-caller-identity`. AVD-AWS-0345 ignore
stays on the apply role (s3:* by design) and now points at #225.
Refs #225
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Step 1 of 2 for #225 (split the Terraform CI role). Part of #225. Step 2, which closes the issue, is the stacked draft PR #227.
Why two steps
Both roles are managed by Terraform, and the plan role doesn't exist until an apply creates it. That apply runs under the existing role, which today trusts only
repo:lentago/solidago:environment:terraform. If this PR moved the plan job to the new role, its own PR plan would fail to assume a role that doesn't exist yet. If it also moved the apply job off the environment, its merge apply would fail because the trust change isn't live yet. So this PR creates the role but doesn't switch to it, and step 2 switches the jobs and then narrows the trust. The README section "Terraform CI Roles: Plan vs Apply" documents the ordering.Changes
solidago-dev-github-actions-terraform-plan(modules/iam)repo:lentago/solidago:pull_request. GitHub mints this subject for a PR job that has noenvironment:key.ReadOnlyAccess, plus what a plan needs thatReadOnlyAccesslacks. These come from the backend config and from what refresh reads:s3:GetObjectonenv/dev/terraform.tfstate, andListBucket.env/dev/terraform.tfstate.tflock. The backend uses S3-native locking (use_lockfile = true), so there's no DynamoDB table for this stack.DecryptandDescribeKey, plusGenerateDataKeyfor the SSE-KMS lockfile PUT.secretsmanager:GetSecretValueonsolidago-dev-*secrets, pluskms:Decrypton the main CMK, but only via Secrets Manager (kms:ViaService).ReadOnlyAccessleaves outGetSecretValue. Refreshingaws_secretsmanager_secret_version(modules/secrets) and theaxiom_tokendata source (modules/alb-log-shipper) needs it, and the main key policy hands authorization to IAM through its root statement.ref:refs/heads/mainis added next to the transitionalenvironment:terraformsubject.terraform.yml: both jobs gain anAWS caller identitystep (aws sts get-caller-identity). Role ARNs are unchanged in this step.trivy:ignore:AVD-AWS-0345stays on the apply role, becauses3:*is there by design. Its comment now says Split the Terraform CI role: read-only plan credentials on pull requests, write apply credentials on main (tf-lint AVD-AWS-0345) #225 resolved Hardening backlog from tf-lint's first trivy run (ECR immutability, CI-role least-privilege, SNS encryption) #180 item 2 as a split.github_actions_terraform_plan_role_arn.Expected plan
Adds 4 resources: the role, a customer policy, and 2 attachments (
ReadOnlyAccessand the custom policy). There's one in-place update: the trust policy ofmodule.iam.aws_iam_role.github_actions_terraform. Nothing is destroyed. This PR's plan and merge apply still run under the existing role viaenvironment:terraform.Verification
terraform fmt -checkandterraform validatepass formodules/iamandenvironments/dev.Notes for the reviewer
pull_requestworkflow in this repo withid-token: write(for exampleclaude.ymlorclaude-review.yml) can assume the plan role. That gives read-only access to the account, including other repos' state objects throughReadOnlyAccess'ss3:Get*. That matches the issue's "ReadOnlyAccess-class" target. Today the same callers can already reach the write role by declaringenvironment: terraform, because that environment has no deployment-branch policy. Step 2 closes that gap.🤖 Generated with Claude Code