Skip to content

test(iac/aws): boundary guard walk misses deploy-path modules; scope lambda:InvokeFunction in the boundary #511

Closed

Description

@cristim

Re-filed: the original #1827 was lost in the monorepo split; its only trace is dc476f8 ("Deferred and tracked: #1827 (guard walk misses two deploy-path modules; lambda:InvokeFunction Resource scope)").

What the guard checks

terraform/environments/aws/ci-cd-permissions/policy_guard_test.go derives, from the module tree, every IAM action the workload roles are granted (TestBoundaryCoversWorkloadServices), every aws_iam_role that must carry the permissions boundary (TestEveryModuleRoleHasPermissionsBoundary) and every attached managed-policy ARN. All three read their file list from findAwsModuleFiles, which walks terraform/modules and keeps only .tf files whose directory path contains an aws path segment.

Gap 1: the walk misses deploy-path modules

The deploy path is terraform/environments/aws/*.tf plus every module it instantiates. Modules whose directory has no aws segment are invisible to the walk, and so are the root *.tf files:

  • terraform/modules/build (module "build" in environments/aws/build.tf)
  • terraform/modules/deployment-checks (module "deployment_checks" in environments/aws/checks.tf)

Neither declares an IAM policy or role today, so nothing is currently uncovered. The walk is a path-name heuristic rather than derived from what the deploy instantiates, so the next IAM policy added to either (or to a root .tf) is not checked against the boundary.

Mutation proof: appended to terraform/modules/build/main.tf

resource "aws_iam_role_policy" "mutation" {
  role = "x"
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{ Effect = "Allow", Action = ["ecs:DeleteCluster", "lambda:DeleteFunction"], Resource = "*" }]
  })
}

go test . in ci-cd-permissions on main: ok github.com/LeanerCloud/cloud-commitments-platform/terraform/environments/aws/ci-cd-permissions 0.318s. Neither action is in the boundary, and the guard still passes.

Gap 2: lambda:InvokeFunction Resource scope

policy_boundary.tf WorkloadServiceCeiling allows lambda:InvokeFunction on Resource = "*" (any function in the account, which runs it as its execution role with an attacker-chosen payload and no iam:PassRole). The only workload role that uses it is the API Lambda's async_self_invoke policy (modules/compute/aws/lambda/main.tf, ~line 486), scoped to aws_lambda_function.main.arn, whose name is ${stack_name}-api, stack_name = "${project_name}-${environment}-<hex>", project_name default cudly. The aws_lambda_permission InvokeFunction entries are resource policies and are not gated by the boundary. The deploy role itself can only create arn:aws:lambda:*:*:function:cudly-* (policy_compute.tf), so every function this deploy creates matches. The tightest correct scope is arn:aws:lambda:*:*:function:cudly-*; the boundary comment already names it as the intended follow-up.

Needs a human apply of ci-cd-permissions after the fix merges.

Activity

  1. added 2 commits that reference this issue on Oct 8, 2026
    76a3064
    ad8f406
  2. cristim commented on Oct 8, 2026

    @cristim
    MemberAuthor

    Fixed by PR #512, squash-merged as 15a5eea (reviewed head ab6dd6f). The boundary guard now walks the full module tree, including nested local children and remote-source modules, and lambda:InvokeFunction is scoped to the Cudly functions. Merged with exact-head review, targeted local verification and green CI; the deployed boundary still needs the documented privileged bootstrap apply.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions