Skip to content

fix(override): make approved overrides unblock app repos and split FAILED from HIGH - #182

Merged
Alexanderamiri merged 3 commits into
mainfrom
fix/override-app-repos
Oct 31, 2026
Merged

Alexanderamiri merged 3 commits into
mainfrom
fix/override-app-repos

Conversation

@Alexanderamiri

Copy link
Copy Markdown
Member

Problem

  • approve-override.yml ran gh run rerun "$RUN_ID" --repo "$REPO" --failed with the platform repo's github.token. That token only covers javaBin/platform, so for an app repo the override got signed but the blocked apply never restarted. All past override runs targeted javaBin/platform, so the app path had never run. No GitHub App installed in the org has actions: write and is usable here (the javabin-platform App has administration/contents/members only), so a cross-repo re-run token can't be had without granting an App more access.
  • A HIGH or FAILED review blocks tf-apply, and docker-build and ecs-deploy wait for tf-apply. A plan-review or Bedrock outage turned into FAILED after one Lambda invoke, and the output looked the same as a risk finding.
  • No doc said how a developer asks for an override.

Change

  • approve-override.yml / scripts/override-target.sh: the repo and run now come from the plan key (plans/<org>/<repo>/<run_id>/tfplan) and are checked before anything is signed. repo and run_id are now optional inputs that must match the key if given. For javaBin/platform runs the failed jobs are still re-run automatically. For app repos the job summary, a notice annotation and the Slack message give the run URL and say to click Re-run failed jobs. The apply gate checks for override.json on every run, so that re-run is all it takes. The security properties stay the same: signing still needs the override-approval environment and the gate's OIDC workflow check.
  • scripts/invoke-plan-review.sh: retries reviews the service couldn't complete (invoke error, Lambda timeout/errorMessage, FAILED verdict), up to REVIEW_INVOKE_ATTEMPTS (default 3) with backoff. Refusals (bad OIDC token, another repo's plan) are not retried. It prints Plan review service failed … not a risk finding or Plan review found HIGH risk. Both still block the apply.
  • plan-review Lambda: takes retrying, and doesn't post FAILED to Slack while the caller will retry (HIGH always posts). The Slack text and PR markdown now say whether the service failed or the review found HIGH risk, and the alert links the blocked run.
  • scripts/override-help.sh (called from invoke-apply-gate.sh when the apply is blocked): prints and writes to the job summary what to do.
    • HIGH: ask the platform team, giving the plan key and run URL; the exact gh workflow run approve-override.yml … command; then Re-run failed jobs.
    • FAILED: Re-run all jobs, because re-running only the failed jobs re-reads the FAILED verdict; or an override.
  • Docs: new "Requesting an override" section in docs/apply-gate.md. Updated override flow, reusable-workflows.md, ci-workflow.md, lambda-functions.md and CLAUDE.md.

javabin.yml is unchanged.

Verification

  • python3 -m unittest discover -s scripts/tests: 145 OK. New test_override_flow.py runs the shell scripts with fake aws/curl/gh: retries until a verdict, retrying only on non-final attempts, FAILED vs HIGH wording, no retry on refusals, plan-key parsing and mismatch refusal, platform re-run vs app instructions.
  • python3 -m unittest discover -s terraform/lambda-src/tests: 222 OK, including new plan-review notification tests.
  • shellcheck -s sh on the new and changed scripts (only a pre-existing SC2129 style note in invoke-apply-gate.sh), and actionlint on approve-override.yml.
  • No Terraform changes. The plan-review Lambda code ships through platform CI.

Notes

  • Until the platform apply deploys the new plan-review Lambda, it ignores retrying, so an outage can post up to 3 FAILED alerts per run.
  • Not fixed (an existing gap, now documented): Re-run all jobs re-plans under the same plan key (the key holds the run ID, not the attempt), so an override.json signed for an earlier attempt is still accepted for the new plan. Binding the HMAC to the review (for example a review ID in risk.json) would close it.
  • If you want fully automatic re-runs for app repos, give a platform GitHub App actions: write and mint a token in approve-override. That is an org admin decision.

🤖 Generated with Claude Code

…ILED from HIGH

approve-override.yml re-ran the blocked run with the platform repo's
github.token, which cannot touch app repos, so an approved override never
restarted an app's apply. The repo and run now come from the plan key
(validated before signing); platform runs are still re-run automatically and
app runs get the exact "Re-run failed jobs" step in the job summary and Slack.

invoke-plan-review.sh retries reviews the service could not complete, and the
logs, PR comment, Slack alert and blocked Terraform Apply say whether the
review service failed or the review found HIGH risk, with how to request an
override. docs/apply-gate.md documents the request path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Alexanderamiri
Alexanderamiri requested a review from a team as a code owner October 31, 2026 12:06
@github-actions

Copy link
Copy Markdown

Terraform Plan

🚧 Changes detected — Plan: 0 to add, 1 to change, 0 to destroy.

