Skip to content

fix(ci): vend read-only plan credentials to app tf-plan - #162

Open
Alexanderamiri wants to merge 2 commits into
mainfrom
fix/sec-plan-readonly
Open

Alexanderamiri wants to merge 2 commits into
mainfrom
fix/sec-plan-readonly

Conversation

@Alexanderamiri

@Alexanderamiri Alexanderamiri commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Why

The CI broker gives app tf-plan jobs javabin-ci-team-{team}: AllowAll plus the developer boundary. That is the same write-capable role the apply gate hands out after LLM review. tf-plan runs on every branch and every PR, and the org default repo permission is write. So anyone who can push a branch to a team repo can change that team's AWS resources directly, with no review and no apply gate. This is finding H1 in the security audit.

What

  • New javabin-ci-plan-{team} roles (for_each over registered_teams). They carry the developer boundary and a team tag, and only the broker role can assume them.
  • Each role gets ReadOnlyAccess plus an inline plan-backend policy:
    • s3:GetObject and s3:ListBucket on the state bucket under apps/{team}/*. There is no Put or Delete, because plan never writes state.
    • DenyForeignTerraformState: an explicit Deny of s3:GetObject(Version) on infra/*, org/*, state/*, env:/* and every other registered team's apps/{team}/*. ReadOnlyAccess grants s3:Get* on everything and the boundary's DenyXTeam exempts the state bucket, so the Allow alone was not a limit.
    • The DynamoDB lock, scoped with the same dynamodb:LeadingKeys condition as ci_team.
    • secretsmanager:GetSecretValue on {team}-* secrets, and kms:Decrypt only through ssm and secretsmanager. These let plan refresh SecureString and secret-version resources, which ReadOnlyAccess cannot read.
    • s3:PutObject on plans/* in the plan-artifacts bucket, for upload-plan.sh.
    • The existing DenyWriteReviewVerdicts deny on risk.json, review.json and override.json.
  • ci_team_allow gets the same Deny, also covering Put/Delete. Before this, ci-team-{team} could read the platform state (which holds the Google OAuth client secret) and overwrite any key in the bucket.
  • ci_broker/handler.py: ROLE_MAP["plan"] becomes javabin-ci-plan-.
  • lambdas/main.tf: the broker may also assume javabin-ci-plan-*. ci-team-* stays allowed for now, so a broker still running the old code keeps working during rollout.
  • New test test_ci_broker.py:
    • plan gets ci-plan-<team>;
    • deploy still gets ci-deploy-<team>;
    • no action ever gets ci-team-*;
    • a bad token gets nothing.
  • Docs: apply-gate.md and reusable-workflows.md.

The roles are named ci-plan-*, not ci-team-plan-*, so they cannot collide with a team whose name starts with plan-.

How verified

  • terraform fmt -recursive -check and terraform validate pass.
  • python -m pytest terraform/lambda-src/tests scripts/tests passes (111 tests). Against the old ROLE_MAP, 2 of the 4 new test cases fail.
  • I read tf-plan.yml from init to upload. The only writes it makes with team credentials are the state lock and the plan upload. Plan review uses the broker OIDC role.
  • I checked the resource types in every app state (cupcake, cupcake-backend, jz-watch, testteam). All of them refresh with Describe/Get/List calls. aws_ssm_parameter decrypts with aws/ssm through SSM.
  • Expected plan:
    • Creates aws_iam_role.ci_plan, its policy attachment and its inline policy, one of each per registered team.
    • Updates the broker role policy, the broker Lambda code and ci_team_allow (in place, one per team).

Review fixes

  • Added DenyForeignTerraformState to ci_plan and ci_team (built from local.foreign_state_keys). A NotResource Deny was not used because it would also deny every other S3 object in the account (plan artifacts, app buckets). Gap: the legacy key apps/platform-test-app/terraform.tfstate is not owned by a registered team and stays readable; it holds no secrets.
  • test_ci_broker.py now patches sys.modules, sys.path and the environment only while importing the handler, so the boto3 stub no longer leaks into later tests.
  • apply-gate.md explains that the Deny, not the Allow, limits state access.

Rollout note

The Lambda code update and the role creation happen in the same apply. An app plan that starts in the seconds between the two could fail once with "Failed to assume role". Re-running the plan fixes it.

Follow-ups

  • Once this has been applied, remove ci-team-* from the broker's sts:AssumeRole and remove the broker trust on ci_team.
  • ci-deploy-{team} (ECR push and ECS update) is still vended for any ref. The broker could require the default branch for deploy. I left that out because the moresleep CI work in flight touches the same flow.
  • If an app's plan needs a read that ReadOnlyAccess does not cover, add it to plan-backend.

🤖 Generated with Claude Code

tf-plan runs on any branch, but the broker vended the write-capable
javabin-ci-team-{team} role, bypassing the LLM review and apply gate. Add
javabin-ci-plan-{team} (ReadOnlyAccess + state read, lock, plan upload)
and have the broker vend it for plan.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@Alexanderamiri
Alexanderamiri requested a review from a team as a code owner October 24, 2026 13:38
@github-actions

Copy link
Copy Markdown

Terraform Plan

🚧 Changes detected — Plan: 9 to add, 2 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_plan["cupcake"] will be created
  + resource "aws_iam_role" "ci_plan" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + AWS = "arn:aws:iam::553637109631:role/javabin-ci-broker"
                        }
                      + Sid       = "AllowViaCIBroker"
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + create_date           = (known after apply)
      + force_detach_policies = false
      + id                    = (known after apply)
      + managed_policy_arns   = (known after apply)
      + max_session_duration  = 3600
      + name                  = "javabin-ci-plan-cupcake"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-developer-boundary"
      + tags                  = {
          + "Name" = "javabin-ci-plan-cupcake"
          + "team" = "cupcake"
        }
      + tags_all              = {
          + "Name"        = "javabin-ci-plan-cupcake"
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "cupcake"
        }
      + unique_id             = (known after apply)
    }

  # module.iam.aws_iam_role.ci_plan["jzwatch"] will be created
  + resource "aws_iam_role" "ci_plan" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + AWS = "arn:aws:iam::553637109631:role/javabin-ci-broker"
                        }
                      + Sid       = "AllowViaCIBroker"
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + create_date           = (known after apply)
      + force_detach_policies = false
      + id                    = (known after apply)
      + managed_policy_arns   = (known after apply)
      + max_session_duration  = 3600
      + name                  = "javabin-ci-plan-jzwatch"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-developer-boundary"
      + tags                  = {
          + "Name" = "javabin-ci-plan-jzwatch"
          + "team" = "jzwatch"
        }
      + tags_all              = {
          + "Name"        = "javabin-ci-plan-jzwatch"
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "jzwatch"
        }
      + unique_id             = (known after apply)
    }

  # module.iam.aws_iam_role.ci_plan["testteam"] will be created
  + resource "aws_iam_role" "ci_plan" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + AWS = "arn:aws:iam::553637109631:role/javabin-ci-broker"
                        }
                      + Sid       = "AllowViaCIBroker"
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + create_date           = (known after apply)
      + force_detach_policies = false
      + id                    = (known after apply)
      + managed_policy_arns   = (known after apply)
      + max_session_duration  = 3600
      + name                  = "javabin-ci-plan-testteam"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-developer-boundary"
      + tags                  = {
          + "Name" = "javabin-ci-plan-testteam"
          + "team" = "testteam"
        }
      + tags_all              = {
          + "Name"        = "javabin-ci-plan-testteam"
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "testteam"
        }
      + unique_id             = (known after apply)
    }

  # module.iam.aws_iam_role_policy.ci_plan["cupcake"] will be created
  + resource "aws_iam_role_policy" "ci_plan" {
      + id          = (known after apply)
      + name        = "plan-backend"
      + name_prefix = (known after apply)
      + policy      = jsonencode(
            {
              + Statement = [
                  + {
                      + Action   = [
                          + "s3:GetObject",
                          + "s3:ListBucket",
                        ]
                      + Effect   = "Allow"
                      + Resource = [
                          + "arn:aws:s3:::javabin-terraform-state-553637109631",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/apps/cupcake/*",
                        ]
                      + Sid      = "ReadTerraformState"
                    },
                  + {
                      + Action    = [
                          + "dynamodb:GetItem",
                          + "dynamodb:PutItem",
                          + "dynamodb:DeleteItem",
                        ]
                      + Condition = {
                          + "ForAllValues:StringLike" = {
                              + "dynamodb:LeadingKeys" = "javabin-terraform-state-553637109631/apps/cupcake/*"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks"
                      + Sid       = "AllowTerraformLocking"
                    },
                  + {
                      + Action   = "secretsmanager:GetSecretValue"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:secretsmanager:eu-central-1:553637109631:secret:cupcake-*"
                      + Sid      = "RefreshTeamSecrets"
                    },
                  + {
                      + Action    = "kms:Decrypt"
                      + Condition = {
                          + StringEquals = {
                              + "kms:ViaService" = [
                                  + "ssm.eu-central-1.amazonaws.com",
                                  + "secretsmanager.eu-central-1.amazonaws.com",
                                ]
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "*"
                      + Sid       = "DecryptViaParameterServices"
                    },
                  + {
                      + Action   = "s3:PutObject"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/plans/*"
                      + Sid      = "UploadPlan"
                    },
                  + {
                      + Action   = [
                          + "s3:PutObject",
                          + "s3:DeleteObject",
                        ]
                      + Effect   = "Deny"
                      + Resource = [
                          + "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*/risk.json",
                          + "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*/review.json",
                          + "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*/override.json",
                        ]
                      + Sid      = "DenyWriteReviewVerdicts"
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + role        = (known after apply)
    }

  # module.iam.aws_iam_role_policy.ci_plan["jzwatch"] will be created
  + resource "aws_iam_role_policy" "ci_plan" {
      + id          = (known after apply)
      + name        = "plan-backend"
      + name_prefix = (known after apply)
      + policy      = jsonencode(
            {
              + Statement = [
                  + {
                      + Action   = [
                          + "s3:GetObject",
                          + "s3:ListBucket",
                        ]
                      + Effect   = "Allow"
                      + Resource = [
                          + "arn:aws:s3:::javabin-terraform-state-553637109631",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/apps/jzwatch/*",
                        ]
                      + Sid      = "ReadTerraformState"
                    },
                  + {
                      + Action    = [
                          + "dynamodb:GetItem",
                          + "dynamodb:PutItem",
                          + "dynamodb:DeleteItem",
                        ]
                      + Condition = {
                          + "ForAllValues:StringLike" = {
                              + "dynamodb:LeadingKeys" = "javabin-terraform-state-553637109631/apps/jzwatch/*"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks"
                      + Sid       = "AllowTerraformLocking"
                    },
                  + {
                      + Action   = "secretsmanager:GetSecretValue"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:secretsmanager:eu-central-1:553637109631:secret:jzwatch-*"
                      + Sid      = "RefreshTeamSecrets"
                    },
                  + {
                      + Action    = "kms:Decrypt"
                      + Condition = {
                          + StringEquals = {
                              + "kms:ViaService" = [
                                  + "ssm.eu-central-1.amazonaws.com",
                                  + "secretsmanager.eu-central-1.amazonaws.com",
                                ]
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "*"
                      + Sid       = "DecryptViaParameterServices"
                    },
                  + {
                      + Action   = "s3:PutObject"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/plans/*"
                      + Sid      = "UploadPlan"
                    },
                  + {
                      + Action   = [
                          + "s3:PutObject",
                          + "s3:DeleteObject",
                        ]
                      + Effect   = "Deny"
                      + Resource = [
                          + "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*/risk.json",
                          + "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*/review.json",
                          + "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*/override.json",
                        ]
                      + Sid      = "DenyWriteReviewVerdicts"
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + role        = (known after apply)
    }

  # module.iam.aws_iam_role_policy.ci_plan["testteam"] will be created
  + resource "aws_iam_role_policy" "ci_plan" {
      + id          = (known after apply)
      + name        = "plan-backend"
      + name_prefix = (known after apply)
      + policy      = jsonencode(
            {
              + Statement = [
                  + {
                      + Action   = [
                          + "s3:GetObject",
                          + "s3:ListBucket",
                        ]
                      + Effect   = "Allow"
                      + Resource = [
                          + "arn:aws:s3:::javabin-terraform-state-553637109631",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/apps/testteam/*",
                        ]
                      + Sid      = "ReadTerraformState"
                    },
                  + {
                      + Action    = [
                          + "dynamodb:GetItem",
                          + "dynamodb:PutItem",
                          + "dynamodb:DeleteItem",
                        ]
                      + Condition = {
                          + "ForAllValues:StringLike" = {
                              + "dynamodb:LeadingKeys" = "javabin-terraform-state-553637109631/apps/testteam/*"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks"
                      + Sid       = "AllowTerraformLocking"
                    },
                  + {
                      + Action   = "secretsmanager:GetSecretValue"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:secretsmanager:eu-central-1:553637109631:secret:testteam-*"
                      + Sid      = "RefreshTeamSecrets"
                    },
                  + {
                      + Action    = "kms:Decrypt"
                      + Condition = {
                          + StringEquals = {
                              + "kms:ViaService" = [
                                  + "ssm.eu-central-1.amazonaws.com",
                                  + "secretsmanager.eu-central-1.amazonaws.com",
                                ]
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "*"
                      + Sid       = "DecryptViaParameterServices"
                    },
                  + {
                      + Action   = "s3:PutObject"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/plans/*"
                      + Sid      = "UploadPlan"
                    },
                  + {
                      + Action   = [
                          + "s3:PutObject",
                          + "s3:DeleteObject",
                        ]
                      + Effect   = "Deny"
                      + Resource = [
                          + "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*/risk.json",
                          + "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*/review.json",
                          + "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*/override.json",
                        ]
                      + Sid      = "DenyWriteReviewVerdicts"
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + role        = (known after apply)
    }

  # module.iam.aws_iam_role_policy_attachment.ci_plan_readonly["cupcake"] will be created
  + resource "aws_iam_role_policy_attachment" "ci_plan_readonly" {
      + id         = (known after apply)
      + policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
      + role       = "javabin-ci-plan-cupcake"
    }

  # module.iam.aws_iam_role_policy_attachment.ci_plan_readonly["jzwatch"] will be created
  + resource "aws_iam_role_policy_attachment" "ci_plan_readonly" {
      + id         = (known after apply)
      + policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
      + role       = "javabin-ci-plan-jzwatch"
    }

  # module.iam.aws_iam_role_policy_attachment.ci_plan_readonly["testteam"] will be created
  + resource "aws_iam_role_policy_attachment" "ci_plan_readonly" {
      + id         = (known after apply)
      + policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
      + role       = "javabin-ci-plan-testteam"
    }

  # module.lambdas.aws_iam_role_policy.ci_broker will be updated in-place
  ~ resource "aws_iam_role_policy" "ci_broker" {
        id     = "javabin-ci-broker:javabin-ci-broker"
        name   = "javabin-ci-broker"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                    {
                        Action   = "ssm:GetParameter"
                        Effect   = "Allow"
                        Resource = [
                            "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/platform/github-app-id",
                            "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/platform/github-app-key",
                        ]
                        Sid      = "ReadGitHubAppCredentials"
                    },
                  ~ {
                      ~ Resource = [
                          + "arn:aws:iam::553637109631:role/javabin-ci-plan-*",
                            "arn:aws:iam::553637109631:role/javabin-ci-team-*",
                            # (1 unchanged element hidden)
                        ]
                        # (3 unchanged attributes hidden)
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.lambdas.aws_lambda_function.ci_broker will be updated in-place
  ~ resource "aws_lambda_function" "ci_broker" {
        id                             = "javabin-ci-broker"
      ~ last_modified                  = "2026-10-23T15:27:38.000+0000" -> (known after apply)
      ~ source_code_hash               = "pBi08oBRCo9QYBZ0XdwEim/lzltg8SEUoch98q41Uig=" -> "1BJmeDUQu1tW2VEsngRCj53Pz/C4AtDqnsmLNj2PHKo="
        tags                           = {}
        # (21 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

Plan: 9 to add, 2 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: 🟡 MEDIUM

Plan creates 3 per-team CI plan IAM roles (cupcake, jzwatch, testteam) with policies and ReadOnlyAccess attachments, and updates the CI broker Lambda code and its IAM policy to allow assuming the new plan roles; nothing is destroyed.

  • 🔒 [security] Each new plan role has ReadOnlyAccess attached, a broad AWS-managed read policy across most services. Scoping is partly compensated by the permissions boundary, but the account-wide read access (including reading secrets metadata and many config resources) is still notable. Confirm this is intended for plan-only CI.
  • 🔒 [security] Each plan role grants secretsmanager:GetSecretValue on team-prefixed secrets (e.g. cupcake-, jzwatch-, testteam-) and kms:Decrypt on '' restricted by kms:ViaService. Plan roles can read secret values, which is more than a plan typically needs; verify the secret prefix wildcards cannot match other teams' secrets.
  • 🔒 [security] The broker's ssm:GetParameter policy is unchanged, but its role-assumption list gains arn:aws:iam::553637109631:role/javabin-ci-plan-*. This widens what the broker can assume; it is bounded by the trust policy and the developer permissions boundary on each new role, which is good. The roles also use a permissions boundary javabin-developer-boundary, which limits privilege escalation.
  • 🔒 [security] Plan roles include a Deny on writing risk.json, review.json and override.json in the plan artifacts bucket, which protects the review verdict pipeline. Note the Deny applies only to the listed object key patterns; verify other write paths (e.g. different key names or prefixes) cannot alter verdicts.
  • ✅ [routine] Lambda ci_broker code is updated in place (source_code_hash change only); no config, runtime or environment changes are shown. Review the deployed artifact source matches the intended commit.
  • 💰 [cost] Only IAM roles, policies and attachments plus one Lambda code update are created; no billable resources such as NAT gateways, RDS or load balancers are added. Cost impact is negligible.

ReadOnlyAccess and AllowAll cover every key in the state bucket and the
developer boundary exempts it, so the apps/{team}/* Allow was not a limit.
Add an explicit Deny on the platform prefixes and other teams' prefixes to
ci-plan (read) and ci-team (read/write). Stop the broker test from leaking
its boto3 stub into later tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@github-actions

Copy link
Copy Markdown

Terraform Plan

🚧 Changes detected — Plan: 9 to add, 5 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_plan["cupcake"] will be created
  + resource "aws_iam_role" "ci_plan" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + AWS = "arn:aws:iam::553637109631:role/javabin-ci-broker"
                        }
                      + Sid       = "AllowViaCIBroker"
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + create_date           = (known after apply)
      + force_detach_policies = false
      + id                    = (known after apply)
      + managed_policy_arns   = (known after apply)
      + max_session_duration  = 3600
      + name                  = "javabin-ci-plan-cupcake"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-developer-boundary"
      + tags                  = {
          + "Name" = "javabin-ci-plan-cupcake"
          + "team" = "cupcake"
        }
      + tags_all              = {
          + "Name"        = "javabin-ci-plan-cupcake"
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "cupcake"
        }
      + unique_id             = (known after apply)
    }

  # module.iam.aws_iam_role.ci_plan["jzwatch"] will be created
  + resource "aws_iam_role" "ci_plan" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + AWS = "arn:aws:iam::553637109631:role/javabin-ci-broker"
                        }
                      + Sid       = "AllowViaCIBroker"
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + create_date           = (known after apply)
      + force_detach_policies = false
      + id                    = (known after apply)
      + managed_policy_arns   = (known after apply)
      + max_session_duration  = 3600
      + name                  = "javabin-ci-plan-jzwatch"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-developer-boundary"
      + tags                  = {
          + "Name" = "javabin-ci-plan-jzwatch"
          + "team" = "jzwatch"
        }
      + tags_all              = {
          + "Name"        = "javabin-ci-plan-jzwatch"
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "jzwatch"
        }
      + unique_id             = (known after apply)
    }

  # module.iam.aws_iam_role.ci_plan["testteam"] will be created
  + resource "aws_iam_role" "ci_plan" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + AWS = "arn:aws:iam::553637109631:role/javabin-ci-broker"
                        }
                      + Sid       = "AllowViaCIBroker"
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + create_date           = (known after apply)
      + force_detach_policies = false
      + id                    = (known after apply)
      + managed_policy_arns   = (known after apply)
      + max_session_duration  = 3600
      + name                  = "javabin-ci-plan-testteam"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-developer-boundary"
      + tags                  = {
          + "Name" = "javabin-ci-plan-testteam"
          + "team" = "testteam"
        }
      + tags_all              = {
          + "Name"        = "javabin-ci-plan-testteam"
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "testteam"
        }
      + unique_id             = (known after apply)
    }

  # module.iam.aws_iam_role_policy.ci_plan["cupcake"] will be created
  + resource "aws_iam_role_policy" "ci_plan" {
      + id          = (known after apply)
      + name        = "plan-backend"
      + name_prefix = (known after apply)
      + policy      = jsonencode(
            {
              + Statement = [
                  + {
                      + Action   = [
                          + "s3:GetObject",
                          + "s3:ListBucket",
                        ]
                      + Effect   = "Allow"
                      + Resource = [
                          + "arn:aws:s3:::javabin-terraform-state-553637109631",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/apps/cupcake/*",
                        ]
                      + Sid      = "ReadTerraformState"
                    },
                  + {
                      + Action   = [
                          + "s3:GetObject",
                          + "s3:GetObjectVersion",
                        ]
                      + Effect   = "Deny"
                      + Resource = [
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/infra/*",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/org/*",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/state/*",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/env:/*",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/apps/jzwatch/*",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/apps/testteam/*",
                        ]
                      + Sid      = "DenyForeignTerraformState"
                    },
                  + {
                      + Action    = [
                          + "dynamodb:GetItem",
                          + "dynamodb:PutItem",
                          + "dynamodb:DeleteItem",
                        ]
                      + Condition = {
                          + "ForAllValues:StringLike" = {
                              + "dynamodb:LeadingKeys" = "javabin-terraform-state-553637109631/apps/cupcake/*"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks"
                      + Sid       = "AllowTerraformLocking"
                    },
                  + {
                      + Action   = "secretsmanager:GetSecretValue"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:secretsmanager:eu-central-1:553637109631:secret:cupcake-*"
                      + Sid      = "RefreshTeamSecrets"
                    },
                  + {
                      + Action    = "kms:Decrypt"
                      + Condition = {
                          + StringEquals = {
                              + "kms:ViaService" = [
                                  + "ssm.eu-central-1.amazonaws.com",
                                  + "secretsmanager.eu-central-1.amazonaws.com",
                                ]
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "*"
                      + Sid       = "DecryptViaParameterServices"
                    },
                  + {
                      + Action   = "s3:PutObject"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/plans/*"
                      + Sid      = "UploadPlan"
                    },
                  + {
                      + Action   = [
                          + "s3:PutObject",
                          + "s3:DeleteObject",
                        ]
                      + Effect   = "Deny"
                      + Resource = [
                          + "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*/risk.json",
                          + "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*/review.json",
                          + "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*/override.json",
                        ]
                      + Sid      = "DenyWriteReviewVerdicts"
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + role        = (known after apply)
    }

  # module.iam.aws_iam_role_policy.ci_plan["jzwatch"] will be created
  + resource "aws_iam_role_policy" "ci_plan" {
      + id          = (known after apply)
      + name        = "plan-backend"
      + name_prefix = (known after apply)
      + policy      = jsonencode(
            {
              + Statement = [
                  + {
                      + Action   = [
                          + "s3:GetObject",
                          + "s3:ListBucket",
                        ]
                      + Effect   = "Allow"
                      + Resource = [
                          + "arn:aws:s3:::javabin-terraform-state-553637109631",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/apps/jzwatch/*",
                        ]
                      + Sid      = "ReadTerraformState"
                    },
                  + {
                      + Action   = [
                          + "s3:GetObject",
                          + "s3:GetObjectVersion",
                        ]
                      + Effect   = "Deny"
                      + Resource = [
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/infra/*",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/org/*",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/state/*",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/env:/*",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/apps/cupcake/*",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/apps/testteam/*",
                        ]
                      + Sid      = "DenyForeignTerraformState"
                    },
                  + {
                      + Action    = [
                          + "dynamodb:GetItem",
                          + "dynamodb:PutItem",
                          + "dynamodb:DeleteItem",
                        ]
                      + Condition = {
                          + "ForAllValues:StringLike" = {
                              + "dynamodb:LeadingKeys" = "javabin-terraform-state-553637109631/apps/jzwatch/*"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks"
                      + Sid       = "AllowTerraformLocking"
                    },
                  + {
                      + Action   = "secretsmanager:GetSecretValue"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:secretsmanager:eu-central-1:553637109631:secret:jzwatch-*"
                      + Sid      = "RefreshTeamSecrets"
                    },
                  + {
                      + Action    = "kms:Decrypt"
                      + Condition = {
                          + StringEquals = {
                              + "kms:ViaService" = [
                                  + "ssm.eu-central-1.amazonaws.com",
                                  + "secretsmanager.eu-central-1.amazonaws.com",
                                ]
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "*"
                      + Sid       = "DecryptViaParameterServices"
                    },
                  + {
                      + Action   = "s3:PutObject"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/plans/*"
                      + Sid      = "UploadPlan"
                    },
                  + {
                      + Action   = [
                          + "s3:PutObject",
                          + "s3:DeleteObject",
                        ]
                      + Effect   = "Deny"
                      + Resource = [
                          + "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*/risk.json",
                          + "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*/review.json",
                          + "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*/override.json",
                        ]
                      + Sid      = "DenyWriteReviewVerdicts"
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + role        = (known after apply)
    }

  # module.iam.aws_iam_role_policy.ci_plan["testteam"] will be created
  + resource "aws_iam_role_policy" "ci_plan" {
      + id          = (known after apply)
      + name        = "plan-backend"
      + name_prefix = (known after apply)
      + policy      = jsonencode(
            {
              + Statement = [
                  + {
                      + Action   = [
                          + "s3:GetObject",
                          + "s3:ListBucket",
                        ]
                      + Effect   = "Allow"
                      + Resource = [
                          + "arn:aws:s3:::javabin-terraform-state-553637109631",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/apps/testteam/*",
                        ]
                      + Sid      = "ReadTerraformState"
                    },
                  + {
                      + Action   = [
                          + "s3:GetObject",
                          + "s3:GetObjectVersion",
                        ]
                      + Effect   = "Deny"
                      + Resource = [
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/infra/*",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/org/*",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/state/*",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/env:/*",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/apps/cupcake/*",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/apps/jzwatch/*",
                        ]
                      + Sid      = "DenyForeignTerraformState"
                    },
                  + {
                      + Action    = [
                          + "dynamodb:GetItem",
                          + "dynamodb:PutItem",
                          + "dynamodb:DeleteItem",
                        ]
                      + Condition = {
                          + "ForAllValues:StringLike" = {
                              + "dynamodb:LeadingKeys" = "javabin-terraform-state-553637109631/apps/testteam/*"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks"
                      + Sid       = "AllowTerraformLocking"
                    },
                  + {
                      + Action   = "secretsmanager:GetSecretValue"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:secretsmanager:eu-central-1:553637109631:secret:testteam-*"
                      + Sid      = "RefreshTeamSecrets"
                    },
                  + {
                      + Action    = "kms:Decrypt"
                      + Condition = {
                          + StringEquals = {
                              + "kms:ViaService" = [
                                  + "ssm.eu-central-1.amazonaws.com",
                                  + "secretsmanager.eu-central-1.amazonaws.com",
                                ]
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "*"
                      + Sid       = "DecryptViaParameterServices"
                    },
                  + {
                      + Action   = "s3:PutObject"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/plans/*"
                      + Sid      = "UploadPlan"
                    },
                  + {
                      + Action   = [
                          + "s3:PutObject",
                          + "s3:DeleteObject",
                        ]
                      + Effect   = "Deny"
                      + Resource = [
                          + "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*/risk.json",
                          + "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*/review.json",
                          + "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*/override.json",
                        ]
                      + Sid      = "DenyWriteReviewVerdicts"
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + role        = (known after apply)
    }

  # module.iam.aws_iam_role_policy.ci_team_allow["cupcake"] will be updated in-place
  ~ resource "aws_iam_role_policy" "ci_team_allow" {
        id     = "javabin-ci-team-cupcake:team-management"
        name   = "team-management"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                    # (2 unchanged elements hidden)
                    {
                        Action   = [
                            "s3:GetObject",
                            "s3:PutObject",
                            "s3:DeleteObject",
                            "s3:ListBucket",
                        ]
                        Effect   = "Allow"
                        Resource = [
                            "arn:aws:s3:::javabin-terraform-state-553637109631",
                            "arn:aws:s3:::javabin-terraform-state-553637109631/apps/cupcake/*",
                        ]
                        Sid      = "AllowTerraformBackend"
                    },
                  + {
                      + Action   = [
                          + "s3:GetObject",
                          + "s3:GetObjectVersion",
                          + "s3:PutObject",
                          + "s3:DeleteObject",
                          + "s3:DeleteObjectVersion",
                        ]
                      + Effect   = "Deny"
                      + Resource = [
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/infra/*",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/org/*",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/state/*",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/env:/*",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/apps/jzwatch/*",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/apps/testteam/*",
                        ]
                      + Sid      = "DenyForeignTerraformState"
                    },
                    {
                        Action    = [
                            "dynamodb:GetItem",
                            "dynamodb:PutItem",
                            "dynamodb:DeleteItem",
                        ]
                        Condition = {
                            "ForAllValues:StringLike" = {
                                "dynamodb:LeadingKeys" = "javabin-terraform-state-553637109631/apps/cupcake/*"
                            }
                        }
                        Effect    = "Allow"
                        Resource  = "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks"
                        Sid       = "AllowTerraformLocking"
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.iam.aws_iam_role_policy.ci_team_allow["jzwatch"] will be updated in-place
  ~ resource "aws_iam_role_policy" "ci_team_allow" {
        id     = "javabin-ci-team-jzwatch:team-management"
        name   = "team-management"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                    # (2 unchanged elements hidden)
                    {
                        Action   = [
                            "s3:GetObject",
                            "s3:PutObject",
                            "s3:DeleteObject",
                            "s3:ListBucket",
                        ]
                        Effect   = "Allow"
                        Resource = [
                            "arn:aws:s3:::javabin-terraform-state-553637109631",
                            "arn:aws:s3:::javabin-terraform-state-553637109631/apps/jzwatch/*",
                        ]
                        Sid      = "AllowTerraformBackend"
                    },
                  + {
                      + Action   = [
                          + "s3:GetObject",
                          + "s3:GetObjectVersion",
                          + "s3:PutObject",
                          + "s3:DeleteObject",
                          + "s3:DeleteObjectVersion",
                        ]
                      + Effect   = "Deny"
                      + Resource = [
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/infra/*",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/org/*",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/state/*",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/env:/*",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/apps/cupcake/*",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/apps/testteam/*",
                        ]
                      + Sid      = "DenyForeignTerraformState"
                    },
                    {
                        Action    = [
                            "dynamodb:GetItem",
                            "dynamodb:PutItem",
                            "dynamodb:DeleteItem",
                        ]
                        Condition = {
                            "ForAllValues:StringLike" = {
                                "dynamodb:LeadingKeys" = "javabin-terraform-state-553637109631/apps/jzwatch/*"
                            }
                        }
                        Effect    = "Allow"
                        Resource  = "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks"
                        Sid       = "AllowTerraformLocking"
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.iam.aws_iam_role_policy.ci_team_allow["testteam"] will be updated in-place
  ~ resource "aws_iam_role_policy" "ci_team_allow" {
        id     = "javabin-ci-team-testteam:team-management"
        name   = "team-management"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                    # (2 unchanged elements hidden)
                    {
                        Action   = [
                            "s3:GetObject",
                            "s3:PutObject",
                            "s3:DeleteObject",
                            "s3:ListBucket",
                        ]
                        Effect   = "Allow"
                        Resource = [
                            "arn:aws:s3:::javabin-terraform-state-553637109631",
                            "arn:aws:s3:::javabin-terraform-state-553637109631/apps/testteam/*",
                        ]
                        Sid      = "AllowTerraformBackend"
                    },
                  + {
                      + Action   = [
                          + "s3:GetObject",
                          + "s3:GetObjectVersion",
                          + "s3:PutObject",
                          + "s3:DeleteObject",
                          + "s3:DeleteObjectVersion",
                        ]
                      + Effect   = "Deny"
                      + Resource = [
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/infra/*",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/org/*",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/state/*",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/env:/*",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/apps/cupcake/*",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/apps/jzwatch/*",
                        ]
                      + Sid      = "DenyForeignTerraformState"
                    },
                    {
                        Action    = [
                            "dynamodb:GetItem",
                            "dynamodb:PutItem",
                            "dynamodb:DeleteItem",
                        ]
                        Condition = {
                            "ForAllValues:StringLike" = {
                                "dynamodb:LeadingKeys" = "javabin-terraform-state-553637109631/apps/testteam/*"
                            }
                        }
                        Effect    = "Allow"
                        Resource  = "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks"
                        Sid       = "AllowTerraformLocking"
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.iam.aws_iam_role_policy_attachment.ci_plan_readonly["cupcake"] will be created
  + resource "aws_iam_role_policy_attachment" "ci_plan_readonly" {
      + id         = (known after apply)
      + policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
      + role       = "javabin-ci-plan-cupcake"
    }

  # module.iam.aws_iam_role_policy_attachment.ci_plan_readonly["jzwatch"] will be created
  + resource "aws_iam_role_policy_attachment" "ci_plan_readonly" {
      + id         = (known after apply)
      + policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
      + role       = "javabin-ci-plan-jzwatch"
    }

  # module.iam.aws_iam_role_policy_attachment.ci_plan_readonly["testteam"] will be created
  + resource "aws_iam_role_policy_attachment" "ci_plan_readonly" {
      + id         = (known after apply)
      + policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
      + role       = "javabin-ci-plan-testteam"
    }

  # module.lambdas.aws_iam_role_policy.ci_broker will be updated in-place
  ~ resource "aws_iam_role_policy" "ci_broker" {
        id     = "javabin-ci-broker:javabin-ci-broker"
        name   = "javabin-ci-broker"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                    {
                        Action   = "ssm:GetParameter"
                        Effect   = "Allow"
                        Resource = [
                            "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/platform/github-app-id",
                            "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/platform/github-app-key",
                        ]
                        Sid      = "ReadGitHubAppCredentials"
                    },
                  ~ {
                      ~ Resource = [
                          + "arn:aws:iam::553637109631:role/javabin-ci-plan-*",
                            "arn:aws:iam::553637109631:role/javabin-ci-team-*",
                            # (1 unchanged element hidden)
                        ]
                        # (3 unchanged attributes hidden)
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.lambdas.aws_lambda_function.ci_broker will be updated in-place
  ~ resource "aws_lambda_function" "ci_broker" {
        id                             = "javabin-ci-broker"
      ~ last_modified                  = "2026-10-23T15:27:38.000+0000" -> (known after apply)
      ~ source_code_hash               = "pBi08oBRCo9QYBZ0XdwEim/lzltg8SEUoch98q41Uig=" -> "1BJmeDUQu1tW2VEsngRCj53Pz/C4AtDqnsmLNj2PHKo="
        tags                           = {}
        # (21 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

Plan: 9 to add, 5 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: 🟡 MEDIUM

Plan creates 9 CI plan IAM resources for three teams (cupcake, jzwatch, testteam), updates team-management policies and the CI broker role/Lambda, and destroys nothing.

  • 🔒 [security] Three new IAM roles (javabin-ci-plan-) are assumable by javabin-ci-broker and carry the javabin-developer-boundary permissions boundary, with ReadOnlyAccess attached. The boundary and cross-team Deny statements limit blast radius, which is good.
  • 🔒 [security] The ci_broker policy adds 'arn:aws:iam::553637109631:role/javabin-ci-plan-*' to its sts:AssumeRole resources. This widens what the broker Lambda can assume, but the scope is limited to the plan roles and the broker is already the trusted principal. Confirm this is intended.
  • 🔒 [security] Each plan role grants kms:Decrypt on '' restricted by kms:ViaService to SSM and Secrets Manager only. Secrets access is limited to per-team name prefixes (cupcake-, jzwatch-, testteam-). Acceptable, but worth noting as a broad KMS grant.
  • 🔒 [security] Team-management policies gain Deny statements for other teams' state paths and for s3:PutObject/DeleteObject on review verdict files (risk.json, review.json, override.json). These are restrictive changes that tighten access, so no grant is widened.
  • 💰 [cost] No billable resources such as NAT gateways, load balancers, or RDS instances are created. IAM and Lambda changes have negligible cost.
  • ✅ [routine] The Lambda javabin-ci-broker is updated in place with a new source_code_hash, so this is a code-only deploy. No destruction or replacement occurs (0 to destroy).

This branch has not been deployed

No deployments
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