Repository navigation
fix(iac/aws): condition RunTask and PassRole on the recommendations and RI-exchange invoker roles - #1118
fix(iac/aws): condition RunTask and PassRole on the recommendations and RI-exchange invoker roles#1118IAmJSD wants to merge 1 commit into
Conversation
…nd RI-exchange invoker roles The recommendations and RI-exchange EventBridge invoker roles in the Fargate module granted ecs:RunTask and iam:PassRole with no conditions, unlike the ladder-run and fire-scheduled-purchases roles beside them. They could run the task definition in any cluster in the account and pass the task role, which holds the SP/RI purchase grants, to any service that accepts a passed role. Add the same two conditions the other two roles carry: ArnEquals ecs:cluster = the module's cluster on RunTask, and StringEquals iam:PassedToService = ecs-tasks.amazonaws.com on PassRole. Tests: - fargate/tests/eventbridge_invoker_iam.tftest.hcl asserts the rendered policy of all four invoker roles (mock provider, no real resources). - eventbridge_invoker_iam_test.go walks terraform/ and cloudformation/stacks for every Allow ecs:RunTask grant and requires both conditions, so a fifth copy added later is held to the same rule. Closes LeanerCloud#292 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Comment |
|
VERDICT: APPROVE 38a786b Reviewed head 38a786b (one commit) against origin/main b6a0649. Findings
Correctness
Attacked / local results (Terraform 1.10.5)
|
|
Independent review 2 (claude-fleet, cold): APPROVE 38a786b Findings (none blocking):
Attacked:
Verified locally, with Terraform 1.10.5 and go1.26.9, no cloud credentials, under the heavy lock:
Reviewed by an independent agent with no author context (second reviewer). Review only: nothing was pushed, and the GitHub review state is unchanged. |
Root cause
terraform/modules/compute/aws/fargate/main.tfhas four EventBridge invoker roles that do the same thing, each with its own hand-copied policy.eventbridge_ladder_runandeventbridge_fire_scheduled_purchasesconditionecs:RunTaskonArnEquals ecs:clusterandiam:PassRoleonStringEquals iam:PassedToService = ecs-tasks.amazonaws.com.eventbridge(recommendations) andeventbridge_ri_exchangehad neither condition. So either role could run the task definition in any cluster in the account, and could pass the task role (which holds the SP/RI purchase grants) to any service that accepts a passed role. Reaching that needs someone who can create EventBridge targets in our account, because the roles trust onlyevents.amazonaws.com.Fix
The two
Conditionblocks fromeventbridge_ladder_runare now on the recommendations and RI-exchange policies, unchanged. TheResourcescoping stays as it was: RunTask onaws_ecs_task_definition.main.arn, PassRole on the task and task-execution role ARNs. After this, all four invoker policies are the same apart from their names. The comment inci-cd-permissions/policy_boundary.tf("The ecs:cluster condition and the task-definition Resource stay on the module policy") was only true for two of the four roles before this PR. It is now true for all four.Runtime effect: none for the scheduled tasks. The EventBridge targets already run
aws_ecs_task_definition.maininaws_ecs_cluster.main, and ECS passes the roles toecs-tasks.amazonaws.com. That is the configuration the ladder-run and fire-scheduled-purchases roles already run with.Tests
terraform/modules/compute/aws/fargate/tests/eventbridge_invoker_iam.tftest.hcl(new; CI's "Test AWS compute modules" loop picks it up). It turns on all four schedules and runscommand = applyagainstmock_provider "aws", so nothing is created. Itjsondecodes each of the four rendered policies and checks two things. First, there is exactly one RunTask statement, on the module's task definition, withArnEquals ecs:cluster= the module's cluster. Second, there is exactly one PassRole statement, on exactly the task and task-execution roles, withStringEquals iam:PassedToService = ecs-tasks.amazonaws.com. The task and task-execution roles get distinct overridden ARNs, so a PassRole statement that names only one of them can't pass because two mock ARNs collapsed into one value.terraform/modules/compute/aws/eventbridge_invoker_iam_test.go(new Go guard, same package and the same tree walk as the #1636 cross-account guard). The tftest names four specific policies. This guard instead finds every Allowecs:RunTaskgrant (alsoecs:*andecs:Run*, case-insensitive) underterraform/andcloudformation/stacks. It requires each grant to have:Resourceon anaws_ecs_task_definition.<x>.arnreferenceArnEquals "ecs:cluster" = aws_ecs_cluster.<x>.arn(by reference, so a literal wildcard ARN fails)aws_iam_role.<x>.arnreferences and carrying theiam:PassedToServiceconditionIf someone copies the policy a fifth time, the new copy is held to the same rule. As a floor, the guard must find all four known policies, so it can't pass by finding nothing.
TestRunTaskGuardRejectsUnconditionedGrantsruns each matcher against the pre-fix and post-fix shapes of the recommendations policy. The permissions boundary (policy_boundary.tf) is a ceiling rather than a grant. The existing walk already skips it.Verification
All evidence below is unit, mock or fixture evidence. Nothing was deployed, and no
terraform plan/applyran against a real backend or account.Fail before the fix (
main.tfatorigin/main, new tests in place):Only the two roles named in the issue fail. Ladder-run and fire-scheduled-purchases pass before the fix as well.
Pass after the fix:
Mutation probes, each applied to the fixed
main.tfand then reverted:ecs:clustercondition (recommendations)iam:PassedToService->lambda.amazonaws.com(recommendations; RI exchange)ecs:cluster-> literalarn:aws:ecs:*:*:cluster/*(fire)Resource = "*"(ladder)"ecs:RunTask"->"ecs:*"(recommendations)Other checks run locally:
terraform fmt -check -recursive terraform/;terraform validateon the module, and onterraform/environments/awswithinit -backend=false(Terraform 1.10.5).terraform.lock.hclfor the fargate module is unchanged. It already carries linux_amd64, darwin_arm64 and darwin_amd64 hashes:terraform providers lockfor those three platforms left it byte-identical. CI'sinit -lockfile=readonlysucceeds on it.go build ./...,go vet, golangci-lint v2.10.1 on./terraform/modules/compute/aws/...(0 issues),gocyclo -over 10(silent)go test ./internal/mocks/ -count=1andgo test ./terraform/environments/aws/ci-cd-permissions/ -count=1pass--register-awsplus thesetup-git-secrets.shpatterns,.gitallowedapplied) scanned the changed files in an isolated temp repo: clean.scripts/test-git-secrets-allowlist.sh: 42 PASS, 0 FAIL. Fixtures use the123456789012placeholder in ARN position only.trivy config --severity HIGH,CRITICALon the module: 0 findingsAWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null, because the default profile on this machine is invalid.Real-cloud gap: these tests don't evaluate IAM. They check that the policy says what it should, not how AWS authorizes it. That these exact conditions work at runtime rests on the ladder-run and fire-scheduled-purchases roles, which already use them.
Out of scope
for_eachso the copies can't drift again. That changes resource addresses (it needsmovedblocks and a state migration on every deployment), so this PR doesn't do it. The Go guard covers the drift risk in the meantime.Closes #292
Maintainer: please mirror the issue's triage labels onto this PR:
priority/p2,severity/medium,urgency/this-quarter,impact/internal,effort/xs,type/security,triaged.🤖 Generated with Claude Code