Plan output
Acquiring state lock. This may take a few moments...
aws_ssm_parameter.atlassian_api_key: Refreshing state... [id=/javabin/platform/atlassian-api-key]
aws_ssm_parameter.atlassian_org_id: Refreshing state... [id=/javabin/platform/atlassian-org-id]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # module.lambdas.aws_lambda_function.plan_review will be updated in-place
  ~ resource "aws_lambda_function" "plan_review" {
        id                             = "javabin-plan-review"
      ~ last_modified                  = "2026-10-29T21:09:02.000+0000" -> (known after apply)
      ~ source_code_hash               = "pssbwXoZg3c98wMsTKW//8ZpVy84IiWLjExZQClIFvY=" -> "283s5UwD53emOHAhkWvLn8R7p5Nj+K2zxOA5zfgIu0s="
        tags                           = {}
        # (21 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: tfplan

To perform exactly these actions, run the following command to apply:
    terraform apply "tfplan"

LLM Review

Risk: 🟢 LOW

Plan updates one existing Lambda function (javabin-plan-review) in place with a new source code hash; no resources are created or destroyed.

  • ✅ [routine] module.lambdas.aws_lambda_function.plan_review is updated in place. Only source_code_hash changes (new deployment package), and last_modified is computed by AWS on apply. This is a routine code deployment.
  • 💥 [destruction] Plan summary is 0 to add, 1 to change, 0 to destroy. No resources are replaced or deleted, so there is no data-loss or downtime risk from the plan itself.
  • 🔒 [security] No IAM role, policy, security group, or public-access changes appear in the plan. The function's execution role and network configuration are unchanged (the hidden attributes are not changing).
  • 💰 [cost] No new billable resources are introduced. Only the code package of an existing Lambda changes, so no cost impact is expected beyond normal invocation charges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Alexanderamiri

Copy link
Copy Markdown
Member Author

Merged origin/main (#184, #178, #180, #187, #186, #185) into this branch with a merge commit.

Conflicts:

Checks after the merge: scripts/tests 196 OK, lambda-src/tests 252 OK, terraform fmt -check -recursive clean, terraform validate (terraform/platform) OK. No generated files were affected.

Not changed: some docs still say "board member" for override approval (ci-workflow.md:48, reusable-workflows.md:216, README.md:16, apply-gate.md:202, approve-override.yml:4), but this PR's new text says "platform team". That was already the case before the merge.

@github-actions

Copy link
Copy Markdown

Terraform Plan

🚧 Changes detected — Plan: 3 to add, 12 to change, 0 to destroy.

Plan output
Acquiring state lock. This may take a few moments...
aws_ssm_parameter.atlassian_api_key: Refreshing state... [id=/javabin/platform/atlassian-api-key]
aws_ssm_parameter.atlassian_org_id: Refreshing state... [id=/javabin/platform/atlassian-org-id]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create
  ~ update in-place

Terraform will perform the following actions:

  # module.iam.aws_iam_role.ci_infra will be updated in-place
  ~ resource "aws_iam_role" "ci_infra" {
      ~ assume_role_policy    = jsonencode(
          ~ {
              ~ Statement = [
                  ~ {
                      ~ Condition = {
                          ~ StringLike   = {
                              ~ "token.actions.githubusercontent.com:sub"              = [
                                    # (1 unchanged element hidden)
                                    "repo:javaBin/platform:environment:production",
                                  + "repo:javaBin@95047/platform@1175354559:ref:refs/heads/main",
                                  + "repo:javaBin@95047/platform@1175354559:environment:production",
                                ]
                                # (1 unchanged attribute hidden)
                            }
                            # (1 unchanged attribute hidden)
                        }
                        # (3 unchanged attributes hidden)
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        id                    = "javabin-ci-infra"
        name                  = "javabin-ci-infra"
        tags                  = {
            "Name" = "javabin-ci-infra"
        }
        # (9 unchanged attributes hidden)

        # (2 unchanged blocks hidden)
    }

  # module.iam.aws_iam_role.ci_infra_plan will be updated in-place
  ~ resource "aws_iam_role" "ci_infra_plan" {
      ~ assume_role_policy    = jsonencode(
          ~ {
              ~ Statement = [
                  ~ {
                      ~ Condition = {
                          ~ StringLike   = {
                              ~ "token.actions.githubusercontent.com:sub" = "repo:javaBin/platform:*" -> [
                                  + "repo:javaBin/platform:*",
                                  + "repo:javaBin@95047/platform@1175354559:*",
                                ]
                            }
                            # (1 unchanged attribute hidden)
                        }
                        # (3 unchanged attributes hidden)
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        id                    = "javabin-ci-infra-plan"
        name                  = "javabin-ci-infra-plan"
        tags                  = {
            "Name" = "javabin-ci-infra-plan"
        }
        # (9 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.iam.aws_iam_role.ci_registry will be updated in-place
  ~ resource "aws_iam_role" "ci_registry" {
      ~ assume_role_policy    = jsonencode(
          ~ {
              ~ Statement = [
                  ~ {
                      ~ Condition = {
                          ~ StringLike   = {
                              ~ "token.actions.githubusercontent.com:sub" = "repo:javaBin/registry:ref:refs/heads/main" -> [
                                  + "repo:javaBin/registry:ref:refs/heads/main",
                                  + "repo:javaBin@95047/registry@1175355770:ref:refs/heads/main",
                                ]
                            }
                            # (1 unchanged attribute hidden)
                        }
                        # (3 unchanged attributes hidden)
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        id                    = "javabin-ci-registry"
        name                  = "javabin-ci-registry"
        tags                  = {
            "Name" = "javabin-ci-registry"
        }
        # (9 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.iam.aws_iam_role_policy.ci_deploy_ecs["cupcake"] will be updated in-place
  ~ resource "aws_iam_role_policy" "ci_deploy_ecs" {
        id     = "javabin-ci-deploy-cupcake:ecs-deploy"
        name   = "ecs-deploy"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                  ~ {
                      ~ Action    = [
                            # (1 unchanged element hidden)
                            "ecs:DescribeServices",
                          + "ecs:ListTagsForResource",
                          + "ecs:DescribeTasks",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  + {
                      + Action    = [
                          + "ecs:ListTasks",
                        ]
                      + Condition = {
                          + ArnEquals = {
                              + "ecs:cluster" = "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "*"
                      + Sid       = "EcsListTasks"
                    },
                  + {
                      + Action    = [
                          + "ecs:TagResource",
                        ]
                      + Condition = {
                          + StringEquals = {
                              + "ecs:CreateAction" = "RegisterTaskDefinition"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "*"
                      + Sid       = "EcsTagOnRegister"
                    },
                    {
                        Action   = [
                            "ecs:DescribeTaskDefinition",
                            "ecs:RegisterTaskDefinition",
                            "ecs:DeregisterTaskDefinition",
                        ]
                        Effect   = "Allow"
                        Resource = "*"
                        Sid      = "EcsTaskDefinitionManagement"
                    },
                    # (1 unchanged element hidden)
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.iam.aws_iam_role_policy.ci_deploy_ecs["javazoneweb"] will be updated in-place
  ~ resource "aws_iam_role_policy" "ci_deploy_ecs" {
        id     = "javabin-ci-deploy-javazoneweb:ecs-deploy"
        name   = "ecs-deploy"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                  ~ {
                      ~ Action    = [
                            # (1 unchanged element hidden)
                            "ecs:DescribeServices",
                          + "ecs:ListTagsForResource",
                          + "ecs:DescribeTasks",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  + {
                      + Action    = [
                          + "ecs:ListTasks",
                        ]
                      + Condition = {
                          + ArnEquals = {
                              + "ecs:cluster" = "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "*"
                      + Sid       = "EcsListTasks"
                    },
                  + {
                      + Action    = [
                          + "ecs:TagResource",
                        ]
                      + Condition = {
                          + StringEquals = {
                              + "ecs:CreateAction" = "RegisterTaskDefinition"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "*"
                      + Sid       = "EcsTagOnRegister"
                    },
                    {
                        Action   = [
                            "ecs:DescribeTaskDefinition",
                            "ecs:RegisterTaskDefinition",
                            "ecs:DeregisterTaskDefinition",
                        ]
                        Effect   = "Allow"
                        Resource = "*"
                        Sid      = "EcsTaskDefinitionManagement"
                    },
                    # (1 unchanged element hidden)
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.iam.aws_iam_role_policy.ci_deploy_ecs["jzwatch"] will be updated in-place
  ~ resource "aws_iam_role_policy" "ci_deploy_ecs" {
        id     = "javabin-ci-deploy-jzwatch:ecs-deploy"
        name   = "ecs-deploy"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                  ~ {
                      ~ Action    = [
                            # (1 unchanged element hidden)
                            "ecs:DescribeServices",
                          + "ecs:ListTagsForResource",
                          + "ecs:DescribeTasks",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  + {
                      + Action    = [
                          + "ecs:ListTasks",
                        ]
                      + Condition = {
                          + ArnEquals = {
                              + "ecs:cluster" = "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "*"
                      + Sid       = "EcsListTasks"
                    },
                  + {
                      + Action    = [
                          + "ecs:TagResource",
                        ]
                      + Condition = {
                          + StringEquals = {
                              + "ecs:CreateAction" = "RegisterTaskDefinition"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "*"
                      + Sid       = "EcsTagOnRegister"
                    },
                    {
                        Action   = [
                            "ecs:DescribeTaskDefinition",
                            "ecs:RegisterTaskDefinition",
                            "ecs:DeregisterTaskDefinition",
                        ]
                        Effect   = "Allow"
                        Resource = "*"
                        Sid      = "EcsTaskDefinitionManagement"
                    },
                    # (1 unchanged element hidden)
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.iam.aws_iam_role_policy.ci_deploy_ecs["testteam"] will be updated in-place
  ~ resource "aws_iam_role_policy" "ci_deploy_ecs" {
        id     = "javabin-ci-deploy-testteam:ecs-deploy"
        name   = "ecs-deploy"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                  ~ {
                      ~ Action    = [
                            # (1 unchanged element hidden)
                            "ecs:DescribeServices",
                          + "ecs:ListTagsForResource",
                          + "ecs:DescribeTasks",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  + {
                      + Action    = [
                          + "ecs:ListTasks",
                        ]
                      + Condition = {
                          + ArnEquals = {
                              + "ecs:cluster" = "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "*"
                      + Sid       = "EcsListTasks"
                    },
                  + {
                      + Action    = [
                          + "ecs:TagResource",
                        ]
                      + Condition = {
                          + StringEquals = {
                              + "ecs:CreateAction" = "RegisterTaskDefinition"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "*"
                      + Sid       = "EcsTagOnRegister"
                    },
                    {
                        Action   = [
                            "ecs:DescribeTaskDefinition",
                            "ecs:RegisterTaskDefinition",
                            "ecs:DeregisterTaskDefinition",
                        ]
                        Effect   = "Allow"
                        Resource = "*"
                        Sid      = "EcsTaskDefinitionManagement"
                    },
                    # (1 unchanged element hidden)
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.lambdas.aws_cloudwatch_event_rule.team_provisioner_access_schedule will be created
  + resource "aws_cloudwatch_event_rule" "team_provisioner_access_schedule" {
      + arn                 = (known after apply)
      + description         = "Retry Identity Center group memberships for members Google SCIM has since provisioned"
      + event_bus_name      = "default"
      + force_destroy       = false
      + id                  = (known after apply)
      + name                = "javabin-team-provisioner-access-schedule"
      + name_prefix         = (known after apply)
      + schedule_expression = "rate(3 hours)"
      + tags_all            = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_event_target.team_provisioner_access will be created
  + resource "aws_cloudwatch_event_target" "team_provisioner_access" {
      + arn            = "arn:aws:lambda:eu-central-1:553637109631:function:javabin-team-provisioner"
      + event_bus_name = "default"
      + force_destroy  = false
      + id             = (known after apply)
      + input          = jsonencode(
            {
              + action = "sync_access_members"
            }
        )
      + rule           = "javabin-team-provisioner-access-schedule"
      + target_id      = "invoke-team-provisioner-access"
    }

  # module.lambdas.aws_iam_role_policy.budget_enforcer will be updated in-place
  ~ resource "aws_iam_role_policy" "budget_enforcer" {
        id     = "javabin-budget-enforcer:javabin-budget-enforcer"
        name   = "javabin-budget-enforcer"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                    # (2 unchanged elements hidden)
                    {
                        Action   = "ecs:ListServices"
                        Effect   = "Allow"
                        Resource = "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform"
                        Sid      = "ECSListServicesUnconstrained"
                    },
                  ~ {
                      ~ Action   = "ecs:UpdateService" -> [
                          + "ecs:UpdateService",
                          + "ecs:TagResource",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.lambdas.aws_iam_role_policy.team_provisioner will be updated in-place
  ~ resource "aws_iam_role_policy" "team_provisioner" {
        id     = "javabin-team-provisioner:javabin-team-provisioner"
        name   = "javabin-team-provisioner"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                    # (4 unchanged elements hidden)
                    {
                        Action   = [
                            "identitystore:CreateGroup",
                            "identitystore:DescribeGroup",
                            "identitystore:DescribeUser",
                            "identitystore:ListGroups",
                            "identitystore:CreateGroupMembership",
                            "identitystore:ListGroupMemberships",
                            "identitystore:DeleteGroupMembership",
                            "identitystore:ListUsers",
                        ]
                        Effect   = "Allow"
                        Resource = "*"
                        Sid      = "IdentityStoreSync"
                    },
                  ~ {
                      ~ Action   = [
                            # (1 unchanged element hidden)
                            "sso:DescribeAccountAssignmentCreationStatus",
                          + "sso:ListAccountAssignments",
                            "sso:ListPermissionSets",
                            # (7 unchanged elements hidden)
                        ]
                        # (3 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "iam:GetPolicy",
                            "iam:GetPolicyVersion",
                            "iam:ListPolicyTags",
                        ]
                        Effect   = "Allow"
                        Resource = "arn:aws:iam::553637109631:policy/javabin-team-*-guardrails"
                        Sid      = "ReadTeamGuardrails"
                    },
                    # (1 unchanged element hidden)
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.lambdas.aws_lambda_function.budget_enforcer will be updated in-place
  ~ resource "aws_lambda_function" "budget_enforcer" {
        id                             = "javabin-budget-enforcer"
      ~ last_modified                  = "2026-03-26T20:01:54.976+0000" -> (known after apply)
      ~ source_code_hash               = "i3E4tI5mT0qCS601izSv52QOgwAndKfcZ3JkLzxTNis=" -> "lM6wzIndpyMp1ZmU5nft0SteLorI7ppyEyOYIL/2hjU="
        tags                           = {}
        # (21 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_function.plan_review will be updated in-place
  ~ resource "aws_lambda_function" "plan_review" {
        id                             = "javabin-plan-review"
      ~ last_modified                  = "2026-10-29T21:09:02.000+0000" -> (known after apply)
      ~ source_code_hash               = "pssbwXoZg3c98wMsTKW//8ZpVy84IiWLjExZQClIFvY=" -> "283s5UwD53emOHAhkWvLn8R7p5Nj+K2zxOA5zfgIu0s="
        tags                           = {}
        # (21 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_function.team_provisioner will be updated in-place
  ~ resource "aws_lambda_function" "team_provisioner" {
        id                             = "javabin-team-provisioner"
      ~ last_modified                  = "2026-10-31T13:03:32.000+0000" -> (known after apply)
      ~ source_code_hash               = "3I4td1T2ENiRLUUEjeBLPinRaiXc/sZrwOT+Ai83lOY=" -> "4Gcog5O29Hs6CEnMQPK5WWLQ1aShJnqsS3zVkqRjVlA="
        tags                           = {}
        # (21 unchanged attributes hidden)

      ~ environment {
          ~ variables = {
              + "AWS_ACCESS_GROUP"             = "javabin-developer@java.no"
                # (19 unchanged elements hidden)
            }
        }

        # (3 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_permission.team_provisioner_access_schedule will be created
  + resource "aws_lambda_permission" "team_provisioner_access_schedule" {
      + action              = "lambda:InvokeFunction"
      + function_name       = "javabin-team-provisioner"
      + id                  = (known after apply)
      + principal           = "events.amazonaws.com"
      + source_arn          = (known after apply)
      + statement_id        = "AllowEventBridgeAccessSchedule"
      + statement_id_prefix = (known after apply)
    }

Plan: 3 to add, 12 to change, 0 to destroy.

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: tfplan

To perform exactly these actions, run the following command to apply:
    terraform apply "tfplan"

LLM Review

Risk: 🔴 HIGH

Plan adds 3 resources and updates 12 in place with 0 destroys; the main concern is that CI OIDC trust policies for three IAM roles are widened to accept subjects from a different GitHub repository path (javaBin@95047/...), and the production deploy role's ECS permissions are expanded.

  • 🔒 [security] module.iam.aws_iam_role.ci_infra, ci_infra_plan and ci_registry: the GitHub OIDC trust policy (StringLike on token.actions.githubusercontent.com:sub) gains new subjects 'repo:javaBin@95047/platform@1175354559:*', 'repo:javaBin@95047/platform@1175354559:environment:production', and 'repo:javaBin@95047/registry@1175355770:ref:refs/heads/main'. The 'javaBin@/@' format is a repository-renamed/recreated subject that could let a different (possibly attacker-controlled or re-registered) repository assume these roles. ci_infra is the production deploy role and ci_infra_plan has a wildcard on the new subject. This must be verified before apply; if not intentional, it is a privilege escalation path into CI roles.
  • 🔒 [security] ci_infra_plan trust is changed from 'repo:javaBin/platform:' to also include 'repo:javaBin@95047/platform@1175354559:', a wildcard that accepts any ref/environment of the new subject.
  • 🔒 [security] module.iam.aws_iam_role_policy.ci_deploy_ecs (cupcake, javazoneweb, jzwatch, testteam): adds ecs:ListTagsForResource, ecs:DescribeTasks, ecs:ListTasks (scoped to the javabin-platform cluster), ecs:TagResource (only on RegisterTaskDefinition), and a new statement with ecs:RegisterTaskDefinition/DeregisterTaskDefinition/DescribeTaskDefinition on Resource ''. Those statements are Allow, but the Resource is '' rather than scoped to the team's services; review whether this broadens what each team deploy role can do across the account.
  • 🔒 [security] module.lambdas.aws_iam_role_policy.team_provisioner: adds sso:ListAccountAssignments (Allow, from the diff context). Also the Lambda gains environment variable AWS_ACCESS_GROUP=javabin-developer@java.no. Low-moderate concern, but it expands Identity Center read access and is driven by a new scheduled job.
  • ✅ [routine] New EventBridge rule javabin-team-provisioner-access-schedule (rate 3 hours) with target to the team-provisioner Lambda (action sync_access_members) and matching lambda permission. Adds no direct cost of note and no destruction. Three-hour schedule invocations are low cost.
  • ✅ [routine] budget-enforcer Lambda and plan-review Lambda code updates (source hash changes), budget-enforcer role gains ecs:TagResource on ECS update; team-provisioner Lambda code update. Standard deploys; no destroy or replacement shown. Budget-enforcer ECS tagging is a minor widening.

@github-actions

Copy link
Copy Markdown

Terraform Plan

🚧 Changes detected — Plan: 6 to add, 12 to change, 0 to destroy.

Plan output
Acquiring state lock. This may take a few moments...
aws_ssm_parameter.atlassian_org_id: Refreshing state... [id=/javabin/platform/atlassian-org-id]
aws_ssm_parameter.atlassian_api_key: Refreshing state... [id=/javabin/platform/atlassian-api-key]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create
  ~ update in-place

Terraform will perform the following actions:

  # module.iam.aws_iam_role.ci_infra will be updated in-place
  ~ resource "aws_iam_role" "ci_infra" {
      ~ assume_role_policy    = jsonencode(
          ~ {
              ~ Statement = [
                  ~ {
                      ~ Condition = {
                          ~ StringLike   = {
                              ~ "token.actions.githubusercontent.com:sub"              = [
                                    # (1 unchanged element hidden)
                                    "repo:javaBin/platform:environment:production",
                                  + "repo:javaBin@95047/platform@1175354559:ref:refs/heads/main",
                                  + "repo:javaBin@95047/platform@1175354559:environment:production",
                                ]
                                # (1 unchanged attribute hidden)
                            }
                            # (1 unchanged attribute hidden)
                        }
                        # (3 unchanged attributes hidden)
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        id                    = "javabin-ci-infra"
        name                  = "javabin-ci-infra"
        tags                  = {
            "Name" = "javabin-ci-infra"
        }
        # (9 unchanged attributes hidden)

        # (2 unchanged blocks hidden)
    }

  # module.iam.aws_iam_role.ci_infra_plan will be updated in-place
  ~ resource "aws_iam_role" "ci_infra_plan" {
      ~ assume_role_policy    = jsonencode(
          ~ {
              ~ Statement = [
                  ~ {
                      ~ Condition = {
                          ~ StringLike   = {
                              ~ "token.actions.githubusercontent.com:sub" = "repo:javaBin/platform:*" -> [
                                  + "repo:javaBin/platform:*",
                                  + "repo:javaBin@95047/platform@1175354559:*",
                                ]
                            }
                            # (1 unchanged attribute hidden)
                        }
                        # (3 unchanged attributes hidden)
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        id                    = "javabin-ci-infra-plan"
        name                  = "javabin-ci-infra-plan"
        tags                  = {
            "Name" = "javabin-ci-infra-plan"
        }
        # (9 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.iam.aws_iam_role.ci_registry will be updated in-place
  ~ resource "aws_iam_role" "ci_registry" {
      ~ assume_role_policy    = jsonencode(
          ~ {
              ~ Statement = [
                  ~ {
                      ~ Condition = {
                          ~ StringLike   = {
                              ~ "token.actions.githubusercontent.com:sub" = "repo:javaBin/registry:ref:refs/heads/main" -> [
                                  + "repo:javaBin/registry:ref:refs/heads/main",
                                  + "repo:javaBin@95047/registry@1175355770:ref:refs/heads/main",
                                ]
                            }
                            # (1 unchanged attribute hidden)
                        }
                        # (3 unchanged attributes hidden)
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        id                    = "javabin-ci-registry"
        name                  = "javabin-ci-registry"
        tags                  = {
            "Name" = "javabin-ci-registry"
        }
        # (9 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.iam.aws_iam_role_policy.ci_deploy_ecs["cupcake"] will be updated in-place
  ~ resource "aws_iam_role_policy" "ci_deploy_ecs" {
        id     = "javabin-ci-deploy-cupcake:ecs-deploy"
        name   = "ecs-deploy"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                  ~ {
                      ~ Action    = [
                            # (1 unchanged element hidden)
                            "ecs:DescribeServices",
                          + "ecs:ListTagsForResource",
                          + "ecs:DescribeTasks",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  + {
                      + Action    = [
                          + "ecs:ListTasks",
                        ]
                      + Condition = {
                          + ArnEquals = {
                              + "ecs:cluster" = "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "*"
                      + Sid       = "EcsListTasks"
                    },
                  + {
                      + Action    = [
                          + "ecs:TagResource",
                        ]
                      + Condition = {
                          + StringEquals = {
                              + "ecs:CreateAction" = "RegisterTaskDefinition"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "*"
                      + Sid       = "EcsTagOnRegister"
                    },
                    {
                        Action   = [
                            "ecs:DescribeTaskDefinition",
                            "ecs:RegisterTaskDefinition",
                            "ecs:DeregisterTaskDefinition",
                        ]
                        Effect   = "Allow"
                        Resource = "*"
                        Sid      = "EcsTaskDefinitionManagement"
                    },
                    # (1 unchanged element hidden)
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.iam.aws_iam_role_policy.ci_deploy_ecs["javazoneweb"] will be updated in-place
  ~ resource "aws_iam_role_policy" "ci_deploy_ecs" {
        id     = "javabin-ci-deploy-javazoneweb:ecs-deploy"
        name   = "ecs-deploy"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                  ~ {
                      ~ Action    = [
                            # (1 unchanged element hidden)
                            "ecs:DescribeServices",
                          + "ecs:ListTagsForResource",
                          + "ecs:DescribeTasks",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  + {
                      + Action    = [
                          + "ecs:ListTasks",
                        ]
                      + Condition = {
                          + ArnEquals = {
                              + "ecs:cluster" = "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "*"
                      + Sid       = "EcsListTasks"
                    },
                  + {
                      + Action    = [
                          + "ecs:TagResource",
                        ]
                      + Condition = {
                          + StringEquals = {
                              + "ecs:CreateAction" = "RegisterTaskDefinition"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "*"
                      + Sid       = "EcsTagOnRegister"
                    },
                    {
                        Action   = [
                            "ecs:DescribeTaskDefinition",
                            "ecs:RegisterTaskDefinition",
                            "ecs:DeregisterTaskDefinition",
                        ]
                        Effect   = "Allow"
                        Resource = "*"
                        Sid      = "EcsTaskDefinitionManagement"
                    },
                    # (1 unchanged element hidden)
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.iam.aws_iam_role_policy.ci_deploy_ecs["jzwatch"] will be updated in-place
  ~ resource "aws_iam_role_policy" "ci_deploy_ecs" {
        id     = "javabin-ci-deploy-jzwatch:ecs-deploy"
        name   = "ecs-deploy"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                  ~ {
                      ~ Action    = [
                            # (1 unchanged element hidden)
                            "ecs:DescribeServices",
                          + "ecs:ListTagsForResource",
                          + "ecs:DescribeTasks",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  + {
                      + Action    = [
                          + "ecs:ListTasks",
                        ]
                      + Condition = {
                          + ArnEquals = {
                              + "ecs:cluster" = "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "*"
                      + Sid       = "EcsListTasks"
                    },
                  + {
                      + Action    = [
                          + "ecs:TagResource",
                        ]
                      + Condition = {
                          + StringEquals = {
                              + "ecs:CreateAction" = "RegisterTaskDefinition"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "*"
                      + Sid       = "EcsTagOnRegister"
                    },
                    {
                        Action   = [
                            "ecs:DescribeTaskDefinition",
                            "ecs:RegisterTaskDefinition",
                            "ecs:DeregisterTaskDefinition",
                        ]
                        Effect   = "Allow"
                        Resource = "*"
                        Sid      = "EcsTaskDefinitionManagement"
                    },
                    # (1 unchanged element hidden)
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.iam.aws_iam_role_policy.ci_deploy_ecs["testteam"] will be updated in-place
  ~ resource "aws_iam_role_policy" "ci_deploy_ecs" {
        id     = "javabin-ci-deploy-testteam:ecs-deploy"
        name   = "ecs-deploy"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                  ~ {
                      ~ Action    = [
                            # (1 unchanged element hidden)
                            "ecs:DescribeServices",
                          + "ecs:ListTagsForResource",
                          + "ecs:DescribeTasks",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  + {
                      + Action    = [
                          + "ecs:ListTasks",
                        ]
                      + Condition = {
                          + ArnEquals = {
                              + "ecs:cluster" = "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "*"
                      + Sid       = "EcsListTasks"
                    },
                  + {
                      + Action    = [
                          + "ecs:TagResource",
                        ]
                      + Condition = {
                          + StringEquals = {
                              + "ecs:CreateAction" = "RegisterTaskDefinition"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "*"
                      + Sid       = "EcsTagOnRegister"
                    },
                    {
                        Action   = [
                            "ecs:DescribeTaskDefinition",
                            "ecs:RegisterTaskDefinition",
                            "ecs:DeregisterTaskDefinition",
                        ]
                        Effect   = "Allow"
                        Resource = "*"
                        Sid      = "EcsTaskDefinitionManagement"
                    },
                    # (1 unchanged element hidden)
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.lambdas.aws_cloudwatch_event_rule.budget_enforcer_reconcile will be created
  + resource "aws_cloudwatch_event_rule" "budget_enforcer_reconcile" {
      + arn                 = (known after apply)
      + description         = "Reconcile team budget suspensions every 6 hours"
      + event_bus_name      = "default"
      + force_destroy       = false
      + id                  = (known after apply)
      + name                = "javabin-budget-enforcer-reconcile"
      + name_prefix         = (known after apply)
      + schedule_expression = "rate(6 hours)"
      + tags_all            = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_event_rule.team_provisioner_access_schedule will be created
  + resource "aws_cloudwatch_event_rule" "team_provisioner_access_schedule" {
      + arn                 = (known after apply)
      + description         = "Retry Identity Center group memberships for members Google SCIM has since provisioned"
      + event_bus_name      = "default"
      + force_destroy       = false
      + id                  = (known after apply)
      + name                = "javabin-team-provisioner-access-schedule"
      + name_prefix         = (known after apply)
      + schedule_expression = "rate(3 hours)"
      + tags_all            = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_event_target.budget_enforcer_reconcile will be created
  + resource "aws_cloudwatch_event_target" "budget_enforcer_reconcile" {
      + arn            = "arn:aws:lambda:eu-central-1:553637109631:function:javabin-budget-enforcer"
      + event_bus_name = "default"
      + force_destroy  = false
      + id             = (known after apply)
      + input          = jsonencode(
            {
              + action = "reconcile"
            }
        )
      + rule           = "javabin-budget-enforcer-reconcile"
      + target_id      = "reconcile-budget-enforcer"
    }

  # module.lambdas.aws_cloudwatch_event_target.team_provisioner_access will be created
  + resource "aws_cloudwatch_event_target" "team_provisioner_access" {
      + arn            = "arn:aws:lambda:eu-central-1:553637109631:function:javabin-team-provisioner"
      + event_bus_name = "default"
      + force_destroy  = false
      + id             = (known after apply)
      + input          = jsonencode(
            {
              + action = "sync_access_members"
            }
        )
      + rule           = "javabin-team-provisioner-access-schedule"
      + target_id      = "invoke-team-provisioner-access"
    }

  # module.lambdas.aws_iam_role_policy.budget_enforcer will be updated in-place
  ~ resource "aws_iam_role_policy" "budget_enforcer" {
        id     = "javabin-budget-enforcer:javabin-budget-enforcer"
        name   = "javabin-budget-enforcer"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                    # (2 unchanged elements hidden)
                    {
                        Action   = "ecs:ListServices"
                        Effect   = "Allow"
                        Resource = "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform"
                        Sid      = "ECSListServicesUnconstrained"
                    },
                  ~ {
                      ~ Action   = "ecs:UpdateService" -> [
                          + "ecs:UpdateService",
                          + "ecs:TagResource",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                  + {
                      + Action   = [
                          + "ssm:GetParameter",
                          + "ssm:GetParametersByPath",
                          + "ssm:PutParameter",
                          + "ssm:DeleteParameter",
                        ]
                      + Effect   = "Allow"
                      + Resource = [
                          + "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/budget-suspensions",
                          + "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/budget-suspensions/*",
                        ]
                      + Sid      = "SuspensionMarkers"
                    },
                  + {
                      + Action   = "rds:DescribeDBInstances"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:rds:eu-central-1:553637109631:db:*"
                      + Sid      = "RDSDescribe"
                    },
                  + {
                      + Action    = [
                          + "rds:StopDBInstance",
                          + "rds:StartDBInstance",
                        ]
                      + Condition = {
                          + Null = {
                              + "aws:ResourceTag/team" = "false"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "arn:aws:rds:eu-central-1:553637109631:db:*"
                      + Sid       = "RDSStopStart"
                    },
                  + {
                      + Action   = "budgets:ViewBudget"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:budgets::553637109631:budget/javabin-team-*"
                      + Sid      = "BudgetsRead"
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.lambdas.aws_iam_role_policy.team_provisioner will be updated in-place
  ~ resource "aws_iam_role_policy" "team_provisioner" {
        id     = "javabin-team-provisioner:javabin-team-provisioner"
        name   = "javabin-team-provisioner"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                    # (4 unchanged elements hidden)
                    {
                        Action   = [
                            "identitystore:CreateGroup",
                            "identitystore:DescribeGroup",
                            "identitystore:DescribeUser",
                            "identitystore:ListGroups",
                            "identitystore:CreateGroupMembership",
                            "identitystore:ListGroupMemberships",
                            "identitystore:DeleteGroupMembership",
                            "identitystore:ListUsers",
                        ]
                        Effect   = "Allow"
                        Resource = "*"
                        Sid      = "IdentityStoreSync"
                    },
                  ~ {
                      ~ Action   = [
                            # (1 unchanged element hidden)
                            "sso:DescribeAccountAssignmentCreationStatus",
                          + "sso:ListAccountAssignments",
                            "sso:ListPermissionSets",
                            # (2 unchanged elements hidden)
                            "sso:PutInlinePolicyToPermissionSet",
                          - "sso:AttachCustomerManagedPolicyReferenceToPermissionSet",
                            "sso:DetachCustomerManagedPolicyReferenceFromPermissionSet",
                          + "sso:AttachManagedPolicyToPermissionSet",
                          + "sso:PutPermissionsBoundaryToPermissionSet",
                            "sso:ProvisionPermissionSet",
                            # (1 unchanged element hidden)
                        ]
                        # (3 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Resource = "arn:aws:iam::553637109631:policy/javabin-team-*-guardrails" -> [
                          + "arn:aws:iam::553637109631:policy/javabin-team-*-guardrails",
                          + "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "iam:GetSAMLProvider",
                            "iam:GetRole",
                            "iam:CreateRole",
                            "iam:AttachRolePolicy",
                            "iam:PutRolePolicy",
                            "iam:UpdateRole",
                            "iam:UpdateRoleDescription",
                            "iam:ListAttachedRolePolicies",
                            "iam:ListRolePolicies",
                            "iam:DeleteRole",
                            "iam:DeleteRolePolicy",
                            "iam:DetachRolePolicy",
                        ]
                        Effect   = "Allow"
                        Resource = [
                            "arn:aws:iam::553637109631:role/aws-reserved/sso.amazonaws.com/*",
                            "arn:aws:iam::553637109631:saml-provider/AWSSSO_*",
                        ]
                        Sid      = "IAMForSSOAssignment"
                    },
                  + {
                      + Action    = "iam:PutRolePermissionsBoundary"
                      + Condition = {
                          + ArnLike = {
                              + "iam:PermissionsBoundary" = "arn:aws:iam::553637109631:policy/javabin-team-*-guardrails"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "arn:aws:iam::553637109631:role/aws-reserved/sso.amazonaws.com/*"
                      + Sid       = "SetTeamGuardrailsBoundary"
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.lambdas.aws_lambda_function.budget_enforcer will be updated in-place
  ~ resource "aws_lambda_function" "budget_enforcer" {
        id                             = "javabin-budget-enforcer"
      ~ last_modified                  = "2026-03-26T20:01:54.976+0000" -> (known after apply)
      ~ source_code_hash               = "i3E4tI5mT0qCS601izSv52QOgwAndKfcZ3JkLzxTNis=" -> "di7/qFyHX8tYBT6OXRTbwov0mZQqBuJuNY4jdUtzM2E="
        tags                           = {}
        # (21 unchanged attributes hidden)

      ~ environment {
          ~ variables = {
              + "AWS_ACCOUNT_ID"     = "553637109631"
              + "SUSPENSION_PATH"    = "/javabin/budget-suspensions"
                # (3 unchanged elements hidden)
            }
        }

        # (3 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_function.plan_review will be updated in-place
  ~ resource "aws_lambda_function" "plan_review" {
        id                             = "javabin-plan-review"
      ~ last_modified                  = "2026-10-29T21:09:02.000+0000" -> (known after apply)
      ~ source_code_hash               = "pssbwXoZg3c98wMsTKW//8ZpVy84IiWLjExZQClIFvY=" -> "283s5UwD53emOHAhkWvLn8R7p5Nj+K2zxOA5zfgIu0s="
        tags                           = {}
        # (21 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_function.team_provisioner will be updated in-place
  ~ resource "aws_lambda_function" "team_provisioner" {
        id                             = "javabin-team-provisioner"
      ~ last_modified                  = "2026-10-31T13:03:32.000+0000" -> (known after apply)
      ~ source_code_hash               = "3I4td1T2ENiRLUUEjeBLPinRaiXc/sZrwOT+Ai83lOY=" -> "tpW4OZM/7/EBE9sKi6c6DAazYfsyHZV2rG/JJUYTTng="
        tags                           = {}
        # (21 unchanged attributes hidden)

      ~ environment {
          ~ variables = {
              + "AWS_ACCESS_GROUP"             = "javabin-developer@java.no"
                # (19 unchanged elements hidden)
            }
        }

        # (3 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_permission.budget_enforcer_reconcile will be created
  + resource "aws_lambda_permission" "budget_enforcer_reconcile" {
      + action              = "lambda:InvokeFunction"
      + function_name       = "javabin-budget-enforcer"
      + id                  = (known after apply)
      + principal           = "events.amazonaws.com"
      + source_arn          = (known after apply)
      + statement_id        = "AllowEventBridgeReconcile"
      + statement_id_prefix = (known after apply)
    }

  # module.lambdas.aws_lambda_permission.team_provisioner_access_schedule will be created
  + resource "aws_lambda_permission" "team_provisioner_access_schedule" {
      + action              = "lambda:InvokeFunction"
      + function_name       = "javabin-team-provisioner"
      + id                  = (known after apply)
      + principal           = "events.amazonaws.com"
      + source_arn          = (known after apply)
      + statement_id        = "AllowEventBridgeAccessSchedule"
      + statement_id_prefix = (known after apply)
    }

Plan: 6 to add, 12 to change, 0 to destroy.

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: tfplan

To perform exactly these actions, run the following command to apply:
    terraform apply "tfplan"

LLM Review

Risk: 🔴 HIGH

Plan adds 6 resources (EventBridge rules, targets, Lambda permissions) and updates 12 IAM/Lambda resources with 0 destroys; the IAM changes widen CI trust policies and team-provisioner permissions, including a new permissions-boundary-setting grant, so it needs careful review before apply.

  • 🔒 [security] CI trust policies for javabin-ci-infra, javabin-ci-infra-plan and javabin-ci-registry gain new OIDC subject claims for repo 'javaBin@95047/platform@1175354559' and 'javaBin@95047/registry@1175355770'. These look like repository-ID-based subject formats that differ from the existing repo names. Any repo matching that pattern could assume these roles. ci-infra-plan now also trusts 'repo:javaBin/platform:*' plus the new pattern, which is a wildcard over all refs/environments. Verify the new subjects are legitimate and tighten the wildcards.
  • 🔒 [security] team_provisioner role policy adds iam:PutRolePermissionsBoundary (Effect Allow) on sso.amazonaws.com reserved roles, conditioned on the boundary being javabin-team-*-guardrails. It also adds the AWS managed ViewOnlyAccess policy as an attachable resource and sso:PutPermissionsBoundaryToPermissionSet / AttachManagedPolicyToPermissionSet. Together these let the lambda change permission boundaries on SSO roles. Confirm this is needed and that the guardrail policies cannot be weakened by the lambda.
  • 🔒 [security] budget_enforcer gains rds:StopDBInstance/StartDBInstance on all DBs, conditioned only on the presence of an aws:ResourceTag/team tag (Null=false). Any tagged database in the account can be stopped. Also ecs:TagResource and ecs:UpdateService are added. The ci_deploy_ecs policies add ecs:RegisterTaskDefinition-related actions with Resource '*'; the EcsTagOnRegister statement is limited by condition but the list-tasks and describe actions are broad. Consider scoping to the cluster ARN.
  • ✅ [routine] Six new resources: two EventBridge rules (budget-enforcer every 6 hours, team-provisioner every 3 hours), two targets, and two lambda permissions for events.amazonaws.com. No NAT gateways, load balancers, RDS or other billable resources are created. Cost impact is negligible (EventBridge invocations, Lambda runtime).
  • ✅ [routine] Lambda function updates for budget-enforcer, plan-review and team-provisioner change code hashes and add environment variables (AWS_ACCOUNT_ID, SUSPENSION_PATH, AWS_ACCESS_GROUP). No destroy or force-replacement. Plan: 6 to add, 12 to change, 0 to destroy.

@Alexanderamiri
Alexanderamiri merged commit 4815576 into main Oct 31, 2026
4 checks passed
@Alexanderamiri
Alexanderamiri deleted the fix/override-app-repos branch October 31, 2026 14:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant