Skip to content

feat(platform): app-to-app calls with private networking and scopes - #156

Open
Alexanderamiri wants to merge 8 commits into
mainfrom
feat/app-to-app-calls
Open

Alexanderamiri wants to merge 8 commits into
mainfrom
feat/app-to-app-calls

Conversation

@Alexanderamiri

@Alexanderamiri Alexanderamiri commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Why

Apps can only reach each other over the internet today (cupcake-backend calls sleepingpill at https://sleepingpill.javazone.no with basic auth), and every app task shares javabin-ecs-tasks-sg, so any team's task can reach any team's database port. This PR lets an app declare private app-to-app calls in app.yaml, optionally with OAuth scopes, and gives every app its own security group.

Design and options (Service Connect, internal ALB, VPC Lattice and Cognito M2M): explore/app-to-app-design.md (outside the repo).

What

app.yaml

# caller (cupcake-backend)
calls:
  - {app: moresleep, team: moresleep, scopes: [talks.read], env: SP_BASE_URL}
# callee (moresleep)
provides:
  scopes: {talks.read: Read talks}
  consumers:
    - {app: cupcake-backend, team: cupcake, scopes: [talks.read]}

An edge exists only when both sides declare it. Granted scopes are what the caller requested, intersected with what the callee allows that caller.

Per-app network (new module app-network, emitted for every app)

  • Each app gets its own security group {team}-{app}-tasks. The ALB can reach only the app's port.
  • Each app gets a Cloud Map name {team}-{app}.javabin.internal in a new shared private namespace (networking/).
  • The module writes the request to /javabin/apps/{team}/{app}/network.
  • ecs-service gets an optional service_registry_arns.
  • service-rds now allows ingress only from the app's own group, which closes the cross-team database known issue.

Lambda javabin-app-peers-sync

  • Triggered by EventBridge on */network, plus a daily reconcile at 03:45 UTC. Every run recomputes everything.
  • Adds and removes ingress rules where both sides agree. Rules carry the tag managed-by=javabin-app-peers-sync, and rules a team created itself are never touched.
  • The security group in a request must be that app's own (name {team}-{app}-tasks, tag team={team}). Otherwise a team could make the Lambda open another team's group.
  • Scopes use the internal Cognito pool: one resource server {t}-{a} per callee, and one client-credentials client m2m.{t}-{a} per caller, with 60-minute tokens.
  • Writes peer-{t}-{a}-url, peer-{t}-{a}-scopes (scopes the caller's client really holds for that callee, or none), m2m-client-{id,secret} and peers-status into the caller's own path, only when a value changes. app-secret-rollout restarts the caller when the URL, scopes or credentials change.
  • An app whose request turns invalid is frozen, not removed: its managed rules, resource server, client scopes and parameters stay while every valid side still declares the edge. The valid side can still withdraw. Peers see callee-invalid / caller-invalid.
  • Reconcile also checks that Cloud Map names match their owner, that every record in the javabin.internal Route53 zone is an A record Cloud Map wrote for a registered instance, and that no task is wearing another app's security group. Findings go to Slack and are never auto-fixed.
  • IAM is least privilege: SG edits only on groups tagged javabin-network=app in the platform VPC, tags only when a rule is created, and Cognito access only on the internal pool.

Injected variables

  • Caller: <APP>_URL (or the env: name), <..>_SCOPES (SSM secret with the granted scopes), PEER_AUTH_TOKEN_URL, PEER_AUTH_CLIENT_ID, PEER_AUTH_CLIENT_SECRET.
  • Callee: PEER_AUTH_ISSUER, PEER_AUTH_JWKS_URL, PEER_AUTH_SCOPE_PREFIX.
  • The generator rejects these names if they collide with names from environment:, resources or auth.

Boundary

  • DenyShared adds servicediscovery:Delete*.
  • DenyTagStrip adds servicediscovery:TagResource and servicediscovery:UntagResource, so teams cannot delete or retag the shared namespace. Both statements are Deny statements with conditions on team=shared (and platform for tags), so teams' own Cloud Map services are unaffected.
  • New DenyAppDNS: denies route53:ChangeResourceRecordSets when any record name matches *javabin.internal. The namespace creates a Route53 private zone, and Route53 ignores tag conditions, so without this any team could rewrite another app's private A record and capture its M2M tokens. Record names are known at plan time, unlike the zone ID, so the boundary stays readable in the plan. IAM policy simulator: denied for cupcake-cupcake.javabin.internal and the apex, allowed for testteam.java.no.
  • DenyNetwork adds route53:DeleteHostedZone (teams only write records; zones are platform-managed). The route53:DeleteHostedZone in DenyShared never worked, because of the tag condition, so it moved here. A private zone's only VPC cannot be disassociated.
  • To make room, DenyNetwork's four internet-gateway and two NAT-gateway actions become ec2:*InternetGateway and ec2:*NatGateway, which also covers egress-only IGWs. DenyPlatInfra's ECS, ELB and DynamoDB ARNs use *:* for region/account, as DenyBadName already does. Both changes only make the denies broader.
  • Size, measured with terraform console: main is 5,946 bytes (live v8: 5,947). This PR brings it to 6,067 of 6,144, leaving 77 bytes. The per-team guardrail copies are smaller still.

Rollout notes

  • Every app redeploys on its next pipeline run. The generator is read from main, so cupcake-cupcake, cupcake-cupcake-backend, jzwatch-jz-watch and testteam-platform-test-app move to their own security group and get a Cloud Map registration in one in-place ECS update (a rolling deploy). The new group's ALB rule is created before the service switches, enforced by depends_on on the module output. None of these apps has a database.
  • App plans need the namespace. Every generated app now looks up the javabin.internal namespace. An app plan that runs between this merge and the end of the platform apply fails, and passes when re-run.
  • Machine-to-machine tokens skip auth-claims. The internal pool's V2_0 pre-token trigger does not run for client-credentials tokens, so they bypass auth-claims. If the internal pool moves to V3_0, auth-claims must pass TokenGeneration_ClientCredentials events through untouched.
  • Cost: about $1.50–2 per month (hosted zone plus Cloud Map instances). Cognito no longer charges per machine-to-machine client.

How verified

  • python3 -m unittest discover -s scripts/tests: 56 OK, including the terraform fmt -check test on generated output for the cupcake and frosting fixtures and a caller/callee pair.
  • python3 -m unittest discover -s terraform/lambda-src/tests: 89 OK.
    • Review fixes added tests: a callee goes invalid and its rule, server, caller scopes and URL survive (and nothing is redone when fixed); a valid caller or callee can still withdraw from a frozen peer; a frozen caller keeps its edge; the scopes parameter holds only granted scopes and follows a callee narrowing; the Route53 audit flags hijacked, orphaned and non-A records.
    • New test_app_peers_sync.py covers consent and scope intersection, pending and callee-missing states, rule add, revoke and port change, team rules left alone, refusal of another team's SG, resource-server ordering, the client with and without grants, caller deletion, writes only on change, invalid requests keeping their credentials, and the audits.
  • terraform fmt -check -recursive terraform: clean.
  • terraform validate on modules/app-network and modules/ecs-service: valid. The platform root is validated by this PR's Terraform Plan check.

Follow-ups

  • Hand the moresleep migration (workstream 2) to moresleep as the first callee and cupcake-backend as the first caller, replacing SP_BASE_URL with basic auth.
  • Add a CI step that prints peers-status after apply.
  • Retire javabin-ecs-tasks-sg and the ecs_tasks_security_group_id output of platform-data once all four services have migrated.
  • Watch the first app apply (testteam) for AccessDenied on the ALB ingress rule, which references the platform-tagged javabin-alb-sg.
  • Consider running the borrowed-group, Cloud Map name and Route53 audits on ECS CreateService/UpdateService and Cloud Map CreateService events through EventBridge, not just daily.
  • Consider having ecs-deploy check that the peer parameters exist and point to peers-status when they do not. The first-deploy dependency is documented in the meantime.
  • Consider TLS between apps (Service Connect) if the residual squatting risk becomes unacceptable.
  • For the security workstream: DenyShared has no Lambda actions, so a team could update a team=shared Lambda, and Route53 record writes in the public app zones are not team-scoped. The private javabin.internal zone is now protected in this PR.

🤖 Generated with Claude Code

Alexanderamiri and others added 3 commits October 24, 2026 15:01
…es in app.yaml

Every app gets its own task security group ({team}-{app}-tasks, ALB ingress on
the app port only) and a Cloud Map name {team}-{app}.javabin.internal in a new
shared private namespace. RDS now accepts only the app's own group, closing the
cross-team database network path.

app.yaml gains calls: (caller) and provides: {scopes, consumers} (callee). The
app-network module writes the request to /javabin/apps/{team}/{app}/network and
the generator injects the peer URL, scope string and M2M credentials.

The boundary denies deleting or retagging the shared namespace.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Recomputes every edge from the app network requests: adds/revokes security
group rules where both sides agree, keeps Cognito resource servers and
client-credentials clients in the internal pool, and writes peer URLs and
client credentials into the caller's SSM path. Daily reconcile audits Cloud Map
names and borrowed security groups. app-secret-rollout ignores network and
peers-status.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
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:02
@github-actions

Copy link
Copy Markdown

Terraform Plan

🚧 Changes detected — Plan: 11 to add, 5 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_policy.developer_boundary will be updated in-place
  ~ resource "aws_iam_policy" "developer_boundary" {
        id               = "arn:aws:iam::553637109631:policy/javabin-developer-boundary"
        name             = "javabin-developer-boundary"
      ~ policy           = jsonencode(
          ~ {
              ~ Statement = [
                    # (9 unchanged elements hidden)
                    {
                        Action      = "*"
                        Condition   = {
                            Null            = {
                                "aws:ResourceTag/team" = "false"
                            }
                            StringNotEquals = {
                                "aws:ResourceTag/team" = [
                                    "${aws:PrincipalTag/team}",
                                    "shared",
                                ]
                            }
                        }
                        Effect      = "Deny"
                        NotResource = [
                            "arn:aws:s3:::javabin-terraform-state-553637109631",
                            "arn:aws:s3:::javabin-terraform-state-553637109631/*",
                            "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*",
                        ]
                        Sid         = "DenyXTeam"
                    },
                  ~ {
                      ~ Action    = [
                            # (18 unchanged elements hidden)
                            "ec2:RevokeSecurityGroup*",
                          + "servicediscovery:Delete*",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Action    = [
                            # (9 unchanged elements hidden)
                            "sns:UntagResource",
                          + "servicediscovery:*agResource",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "ssm:GetParameter*",
                            "ssm:GetParametersByPath",
                        ]
                        Effect   = "Deny"
                        Resource = [
                            "arn:aws:ssm:*:*:parameter/javabin/platform/*",
                            "arn:aws:ssm:*:*:parameter/javabin/slack/*",
                            "arn:aws:ssm:*:*:parameter/javabin/platform-overrides/*",
                        ]
                        Sid      = "DenyPlatSSM"
                    },
                    # (5 unchanged elements hidden)
                ]
                # (1 unchanged attribute hidden)
            }
        )
        tags             = {
            "Name" = "javabin-developer-boundary"
        }
        # (6 unchanged attributes hidden)
    }

  # module.iam.aws_iam_policy.team_guardrails["cupcake"] will be updated in-place
  ~ resource "aws_iam_policy" "team_guardrails" {
        id               = "arn:aws:iam::553637109631:policy/javabin-team-cupcake-guardrails"
        name             = "javabin-team-cupcake-guardrails"
      ~ policy           = jsonencode(
          ~ {
              ~ Statement = [
                    # (9 unchanged elements hidden)
                    {
                        Action      = "*"
                        Condition   = {
                            Null            = {
                                "aws:ResourceTag/team" = "false"
                            }
                            StringNotEquals = {
                                "aws:ResourceTag/team" = [
                                    "cupcake",
                                    "shared",
                                ]
                            }
                        }
                        Effect      = "Deny"
                        NotResource = [
                            "arn:aws:s3:::javabin-terraform-state-553637109631",
                            "arn:aws:s3:::javabin-terraform-state-553637109631/*",
                            "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*",
                        ]
                        Sid         = "DenyXTeam"
                    },
                  ~ {
                      ~ Action    = [
                            # (18 unchanged elements hidden)
                            "ec2:RevokeSecurityGroup*",
                          + "servicediscovery:Delete*",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Action    = [
                            # (9 unchanged elements hidden)
                            "sns:UntagResource",
                          + "servicediscovery:*agResource",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "ssm:GetParameter*",
                            "ssm:GetParametersByPath",
                        ]
                        Effect   = "Deny"
                        Resource = [
                            "arn:aws:ssm:*:*:parameter/javabin/platform/*",
                            "arn:aws:ssm:*:*:parameter/javabin/slack/*",
                            "arn:aws:ssm:*:*:parameter/javabin/platform-overrides/*",
                        ]
                        Sid      = "DenyPlatSSM"
                    },
                    # (5 unchanged elements hidden)
                ]
                # (1 unchanged attribute hidden)
            }
        )
        tags             = {
            "Name"     = "javabin-team-cupcake-guardrails"
            "for-team" = "cupcake"
            "team"     = "shared"
        }
        # (6 unchanged attributes hidden)
    }

  # module.iam.aws_iam_policy.team_guardrails["jzwatch"] will be updated in-place
  ~ resource "aws_iam_policy" "team_guardrails" {
        id               = "arn:aws:iam::553637109631:policy/javabin-team-jzwatch-guardrails"
        name             = "javabin-team-jzwatch-guardrails"
      ~ policy           = jsonencode(
          ~ {
              ~ Statement = [
                    # (9 unchanged elements hidden)
                    {
                        Action      = "*"
                        Condition   = {
                            Null            = {
                                "aws:ResourceTag/team" = "false"
                            }
                            StringNotEquals = {
                                "aws:ResourceTag/team" = [
                                    "jzwatch",
                                    "shared",
                                ]
                            }
                        }
                        Effect      = "Deny"
                        NotResource = [
                            "arn:aws:s3:::javabin-terraform-state-553637109631",
                            "arn:aws:s3:::javabin-terraform-state-553637109631/*",
                            "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*",
                        ]
                        Sid         = "DenyXTeam"
                    },
                  ~ {
                      ~ Action    = [
                            # (18 unchanged elements hidden)
                            "ec2:RevokeSecurityGroup*",
                          + "servicediscovery:Delete*",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Action    = [
                            # (9 unchanged elements hidden)
                            "sns:UntagResource",
                          + "servicediscovery:*agResource",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "ssm:GetParameter*",
                            "ssm:GetParametersByPath",
                        ]
                        Effect   = "Deny"
                        Resource = [
                            "arn:aws:ssm:*:*:parameter/javabin/platform/*",
                            "arn:aws:ssm:*:*:parameter/javabin/slack/*",
                            "arn:aws:ssm:*:*:parameter/javabin/platform-overrides/*",
                        ]
                        Sid      = "DenyPlatSSM"
                    },
                    # (5 unchanged elements hidden)
                ]
                # (1 unchanged attribute hidden)
            }
        )
        tags             = {
            "Name"     = "javabin-team-jzwatch-guardrails"
            "for-team" = "jzwatch"
            "team"     = "shared"
        }
        # (6 unchanged attributes hidden)
    }

  # module.iam.aws_iam_policy.team_guardrails["testteam"] will be updated in-place
  ~ resource "aws_iam_policy" "team_guardrails" {
        id               = "arn:aws:iam::553637109631:policy/javabin-team-testteam-guardrails"
        name             = "javabin-team-testteam-guardrails"
      ~ policy           = jsonencode(
          ~ {
              ~ Statement = [
                    # (9 unchanged elements hidden)
                    {
                        Action      = "*"
                        Condition   = {
                            Null            = {
                                "aws:ResourceTag/team" = "false"
                            }
                            StringNotEquals = {
                                "aws:ResourceTag/team" = [
                                    "testteam",
                                    "shared",
                                ]
                            }
                        }
                        Effect      = "Deny"
                        NotResource = [
                            "arn:aws:s3:::javabin-terraform-state-553637109631",
                            "arn:aws:s3:::javabin-terraform-state-553637109631/*",
                            "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*",
                        ]
                        Sid         = "DenyXTeam"
                    },
                  ~ {
                      ~ Action    = [
                            # (18 unchanged elements hidden)
                            "ec2:RevokeSecurityGroup*",
                          + "servicediscovery:Delete*",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Action    = [
                            # (9 unchanged elements hidden)
                            "sns:UntagResource",
                          + "servicediscovery:*agResource",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "ssm:GetParameter*",
                            "ssm:GetParametersByPath",
                        ]
                        Effect   = "Deny"
                        Resource = [
                            "arn:aws:ssm:*:*:parameter/javabin/platform/*",
                            "arn:aws:ssm:*:*:parameter/javabin/slack/*",
                            "arn:aws:ssm:*:*:parameter/javabin/platform-overrides/*",
                        ]
                        Sid      = "DenyPlatSSM"
                    },
                    # (5 unchanged elements hidden)
                ]
                # (1 unchanged attribute hidden)
            }
        )
        tags             = {
            "Name"     = "javabin-team-testteam-guardrails"
            "for-team" = "testteam"
            "team"     = "shared"
        }
        # (6 unchanged attributes hidden)
    }

  # module.lambdas.aws_cloudwatch_event_rule.app_peers_sync_reconcile will be created
  + resource "aws_cloudwatch_event_rule" "app_peers_sync_reconcile" {
      + arn                 = (known after apply)
      + description         = "Daily reconcile and audit of app-to-app calls"
      + event_bus_name      = "default"
      + force_destroy       = false
      + id                  = (known after apply)
      + name                = "javabin-app-peers-sync-reconcile"
      + name_prefix         = (known after apply)
      + schedule_expression = "cron(45 3 * * ? *)"
      + tags_all            = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_event_rule.app_peers_sync_trigger will be created
  + resource "aws_cloudwatch_event_rule" "app_peers_sync_trigger" {
      + arn            = (known after apply)
      + description    = "app.yaml calls/provides request created, changed or deleted"
      + event_bus_name = "default"
      + event_pattern  = jsonencode(
            {
              + detail      = {
                  + name      = [
                      + {
                          + wildcard = "/javabin/apps/*/*/network"
                        },
                    ]
                  + operation = [
                      + "Create",
                      + "Update",
                      + "Delete",
                    ]
                }
              + detail-type = [
                  + "Parameter Store Change",
                ]
              + source      = [
                  + "aws.ssm",
                ]
            }
        )
      + force_destroy  = false
      + id             = (known after apply)
      + name           = "javabin-app-peers-sync-trigger"
      + name_prefix    = (known after apply)
      + tags_all       = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_event_target.app_peers_sync will be created
  + resource "aws_cloudwatch_event_target" "app_peers_sync" {
      + arn            = (known after apply)
      + event_bus_name = "default"
      + force_destroy  = false
      + id             = (known after apply)
      + rule           = "javabin-app-peers-sync-trigger"
      + target_id      = "invoke-app-peers-sync"
    }

  # module.lambdas.aws_cloudwatch_event_target.app_peers_sync_reconcile will be created
  + resource "aws_cloudwatch_event_target" "app_peers_sync_reconcile" {
      + arn            = (known after apply)
      + event_bus_name = "default"
      + force_destroy  = false
      + id             = (known after apply)
      + input          = jsonencode(
            {
              + action = "reconcile"
            }
        )
      + rule           = "javabin-app-peers-sync-reconcile"
      + target_id      = "reconcile-app-peers-sync"
    }

  # module.lambdas.aws_iam_role.app_peers_sync will be created
  + resource "aws_iam_role" "app_peers_sync" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "lambda.amazonaws.com"
                        }
                    },
                ]
              + 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-app-peers-sync"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      + tags_all              = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
      + unique_id             = (known after apply)
    }

  # module.lambdas.aws_iam_role_policy.app_peers_sync will be created
  + resource "aws_iam_role_policy" "app_peers_sync" {
      + id          = (known after apply)
      + name        = "javabin-app-peers-sync"
      + name_prefix = (known after apply)
      + policy      = jsonencode(
            {
              + Statement = [
                  + {
                      + Action   = [
                          + "ssm:GetParameter",
                          + "ssm:GetParametersByPath",
                        ]
                      + Effect   = "Allow"
                      + Resource = [
                          + "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/apps",
                          + "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/apps/*",
                        ]
                      + Sid      = "ReadAppRequests"
                    },
                  + {
                      + Action   = [
                          + "ssm:PutParameter",
                          + "ssm:DeleteParameter",
                          + "ssm:DeleteParameters",
                          + "ssm:AddTagsToResource",
                        ]
                      + Effect   = "Allow"
                      + Resource = [
                          + "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/apps/*/*/peer-*",
                          + "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/apps/*/*/m2m-client-*",
                          + "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/apps/*/*/peers-status",
                        ]
                      + Sid      = "WritePeerParameters"
                    },
                  + {
                      + Action   = "ssm:GetParameter"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/slack/platform-resource-alerts-webhook"
                      + Sid      = "ReadSlackWebhook"
                    },
                  + {
                      + Action    = [
                          + "ec2:AuthorizeSecurityGroupIngress",
                          + "ec2:RevokeSecurityGroupIngress",
                        ]
                      + Condition = {
                          + ArnEquals    = {
                              + "ec2:Vpc" = "arn:aws:ec2:eu-central-1:553637109631:vpc/vpc-0cd3502de2527a310"
                            }
                          + StringEquals = {
                              + "aws:ResourceTag/javabin-network" = "app"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "arn:aws:ec2:eu-central-1:553637109631:security-group/*"
                      + Sid       = "EditAppSecurityGroups"
                    },
                  + {
                      + Action   = [
                          + "ec2:AuthorizeSecurityGroupIngress",
                          + "ec2:RevokeSecurityGroupIngress",
                        ]
                      + Effect   = "Allow"
                      + Resource = "arn:aws:ec2:eu-central-1:553637109631:security-group-rule/*"
                      + Sid      = "EditPeerRules"
                    },
                  + {
                      + Action    = "ec2:CreateTags"
                      + Condition = {
                          + StringEquals = {
                              + "ec2:CreateAction" = "AuthorizeSecurityGroupIngress"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "arn:aws:ec2:eu-central-1:553637109631:security-group-rule/*"
                      + Sid       = "TagPeerRulesOnCreate"
                    },
                  + {
                      + Action   = [
                          + "ec2:DescribeSecurityGroups",
                          + "ec2:DescribeSecurityGroupRules",
                          + "ec2:DescribeNetworkInterfaces",
                          + "servicediscovery:ListServices",
                          + "servicediscovery:ListInstances",
                          + "servicediscovery:ListTagsForResource",
                          + "ecs:ListTasks",
                        ]
                      + Effect   = "Allow"
                      + Resource = "*"
                      + Sid      = "Describe"
                    },
                  + {
                      + Action   = "ecs:DescribeTasks"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:ecs:eu-central-1:553637109631:task/javabin-platform/*"
                      + Sid      = "DescribeAppTasks"
                    },
                  + {
                      + Action   = [
                          + "cognito-idp:CreateResourceServer",
                          + "cognito-idp:UpdateResourceServer",
                          + "cognito-idp:DeleteResourceServer",
                          + "cognito-idp:DescribeResourceServer",
                          + "cognito-idp:ListResourceServers",
                          + "cognito-idp:CreateUserPoolClient",
                          + "cognito-idp:UpdateUserPoolClient",
                          + "cognito-idp:DeleteUserPoolClient",
                          + "cognito-idp:DescribeUserPoolClient",
                          + "cognito-idp:ListUserPoolClients",
                        ]
                      + Effect   = "Allow"
                      + Resource = "arn:aws:cognito-idp:eu-central-1:553637109631:userpool/eu-central-1_Icikv3dtD"
                      + Sid      = "ManageM2M"
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + role        = (known after apply)
    }

  # module.lambdas.aws_iam_role_policy_attachment.app_peers_sync_logs will be created
  + resource "aws_iam_role_policy_attachment" "app_peers_sync_logs" {
      + id         = (known after apply)
      + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
      + role       = "javabin-app-peers-sync"
    }

  # module.lambdas.aws_lambda_function.app_peers_sync will be created
  + resource "aws_lambda_function" "app_peers_sync" {
      + architectures                  = (known after apply)
      + arn                            = (known after apply)
      + code_sha256                    = (known after apply)
      + filename                       = "lambdas/builds/app_peers_sync.zip"
      + function_name                  = "javabin-app-peers-sync"
      + handler                        = "handler.handler"
      + id                             = (known after apply)
      + invoke_arn                     = (known after apply)
      + last_modified                  = (known after apply)
      + memory_size                    = 128
      + package_type                   = "Zip"
      + publish                        = false
      + qualified_arn                  = (known after apply)
      + qualified_invoke_arn           = (known after apply)
      + reserved_concurrent_executions = 1
      + role                           = (known after apply)
      + runtime                        = "python3.12"
      + signing_job_arn                = (known after apply)
      + signing_profile_version_arn    = (known after apply)
      + skip_destroy                   = false
      + source_code_hash               = "dDrDMlP1Ae5VFDQYI5L1lKVTBG4wmhoYHFVLR8+kSko="
      + source_code_size               = (known after apply)
      + tags_all                       = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
      + timeout                        = 120
      + version                        = (known after apply)

      + environment {
          + variables = (known after apply)
        }
    }

  # module.lambdas.aws_lambda_function.app_secret_rollout will be updated in-place
  ~ resource "aws_lambda_function" "app_secret_rollout" {
        id                             = "javabin-app-secret-rollout"
      ~ last_modified                  = "2026-10-23T22:11:34.519+0000" -> (known after apply)
      ~ source_code_hash               = "Z+R/0D8XkgfnD5R4woPCl4cGzPFa2VcQlnZkX1wB0e0=" -> "Hnafa/5EpXUJ0Fs1gqLFWKsTj3ZEA0ynsoPNfo3gJP8="
        tags                           = {}
        # (21 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_permission.app_peers_sync_eventbridge will be created
  + resource "aws_lambda_permission" "app_peers_sync_eventbridge" {
      + action              = "lambda:InvokeFunction"
      + function_name       = "javabin-app-peers-sync"
      + id                  = (known after apply)
      + principal           = "events.amazonaws.com"
      + source_arn          = (known after apply)
      + statement_id        = "AllowEventBridge"
      + statement_id_prefix = (known after apply)
    }

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

  # module.networking.aws_service_discovery_private_dns_namespace.internal will be created
  + resource "aws_service_discovery_private_dns_namespace" "internal" {
      + arn         = (known after apply)
      + description = "Private app-to-app DNS (app.yaml calls/provides)"
      + hosted_zone = (known after apply)
      + id          = (known after apply)
      + name        = "javabin.internal"
      + tags        = {
          + "Name" = "javabin.internal"
          + "team" = "shared"
        }
      + tags_all    = {
          + "Name"        = "javabin.internal"
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "shared"
        }
      + vpc         = "vpc-0cd3502de2527a310"
    }

Plan: 11 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: 🔴 HIGH

Plan creates 11 resources (app-peers-sync Lambda with IAM role, EventBridge rules, service discovery namespace) and updates 5 (developer boundary, three team guardrail policies, and a Lambda code change); nothing is destroyed, but the IAM boundary/guardrail changes add servicediscovery:Delete* and a malformed servicediscovery:*agResource wildcard, and the new Lambda role grants security-group edits.

  • 🔒 [security] The developer permissions boundary (javabin-developer-boundary) and all three team guardrail policies gain a new Allow-style action 'servicediscovery:Delete*' in an existing statement. The diff does not show Effect for those statements, but they are Allow-side statements, so this widens what developers can delete in Cloud Map. Confirm whether this is intended; boundaries are meant to restrict, not grant.
  • 🔒 [security] The boundaries and guardrails also gain 'servicediscovery:*agResource', an odd wildcard pattern that matches any action ending in 'agResource' (e.g. ListTagsForResource, TagResource, UntagResource-style actions). It looks like a typo and is broader than intended. Fix or remove before apply.
  • 🔒 [security] The new Lambda role javabin-app-peers-sync has permissions boundary javabin-org-boundary and can authorize/revoke ingress on any security group tagged javabin-network=app within vpc-0cd3502de2527a310, plus create tags on rules, manage Cognito user pool clients and resource servers, and write SSM parameters under /javabin/apps///peer-*. This is a substantial privilege; the scope is tag-limited but worth a deliberate review. Lambda reserved concurrency is 1, which limits blast radius somewhat.
  • ✅ [routine] app_secret_rollout Lambda is updated in place (source_code_hash change only), a routine code deploy. New Lambda app_peers_sync is 128 MB, 120s timeout, python3.12, reserved concurrency 1; no VPC config, so no NAT cost. EventBridge rules, targets, and permissions are standard. The Cloud Map private DNS namespace javabin.internal is new and tied to vpc-0cd3502de2527a310.
  • 💰 [cost] Estimated cost is low: Lambda invocations are infrequent (daily reconcile plus SSM change events), and Cloud Map namespace and EventBridge rules carry minimal charges. No NAT gateways, load balancers, or databases are created. No resources are destroyed.

@github-actions

Copy link
Copy Markdown

Terraform Plan

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

Plan output
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_policy.developer_boundary will be updated in-place
  ~ resource "aws_iam_policy" "developer_boundary" {
        id               = "arn:aws:iam::553637109631:policy/javabin-developer-boundary"
        name             = "javabin-developer-boundary"
      ~ policy           = jsonencode(
          ~ {
              ~ Statement = [
                    # (9 unchanged elements hidden)
                    {
                        Action      = "*"
                        Condition   = {
                            Null            = {
                                "aws:ResourceTag/team" = "false"
                            }
                            StringNotEquals = {
                                "aws:ResourceTag/team" = [
                                    "${aws:PrincipalTag/team}",
                                    "shared",
                                ]
                            }
                        }
                        Effect      = "Deny"
                        NotResource = [
                            "arn:aws:s3:::javabin-terraform-state-553637109631",
                            "arn:aws:s3:::javabin-terraform-state-553637109631/*",
                            "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*",
                        ]
                        Sid         = "DenyXTeam"
                    },
                  ~ {
                      ~ Action    = [
                            # (18 unchanged elements hidden)
                            "ec2:RevokeSecurityGroup*",
                          + "servicediscovery:Delete*",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Action    = [
                            # (9 unchanged elements hidden)
                            "sns:UntagResource",
                          + "servicediscovery:TagResource",
                          + "servicediscovery:UntagResource",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "ssm:GetParameter*",
                            "ssm:GetParametersByPath",
                        ]
                        Effect   = "Deny"
                        Resource = [
                            "arn:aws:ssm:*:*:parameter/javabin/platform/*",
                            "arn:aws:ssm:*:*:parameter/javabin/slack/*",
                            "arn:aws:ssm:*:*:parameter/javabin/platform-overrides/*",
                        ]
                        Sid      = "DenyPlatSSM"
                    },
                    # (5 unchanged elements hidden)
                ]
                # (1 unchanged attribute hidden)
            }
        )
        tags             = {
            "Name" = "javabin-developer-boundary"
        }
        # (6 unchanged attributes hidden)
    }

  # module.iam.aws_iam_policy.team_guardrails["cupcake"] will be updated in-place
  ~ resource "aws_iam_policy" "team_guardrails" {
        id               = "arn:aws:iam::553637109631:policy/javabin-team-cupcake-guardrails"
        name             = "javabin-team-cupcake-guardrails"
      ~ policy           = jsonencode(
          ~ {
              ~ Statement = [
                    # (9 unchanged elements hidden)
                    {
                        Action      = "*"
                        Condition   = {
                            Null            = {
                                "aws:ResourceTag/team" = "false"
                            }
                            StringNotEquals = {
                                "aws:ResourceTag/team" = [
                                    "cupcake",
                                    "shared",
                                ]
                            }
                        }
                        Effect      = "Deny"
                        NotResource = [
                            "arn:aws:s3:::javabin-terraform-state-553637109631",
                            "arn:aws:s3:::javabin-terraform-state-553637109631/*",
                            "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*",
                        ]
                        Sid         = "DenyXTeam"
                    },
                  ~ {
                      ~ Action    = [
                            # (18 unchanged elements hidden)
                            "ec2:RevokeSecurityGroup*",
                          + "servicediscovery:Delete*",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Action    = [
                            # (9 unchanged elements hidden)
                            "sns:UntagResource",
                          + "servicediscovery:TagResource",
                          + "servicediscovery:UntagResource",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "ssm:GetParameter*",
                            "ssm:GetParametersByPath",
                        ]
                        Effect   = "Deny"
                        Resource = [
                            "arn:aws:ssm:*:*:parameter/javabin/platform/*",
                            "arn:aws:ssm:*:*:parameter/javabin/slack/*",
                            "arn:aws:ssm:*:*:parameter/javabin/platform-overrides/*",
                        ]
                        Sid      = "DenyPlatSSM"
                    },
                    # (5 unchanged elements hidden)
                ]
                # (1 unchanged attribute hidden)
            }
        )
        tags             = {
            "Name"     = "javabin-team-cupcake-guardrails"
            "for-team" = "cupcake"
            "team"     = "shared"
        }
        # (6 unchanged attributes hidden)
    }

  # module.iam.aws_iam_policy.team_guardrails["jzwatch"] will be updated in-place
  ~ resource "aws_iam_policy" "team_guardrails" {
        id               = "arn:aws:iam::553637109631:policy/javabin-team-jzwatch-guardrails"
        name             = "javabin-team-jzwatch-guardrails"
      ~ policy           = jsonencode(
          ~ {
              ~ Statement = [
                    # (9 unchanged elements hidden)
                    {
                        Action      = "*"
                        Condition   = {
                            Null            = {
                                "aws:ResourceTag/team" = "false"
                            }
                            StringNotEquals = {
                                "aws:ResourceTag/team" = [
                                    "jzwatch",
                                    "shared",
                                ]
                            }
                        }
                        Effect      = "Deny"
                        NotResource = [
                            "arn:aws:s3:::javabin-terraform-state-553637109631",
                            "arn:aws:s3:::javabin-terraform-state-553637109631/*",
                            "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*",
                        ]
                        Sid         = "DenyXTeam"
                    },
                  ~ {
                      ~ Action    = [
                            # (18 unchanged elements hidden)
                            "ec2:RevokeSecurityGroup*",
                          + "servicediscovery:Delete*",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Action    = [
                            # (9 unchanged elements hidden)
                            "sns:UntagResource",
                          + "servicediscovery:TagResource",
                          + "servicediscovery:UntagResource",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "ssm:GetParameter*",
                            "ssm:GetParametersByPath",
                        ]
                        Effect   = "Deny"
                        Resource = [
                            "arn:aws:ssm:*:*:parameter/javabin/platform/*",
                            "arn:aws:ssm:*:*:parameter/javabin/slack/*",
                            "arn:aws:ssm:*:*:parameter/javabin/platform-overrides/*",
                        ]
                        Sid      = "DenyPlatSSM"
                    },
                    # (5 unchanged elements hidden)
                ]
                # (1 unchanged attribute hidden)
            }
        )
        tags             = {
            "Name"     = "javabin-team-jzwatch-guardrails"
            "for-team" = "jzwatch"
            "team"     = "shared"
        }
        # (6 unchanged attributes hidden)
    }

  # module.iam.aws_iam_policy.team_guardrails["testteam"] will be updated in-place
  ~ resource "aws_iam_policy" "team_guardrails" {
        id               = "arn:aws:iam::553637109631:policy/javabin-team-testteam-guardrails"
        name             = "javabin-team-testteam-guardrails"
      ~ policy           = jsonencode(
          ~ {
              ~ Statement = [
                    # (9 unchanged elements hidden)
                    {
                        Action      = "*"
                        Condition   = {
                            Null            = {
                                "aws:ResourceTag/team" = "false"
                            }
                            StringNotEquals = {
                                "aws:ResourceTag/team" = [
                                    "testteam",
                                    "shared",
                                ]
                            }
                        }
                        Effect      = "Deny"
                        NotResource = [
                            "arn:aws:s3:::javabin-terraform-state-553637109631",
                            "arn:aws:s3:::javabin-terraform-state-553637109631/*",
                            "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*",
                        ]
                        Sid         = "DenyXTeam"
                    },
                  ~ {
                      ~ Action    = [
                            # (18 unchanged elements hidden)
                            "ec2:RevokeSecurityGroup*",
                          + "servicediscovery:Delete*",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Action    = [
                            # (9 unchanged elements hidden)
                            "sns:UntagResource",
                          + "servicediscovery:TagResource",
                          + "servicediscovery:UntagResource",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "ssm:GetParameter*",
                            "ssm:GetParametersByPath",
                        ]
                        Effect   = "Deny"
                        Resource = [
                            "arn:aws:ssm:*:*:parameter/javabin/platform/*",
                            "arn:aws:ssm:*:*:parameter/javabin/slack/*",
                            "arn:aws:ssm:*:*:parameter/javabin/platform-overrides/*",
                        ]
                        Sid      = "DenyPlatSSM"
                    },
                    # (5 unchanged elements hidden)
                ]
                # (1 unchanged attribute hidden)
            }
        )
        tags             = {
            "Name"     = "javabin-team-testteam-guardrails"
            "for-team" = "testteam"
            "team"     = "shared"
        }
        # (6 unchanged attributes hidden)
    }

  # module.lambdas.aws_cloudwatch_event_rule.app_peers_sync_reconcile will be created
  + resource "aws_cloudwatch_event_rule" "app_peers_sync_reconcile" {
      + arn                 = (known after apply)
      + description         = "Daily reconcile and audit of app-to-app calls"
      + event_bus_name      = "default"
      + force_destroy       = false
      + id                  = (known after apply)
      + name                = "javabin-app-peers-sync-reconcile"
      + name_prefix         = (known after apply)
      + schedule_expression = "cron(45 3 * * ? *)"
      + tags_all            = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_event_rule.app_peers_sync_trigger will be created
  + resource "aws_cloudwatch_event_rule" "app_peers_sync_trigger" {
      + arn            = (known after apply)
      + description    = "app.yaml calls/provides request created, changed or deleted"
      + event_bus_name = "default"
      + event_pattern  = jsonencode(
            {
              + detail      = {
                  + name      = [
                      + {
                          + wildcard = "/javabin/apps/*/*/network"
                        },
                    ]
                  + operation = [
                      + "Create",
                      + "Update",
                      + "Delete",
                    ]
                }
              + detail-type = [
                  + "Parameter Store Change",
                ]
              + source      = [
                  + "aws.ssm",
                ]
            }
        )
      + force_destroy  = false
      + id             = (known after apply)
      + name           = "javabin-app-peers-sync-trigger"
      + name_prefix    = (known after apply)
      + tags_all       = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_event_target.app_peers_sync will be created
  + resource "aws_cloudwatch_event_target" "app_peers_sync" {
      + arn            = (known after apply)
      + event_bus_name = "default"
      + force_destroy  = false
      + id             = (known after apply)
      + rule           = "javabin-app-peers-sync-trigger"
      + target_id      = "invoke-app-peers-sync"
    }

  # module.lambdas.aws_cloudwatch_event_target.app_peers_sync_reconcile will be created
  + resource "aws_cloudwatch_event_target" "app_peers_sync_reconcile" {
      + arn            = (known after apply)
      + event_bus_name = "default"
      + force_destroy  = false
      + id             = (known after apply)
      + input          = jsonencode(
            {
              + action = "reconcile"
            }
        )
      + rule           = "javabin-app-peers-sync-reconcile"
      + target_id      = "reconcile-app-peers-sync"
    }

  # module.lambdas.aws_iam_role.app_peers_sync will be created
  + resource "aws_iam_role" "app_peers_sync" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "lambda.amazonaws.com"
                        }
                    },
                ]
              + 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-app-peers-sync"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      + tags_all              = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
      + unique_id             = (known after apply)
    }

  # module.lambdas.aws_iam_role_policy.app_peers_sync will be created
  + resource "aws_iam_role_policy" "app_peers_sync" {
      + id          = (known after apply)
      + name        = "javabin-app-peers-sync"
      + name_prefix = (known after apply)
      + policy      = jsonencode(
            {
              + Statement = [
                  + {
                      + Action   = [
                          + "ssm:GetParameter",
                          + "ssm:GetParametersByPath",
                        ]
                      + Effect   = "Allow"
                      + Resource = [
                          + "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/apps",
                          + "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/apps/*",
                        ]
                      + Sid      = "ReadAppRequests"
                    },
                  + {
                      + Action   = [
                          + "ssm:PutParameter",
                          + "ssm:DeleteParameter",
                          + "ssm:DeleteParameters",
                          + "ssm:AddTagsToResource",
                        ]
                      + Effect   = "Allow"
                      + Resource = [
                          + "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/apps/*/*/peer-*",
                          + "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/apps/*/*/m2m-client-*",
                          + "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/apps/*/*/peers-status",
                        ]
                      + Sid      = "WritePeerParameters"
                    },
                  + {
                      + Action   = "ssm:GetParameter"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/slack/platform-resource-alerts-webhook"
                      + Sid      = "ReadSlackWebhook"
                    },
                  + {
                      + Action    = [
                          + "ec2:AuthorizeSecurityGroupIngress",
                          + "ec2:RevokeSecurityGroupIngress",
                        ]
                      + Condition = {
                          + ArnEquals    = {
                              + "ec2:Vpc" = "arn:aws:ec2:eu-central-1:553637109631:vpc/vpc-0cd3502de2527a310"
                            }
                          + StringEquals = {
                              + "aws:ResourceTag/javabin-network" = "app"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "arn:aws:ec2:eu-central-1:553637109631:security-group/*"
                      + Sid       = "EditAppSecurityGroups"
                    },
                  + {
                      + Action   = [
                          + "ec2:AuthorizeSecurityGroupIngress",
                          + "ec2:RevokeSecurityGroupIngress",
                        ]
                      + Effect   = "Allow"
                      + Resource = "arn:aws:ec2:eu-central-1:553637109631:security-group-rule/*"
                      + Sid      = "EditPeerRules"
                    },
                  + {
                      + Action    = "ec2:CreateTags"
                      + Condition = {
                          + StringEquals = {
                              + "ec2:CreateAction" = "AuthorizeSecurityGroupIngress"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "arn:aws:ec2:eu-central-1:553637109631:security-group-rule/*"
                      + Sid       = "TagPeerRulesOnCreate"
                    },
                  + {
                      + Action   = [
                          + "ec2:DescribeSecurityGroups",
                          + "ec2:DescribeSecurityGroupRules",
                          + "ec2:DescribeNetworkInterfaces",
                          + "servicediscovery:ListServices",
                          + "servicediscovery:ListInstances",
                          + "servicediscovery:ListTagsForResource",
                          + "ecs:ListTasks",
                        ]
                      + Effect   = "Allow"
                      + Resource = "*"
                      + Sid      = "Describe"
                    },
                  + {
                      + Action   = "ecs:DescribeTasks"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:ecs:eu-central-1:553637109631:task/javabin-platform/*"
                      + Sid      = "DescribeAppTasks"
                    },
                  + {
                      + Action   = [
                          + "cognito-idp:CreateResourceServer",
                          + "cognito-idp:UpdateResourceServer",
                          + "cognito-idp:DeleteResourceServer",
                          + "cognito-idp:DescribeResourceServer",
                          + "cognito-idp:ListResourceServers",
                          + "cognito-idp:CreateUserPoolClient",
                          + "cognito-idp:UpdateUserPoolClient",
                          + "cognito-idp:DeleteUserPoolClient",
                          + "cognito-idp:DescribeUserPoolClient",
                          + "cognito-idp:ListUserPoolClients",
                        ]
                      + Effect   = "Allow"
                      + Resource = "arn:aws:cognito-idp:eu-central-1:553637109631:userpool/eu-central-1_Icikv3dtD"
                      + Sid      = "ManageM2M"
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + role        = (known after apply)
    }

  # module.lambdas.aws_iam_role_policy_attachment.app_peers_sync_logs will be created
  + resource "aws_iam_role_policy_attachment" "app_peers_sync_logs" {
      + id         = (known after apply)
      + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
      + role       = "javabin-app-peers-sync"
    }

  # module.lambdas.aws_lambda_function.app_peers_sync will be created
  + resource "aws_lambda_function" "app_peers_sync" {
      + architectures                  = (known after apply)
      + arn                            = (known after apply)
      + code_sha256                    = (known after apply)
      + filename                       = "lambdas/builds/app_peers_sync.zip"
      + function_name                  = "javabin-app-peers-sync"
      + handler                        = "handler.handler"
      + id                             = (known after apply)
      + invoke_arn                     = (known after apply)
      + last_modified                  = (known after apply)
      + memory_size                    = 128
      + package_type                   = "Zip"
      + publish                        = false
      + qualified_arn                  = (known after apply)
      + qualified_invoke_arn           = (known after apply)
      + reserved_concurrent_executions = 1
      + role                           = (known after apply)
      + runtime                        = "python3.12"
      + signing_job_arn                = (known after apply)
      + signing_profile_version_arn    = (known after apply)
      + skip_destroy                   = false
      + source_code_hash               = "dDrDMlP1Ae5VFDQYI5L1lKVTBG4wmhoYHFVLR8+kSko="
      + source_code_size               = (known after apply)
      + tags_all                       = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
      + timeout                        = 120
      + version                        = (known after apply)

      + environment {
          + variables = (known after apply)
        }
    }

  # module.lambdas.aws_lambda_function.app_secret_rollout will be updated in-place
  ~ resource "aws_lambda_function" "app_secret_rollout" {
        id                             = "javabin-app-secret-rollout"
      ~ last_modified                  = "2026-10-23T22:11:34.519+0000" -> (known after apply)
      ~ source_code_hash               = "Z+R/0D8XkgfnD5R4woPCl4cGzPFa2VcQlnZkX1wB0e0=" -> "Hnafa/5EpXUJ0Fs1gqLFWKsTj3ZEA0ynsoPNfo3gJP8="
        tags                           = {}
        # (21 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_permission.app_peers_sync_eventbridge will be created
  + resource "aws_lambda_permission" "app_peers_sync_eventbridge" {
      + action              = "lambda:InvokeFunction"
      + function_name       = "javabin-app-peers-sync"
      + id                  = (known after apply)
      + principal           = "events.amazonaws.com"
      + source_arn          = (known after apply)
      + statement_id        = "AllowEventBridge"
      + statement_id_prefix = (known after apply)
    }

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

  # module.networking.aws_service_discovery_private_dns_namespace.internal will be created
  + resource "aws_service_discovery_private_dns_namespace" "internal" {
      + arn         = (known after apply)
      + description = "Private app-to-app DNS (app.yaml calls/provides)"
      + hosted_zone = (known after apply)
      + id          = (known after apply)
      + name        = "javabin.internal"
      + tags        = {
          + "Name" = "javabin.internal"
          + "team" = "shared"
        }
      + tags_all    = {
          + "Name"        = "javabin.internal"
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "shared"
        }
      + vpc         = "vpc-0cd3502de2527a310"
    }

Plan: 11 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 11 resources (a new app-peers-sync Lambda with IAM role, EventBridge rules, service discovery namespace) and updates 5 (developer boundary, three team guardrail policies, and one Lambda code update), with 0 destroys.

  • 🔒 [security] The developer permissions boundary and all three team guardrail policies gain servicediscovery:Delete*, servicediscovery:TagResource and servicediscovery:UntagResource. These are in the Action list of existing Deny-style statements (the diff shows changed Action arrays without Effect). Because the Sid/Effect is not shown for these statements, it must be confirmed in the full policy that these actions are being added to a Deny statement rather than an Allow. If they are Allow grants, developers could delete and tag service discovery resources, which weakens the boundary.
  • 🔒 [security] The new app_peers_sync Lambda role is bounded by javabin-org-boundary and has ec2:AuthorizeSecurityGroupIngress and ec2:RevokeSecurityGroupIngress, scoped by ec2:Vpc and a tag condition (javabin-network=app). It also has cognito-idp Create/Update/Delete on the user pool client and resource server, and SSM Put/Delete on javabin/apps///peer-* parameters. These are broad but reasonably scoped. Review the Cognito and SG permissions for least privilege. No internet-open SG rules are shown.
  • 🔒 [security] The Lambda's own permissions include ec2:RevokeSecurityGroupIngress and ssm:DeleteParameter(s). A bug or malicious app.yaml could remove ingress rules on app security groups or delete peer parameters. This is an operational risk, not a plan error.
  • 💥 [destruction] No resources are destroyed. The app_secret_rollout Lambda only has its code hash updated in place. Routine.
  • 💰 [cost] Net new cost is small: one Lambda with reserved concurrency 1 and 128 MB, daily and event-driven EventBridge rules, and a Cloud Map private DNS namespace (about USD 0.50/month per hosted zone plus query charges). No NAT gateways, RDS or load balancers are added.

Alexanderamiri and others added 2 commits October 24, 2026 15:46
The Cloud Map namespace creates a Route53 private hosted zone. Route53
ignores tag conditions, so DenyShared/DenyXTeam never applied to it: a team
could UPSERT another app's A record and capture M2M bearer tokens sent over
plain HTTP. DenyPlatInfra now denies route53:C*/D* (change, create, delete,
disassociate, disable) on that zone's ARN; reads stay allowed. The guardrail
copies get the same rule.

Bytes come from writing DenyPlatInfra's ECS/ELB/DynamoDB ARNs with *:*
region/account, as DenyBadName already does: 6,050 of 6,144.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…ate zone

- An app whose network request is invalid is frozen, not removed: its
  managed rules, resource server and client scopes stay while every valid
  side still declares the edge (the valid side can still withdraw). Peers
  see callee-invalid / caller-invalid.
- <APP>_SCOPES is now an SSM secret peer-{t}-{a}-scopes holding the scopes
  the caller's client really holds ("none" if none), so a callee granting
  fewer scopes no longer makes the caller's token request fail outright;
  a change restarts the caller via app-secret-rollout.
- Daily reconcile compares the private zone's records with Cloud Map
  instances.
- app-network: token_url uses an explicit internal_pool_domain variable.
- ecs-service: validate at most one service registry.
- Docs: network-only calls are not caller authentication, name squatting,
  first-deploy dependency on app-peers-sync, frozen state.

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

Copy link
Copy Markdown

Terraform Plan

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

Plan output
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_policy.developer_boundary will be updated in-place
  ~ resource "aws_iam_policy" "developer_boundary" {
        id               = "arn:aws:iam::553637109631:policy/javabin-developer-boundary"
        name             = "javabin-developer-boundary"
      ~ policy           = jsonencode(
            {
              - Statement = [
                  - {
                      - Action   = "*"
                      - Effect   = "Allow"
                      - Resource = "*"
                      - Sid      = "Allow"
                    },
                  - {
                      - Action   = [
                          - "iam:DeletePolicy",
                          - "iam:DeletePolicyVersion",
                          - "iam:CreatePolicyVersion",
                          - "iam:SetDefaultPolicyVersion",
                          - "iam:CreatePolicy",
                          - "iam:TagPolicy",
                          - "iam:UntagPolicy",
                        ]
                      - Effect   = "Deny"
                      - Resource = [
                          - "arn:aws:iam::553637109631:policy/javabin-developer-boundary",
                          - "arn:aws:iam::553637109631:policy/javabin-team-*-guardrails",
                        ]
                      - Sid      = "DenyBndMod"
                    },
                  - {
                      - Action    = [
                          - "iam:CreateRole",
                          - "iam:PutRolePermissionsBoundary",
                        ]
                      - Condition = {
                          - StringNotEquals = {
                              - "iam:PermissionsBoundary" = "arn:aws:iam::553637109631:policy/javabin-developer-boundary"
                            }
                        }
                      - Effect    = "Deny"
                      - Resource  = "*"
                      - Sid       = "DenyNoBnd"
                    },
                  - {
                      - Action   = "iam:DeleteRolePermissionsBoundary"
                      - Effect   = "Deny"
                      - Resource = "*"
                      - Sid      = "DenyStripBnd"
                    },
                  - {
                      - Action   = [
                          - "iam:CreateUser",
                          - "iam:CreateLoginProfile",
                          - "iam:UpdateLoginProfile",
                          - "iam:CreateAccessKey",
                          - "iam:DeactivateMFADevice",
                        ]
                      - Effect   = "Deny"
                      - Resource = "*"
                      - Sid      = "DenyIAMUsers"
                    },
                  - {
                      - Action   = [
                          - "organizations:*",
                          - "sso:*",
                          - "sso-directory:*",
                          - "identitystore:*",
                          - "account:*",
                        ]
                      - Effect   = "Deny"
                      - Resource = "*"
                      - Sid      = "DenyOrgSSO"
                    },
                  - {
                      - Action   = [
                          - "ec2:CreateVpc",
                          - "ec2:DeleteVpc",
                          - "ec2:ModifyVpcAttribute",
                          - "ec2:CreateSubnet",
                          - "ec2:DeleteSubnet",
                          - "ec2:CreateInternetGateway",
                          - "ec2:DeleteInternetGateway",
                          - "ec2:AttachInternetGateway",
                          - "ec2:DetachInternetGateway",
                          - "ec2:CreateNatGateway",
                          - "ec2:DeleteNatGateway",
                          - "ec2:CreateRouteTable",
                          - "ec2:DeleteRouteTable",
                        ]
                      - Effect   = "Deny"
                      - Resource = "*"
                      - Sid      = "DenyNetwork"
                    },
                  - {
                      - Action   = [
                          - "ecs:DeleteCluster",
                          - "ecs:UpdateCluster",
                          - "elasticloadbalancing:DeleteLoadBalancer",
                          - "elasticloadbalancing:ModifyLoadBalancerAttributes",
                          - "s3:DeleteBucket",
                          - "s3:PutBucketPolicy",
                          - "s3:DeleteBucketPolicy",
                          - "s3:PutBucketVersioning",
                          - "s3:PutEncryptionConfiguration",
                          - "dynamodb:DeleteTable",
                          - "dynamodb:UpdateTable",
                        ]
                      - Effect   = "Deny"
                      - Resource = [
                          - "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform",
                          - "arn:aws:elasticloadbalancing:eu-central-1:553637109631:loadbalancer/app/javabin-*",
                          - "arn:aws:s3:::javabin-terraform-*",
                          - "arn:aws:s3:::javabin-ci-*",
                          - "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-*",
                        ]
                      - Sid      = "DenyPlatInfra"
                    },
                  - {
                      - Action   = [
                          - "guardduty:*",
                          - "securityhub:*",
                          - "config:*",
                          - "cloudtrail:*",
                          - "bedrock:Invoke*",
                          - "bedrock:Converse*",
                        ]
                      - Effect   = "Deny"
                      - Resource = "*"
                      - Sid      = "DenySecSvcs"
                    },
                  - {
                      - Action      = "*"
                      - Condition   = {
                          - Null            = {
                              - "aws:ResourceTag/team" = "false"
                            }
                          - StringNotEquals = {
                              - "aws:ResourceTag/team" = [
                                  - "${aws:PrincipalTag/team}",
                                  - "shared",
                                ]
                            }
                        }
                      - Effect      = "Deny"
                      - NotResource = [
                          - "arn:aws:s3:::javabin-terraform-state-553637109631",
                          - "arn:aws:s3:::javabin-terraform-state-553637109631/*",
                          - "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks",
                          - "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631",
                          - "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*",
                        ]
                      - Sid         = "DenyXTeam"
                    },
                  - {
                      - Action    = [
                          - "ec2:Delete*",
                          - "ec2:Modify*",
                          - "elasticloadbalancing:Delete*",
                          - "elasticloadbalancing:Modify*",
                          - "ecs:DeleteCluster",
                          - "ecs:UpdateCluster",
                          - "ecs:UpdateClusterSettings",
                          - "iam:DeleteRole",
                          - "iam:UpdateRole",
                          - "iam:DeleteRolePolicy",
                          - "iam:UpdateAssumeRolePolicy",
                          - "iam:PutRolePolicy",
                          - "iam:AttachRolePolicy",
                          - "iam:DetachRolePolicy",
                          - "route53:DeleteHostedZone",
                          - "sns:DeleteTopic",
                          - "sns:SetTopicAttributes",
                          - "ec2:AuthorizeSecurityGroup*",
                          - "ec2:RevokeSecurityGroup*",
                        ]
                      - Condition = {
                          - StringEquals = {
                              - "aws:ResourceTag/team" = "shared"
                            }
                        }
                      - Effect    = "Deny"
                      - Resource  = "*"
                      - Sid       = "DenyShared"
                    },
                  - {
                      - Action    = [
                          - "ec2:DeleteTags",
                          - "ec2:CreateTags",
                          - "ecs:TagResource",
                          - "ecs:UntagResource",
                          - "elasticloadbalancing:AddTags",
                          - "elasticloadbalancing:RemoveTags",
                          - "iam:TagRole",
                          - "iam:UntagRole",
                          - "sns:TagResource",
                          - "sns:UntagResource",
                        ]
                      - Condition = {
                          - StringEquals = {
                              - "aws:ResourceTag/team" = [
                                  - "shared",
                                  - "platform",
                                ]
                            }
                        }
                      - Effect    = "Deny"
                      - Resource  = "*"
                      - Sid       = "DenyTagStrip"
                    },
                  - {
                      - Action   = [
                          - "ssm:GetParameter*",
                          - "ssm:GetParametersByPath",
                        ]
                      - Effect   = "Deny"
                      - Resource = [
                          - "arn:aws:ssm:*:*:parameter/javabin/platform/*",
                          - "arn:aws:ssm:*:*:parameter/javabin/slack/*",
                          - "arn:aws:ssm:*:*:parameter/javabin/platform-overrides/*",
                        ]
                      - Sid      = "DenyPlatSSM"
                    },
                  - {
                      - Action      = "iam:PassRole"
                      - Effect      = "Deny"
                      - NotResource = [
                          - "arn:aws:iam::*:role/${aws:PrincipalTag/team}-*",
                          - "arn:aws:iam::*:role/javabin-ecs-exec-${aws:PrincipalTag/team}",
                        ]
                      - Sid         = "DenyPassRole"
                    },
                  - {
                      - Action      = [
                          - "logs:GetLogEvents",
                          - "logs:FilterLogEvents",
                          - "logs:StartQuery",
                          - "logs:GetQueryResults",
                        ]
                      - Effect      = "Deny"
                      - NotResource = [
                          - "arn:aws:logs:*:*:log-group:/ecs/${aws:PrincipalTag/team}/*",
                          - "arn:aws:logs:*:*:log-group:/ecs/${aws:PrincipalTag/team}/*:*",
                        ]
                      - Sid         = "DenyXLogs"
                    },
                  - {
                      - Action   = [
                          - "iam:PutRolePolicy",
                          - "iam:DeleteRolePolicy",
                          - "iam:AttachRolePolicy",
                          - "iam:DetachRolePolicy",
                          - "iam:DeleteRole",
                          - "iam:UpdateRole",
                          - "iam:UpdateAssumeRolePolicy",
                        ]
                      - Effect   = "Deny"
                      - Resource = "arn:aws:iam::553637109631:role/javabin-ci-*"
                      - Sid      = "DenyModCI"
                    },
                  - {
                      - Action    = "ec2:CreateSecurityGroup"
                      - Condition = {
                          - StringNotEqualsIfExists = {
                              - "aws:RequestTag/team" = "${aws:PrincipalTag/team}"
                            }
                        }
                      - Effect    = "Deny"
                      - Resource  = "*"
                      - Sid       = "DenyNoTag"
                    },
                  - {
                      - Action      = [
                          - "s3:CreateBucket",
                          - "dynamodb:CreateTable",
                          - "sqs:CreateQueue",
                          - "sns:CreateTopic",
                          - "rds:CreateDBInstance",
                          - "rds:CreateDBSubnetGroup",
                          - "ecs:CreateService",
                          - "ecs:RegisterTaskDefinition",
                          - "ecr:CreateRepository",
                          - "logs:CreateLogGroup",
                          - "ssm:PutParameter",
                          - "lambda:CreateFunction",
                          - "events:PutRule",
                          - "states:CreateStateMachine",
                        ]
                      - Effect      = "Deny"
                      - NotResource = [
                          - "arn:aws:s3:::${aws:PrincipalTag/team}-*",
                          - "arn:aws:dynamodb:*:*:table/${aws:PrincipalTag/team}-*",
                          - "arn:aws:sqs:*:*:${aws:PrincipalTag/team}-*",
                          - "arn:aws:sns:*:*:${aws:PrincipalTag/team}-*",
                          - "arn:aws:rds:*:*:db:${aws:PrincipalTag/team}-*",
                          - "arn:aws:rds:*:*:subgrp:${aws:PrincipalTag/team}-*",
                          - "arn:aws:ecs:*:*:service/*/${aws:PrincipalTag/team}-*",
                          - "arn:aws:ecs:*:*:task-definition/${aws:PrincipalTag/team}-*:*",
                          - "arn:aws:ecr:*:*:repository/${aws:PrincipalTag/team}-*",
                          - "arn:aws:logs:*:*:log-group:/ecs/${aws:PrincipalTag/team}/*",
                          - "arn:aws:ssm:*:*:parameter/javabin/apps/${aws:PrincipalTag/team}/*",
                          - "arn:aws:lambda:*:*:function:${aws:PrincipalTag/team}-*",
                          - "arn:aws:events:*:*:rule/${aws:PrincipalTag/team}-*",
                          - "arn:aws:states:*:*:stateMachine:${aws:PrincipalTag/team}-*",
                        ]
                      - Sid         = "DenyBadName"
                    },
                ]
              - Version   = "2012-10-17"
            }
        ) -> (known after apply)
        tags             = {
            "Name" = "javabin-developer-boundary"
        }
        # (6 unchanged attributes hidden)
    }

  # module.iam.aws_iam_policy.team_guardrails["cupcake"] will be updated in-place
  ~ resource "aws_iam_policy" "team_guardrails" {
        id               = "arn:aws:iam::553637109631:policy/javabin-team-cupcake-guardrails"
        name             = "javabin-team-cupcake-guardrails"
      ~ policy           = jsonencode(
            {
              - Statement = [
                  - {
                      - Action   = "*"
                      - Effect   = "Allow"
                      - Resource = "*"
                      - Sid      = "Allow"
                    },
                  - {
                      - Action   = [
                          - "iam:DeletePolicy",
                          - "iam:DeletePolicyVersion",
                          - "iam:CreatePolicyVersion",
                          - "iam:SetDefaultPolicyVersion",
                          - "iam:CreatePolicy",
                          - "iam:TagPolicy",
                          - "iam:UntagPolicy",
                        ]
                      - Effect   = "Deny"
                      - Resource = [
                          - "arn:aws:iam::553637109631:policy/javabin-developer-boundary",
                          - "arn:aws:iam::553637109631:policy/javabin-team-*-guardrails",
                        ]
                      - Sid      = "DenyBndMod"
                    },
                  - {
                      - Action    = [
                          - "iam:CreateRole",
                          - "iam:PutRolePermissionsBoundary",
                        ]
                      - Condition = {
                          - StringNotEquals = {
                              - "iam:PermissionsBoundary" = "arn:aws:iam::553637109631:policy/javabin-developer-boundary"
                            }
                        }
                      - Effect    = "Deny"
                      - Resource  = "*"
                      - Sid       = "DenyNoBnd"
                    },
                  - {
                      - Action   = "iam:DeleteRolePermissionsBoundary"
                      - Effect   = "Deny"
                      - Resource = "*"
                      - Sid      = "DenyStripBnd"
                    },
                  - {
                      - Action   = [
                          - "iam:CreateUser",
                          - "iam:CreateLoginProfile",
                          - "iam:UpdateLoginProfile",
                          - "iam:CreateAccessKey",
                          - "iam:DeactivateMFADevice",
                        ]
                      - Effect   = "Deny"
                      - Resource = "*"
                      - Sid      = "DenyIAMUsers"
                    },
                  - {
                      - Action   = [
                          - "organizations:*",
                          - "sso:*",
                          - "sso-directory:*",
                          - "identitystore:*",
                          - "account:*",
                        ]
                      - Effect   = "Deny"
                      - Resource = "*"
                      - Sid      = "DenyOrgSSO"
                    },
                  - {
                      - Action   = [
                          - "ec2:CreateVpc",
                          - "ec2:DeleteVpc",
                          - "ec2:ModifyVpcAttribute",
                          - "ec2:CreateSubnet",
                          - "ec2:DeleteSubnet",
                          - "ec2:CreateInternetGateway",
                          - "ec2:DeleteInternetGateway",
                          - "ec2:AttachInternetGateway",
                          - "ec2:DetachInternetGateway",
                          - "ec2:CreateNatGateway",
                          - "ec2:DeleteNatGateway",
                          - "ec2:CreateRouteTable",
                          - "ec2:DeleteRouteTable",
                        ]
                      - Effect   = "Deny"
                      - Resource = "*"
                      - Sid      = "DenyNetwork"
                    },
                  - {
                      - Action   = [
                          - "ecs:DeleteCluster",
                          - "ecs:UpdateCluster",
                          - "elasticloadbalancing:DeleteLoadBalancer",
                          - "elasticloadbalancing:ModifyLoadBalancerAttributes",
                          - "s3:DeleteBucket",
                          - "s3:PutBucketPolicy",
                          - "s3:DeleteBucketPolicy",
                          - "s3:PutBucketVersioning",
                          - "s3:PutEncryptionConfiguration",
                          - "dynamodb:DeleteTable",
                          - "dynamodb:UpdateTable",
                        ]
                      - Effect   = "Deny"
                      - Resource = [
                          - "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform",
                          - "arn:aws:elasticloadbalancing:eu-central-1:553637109631:loadbalancer/app/javabin-*",
                          - "arn:aws:s3:::javabin-terraform-*",
                          - "arn:aws:s3:::javabin-ci-*",
                          - "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-*",
                        ]
                      - Sid      = "DenyPlatInfra"
                    },
                  - {
                      - Action   = [
                          - "guardduty:*",
                          - "securityhub:*",
                          - "config:*",
                          - "cloudtrail:*",
                          - "bedrock:Invoke*",
                          - "bedrock:Converse*",
                        ]
                      - Effect   = "Deny"
                      - Resource = "*"
                      - Sid      = "DenySecSvcs"
                    },
                  - {
                      - Action      = "*"
                      - Condition   = {
                          - Null            = {
                              - "aws:ResourceTag/team" = "false"
                            }
                          - StringNotEquals = {
                              - "aws:ResourceTag/team" = [
                                  - "cupcake",
                                  - "shared",
                                ]
                            }
                        }
                      - Effect      = "Deny"
                      - NotResource = [
                          - "arn:aws:s3:::javabin-terraform-state-553637109631",
                          - "arn:aws:s3:::javabin-terraform-state-553637109631/*",
                          - "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks",
                          - "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631",
                          - "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*",
                        ]
                      - Sid         = "DenyXTeam"
                    },
                  - {
                      - Action    = [
                          - "ec2:Delete*",
                          - "ec2:Modify*",
                          - "elasticloadbalancing:Delete*",
                          - "elasticloadbalancing:Modify*",
                          - "ecs:DeleteCluster",
                          - "ecs:UpdateCluster",
                          - "ecs:UpdateClusterSettings",
                          - "iam:DeleteRole",
                          - "iam:UpdateRole",
                          - "iam:DeleteRolePolicy",
                          - "iam:UpdateAssumeRolePolicy",
                          - "iam:PutRolePolicy",
                          - "iam:AttachRolePolicy",
                          - "iam:DetachRolePolicy",
                          - "route53:DeleteHostedZone",
                          - "sns:DeleteTopic",
                          - "sns:SetTopicAttributes",
                          - "ec2:AuthorizeSecurityGroup*",
                          - "ec2:RevokeSecurityGroup*",
                        ]
                      - Condition = {
                          - StringEquals = {
                              - "aws:ResourceTag/team" = "shared"
                            }
                        }
                      - Effect    = "Deny"
                      - Resource  = "*"
                      - Sid       = "DenyShared"
                    },
                  - {
                      - Action    = [
                          - "ec2:DeleteTags",
                          - "ec2:CreateTags",
                          - "ecs:TagResource",
                          - "ecs:UntagResource",
                          - "elasticloadbalancing:AddTags",
                          - "elasticloadbalancing:RemoveTags",
                          - "iam:TagRole",
                          - "iam:UntagRole",
                          - "sns:TagResource",
                          - "sns:UntagResource",
                        ]
                      - Condition = {
                          - StringEquals = {
                              - "aws:ResourceTag/team" = [
                                  - "shared",
                                  - "platform",
                                ]
                            }
                        }
                      - Effect    = "Deny"
                      - Resource  = "*"
                      - Sid       = "DenyTagStrip"
                    },
                  - {
                      - Action   = [
                          - "ssm:GetParameter*",
                          - "ssm:GetParametersByPath",
                        ]
                      - Effect   = "Deny"
                      - Resource = [
                          - "arn:aws:ssm:*:*:parameter/javabin/platform/*",
                          - "arn:aws:ssm:*:*:parameter/javabin/slack/*",
                          - "arn:aws:ssm:*:*:parameter/javabin/platform-overrides/*",
                        ]
                      - Sid      = "DenyPlatSSM"
                    },
                  - {
                      - Action      = "iam:PassRole"
                      - Effect      = "Deny"
                      - NotResource = [
                          - "arn:aws:iam::*:role/cupcake-*",
                          - "arn:aws:iam::*:role/javabin-ecs-exec-cupcake",
                        ]
                      - Sid         = "DenyPassRole"
                    },
                  - {
                      - Action      = [
                          - "logs:GetLogEvents",
                          - "logs:FilterLogEvents",
                          - "logs:StartQuery",
                          - "logs:GetQueryResults",
                        ]
                      - Effect      = "Deny"
                      - NotResource = [
                          - "arn:aws:logs:*:*:log-group:/ecs/cupcake/*",
                          - "arn:aws:logs:*:*:log-group:/ecs/cupcake/*:*",
                        ]
                      - Sid         = "DenyXLogs"
                    },
                  - {
                      - Action   = [
                          - "iam:PutRolePolicy",
                          - "iam:DeleteRolePolicy",
                          - "iam:AttachRolePolicy",
                          - "iam:DetachRolePolicy",
                          - "iam:DeleteRole",
                          - "iam:UpdateRole",
                          - "iam:UpdateAssumeRolePolicy",
                        ]
                      - Effect   = "Deny"
                      - Resource = "arn:aws:iam::553637109631:role/javabin-ci-*"
                      - Sid      = "DenyModCI"
                    },
                  - {
                      - Action    = "ec2:CreateSecurityGroup"
                      - Condition = {
                          - StringNotEqualsIfExists = {
                              - "aws:RequestTag/team" = "cupcake"
                            }
                        }
                      - Effect    = "Deny"
                      - Resource  = "*"
                      - Sid       = "DenyNoTag"
                    },
                  - {
                      - Action      = [
                          - "s3:CreateBucket",
                          - "dynamodb:CreateTable",
                          - "sqs:CreateQueue",
                          - "sns:CreateTopic",
                          - "rds:CreateDBInstance",
                          - "rds:CreateDBSubnetGroup",
                          - "ecs:CreateService",
                          - "ecs:RegisterTaskDefinition",
                          - "ecr:CreateRepository",
                          - "logs:CreateLogGroup",
                          - "ssm:PutParameter",
                          - "lambda:CreateFunction",
                          - "events:PutRule",
                          - "states:CreateStateMachine",
                        ]
                      - Effect      = "Deny"
                      - NotResource = [
                          - "arn:aws:s3:::cupcake-*",
                          - "arn:aws:dynamodb:*:*:table/cupcake-*",
                          - "arn:aws:sqs:*:*:cupcake-*",
                          - "arn:aws:sns:*:*:cupcake-*",
                          - "arn:aws:rds:*:*:db:cupcake-*",
                          - "arn:aws:rds:*:*:subgrp:cupcake-*",
                          - "arn:aws:ecs:*:*:service/*/cupcake-*",
                          - "arn:aws:ecs:*:*:task-definition/cupcake-*:*",
                          - "arn:aws:ecr:*:*:repository/cupcake-*",
                          - "arn:aws:logs:*:*:log-group:/ecs/cupcake/*",
                          - "arn:aws:ssm:*:*:parameter/javabin/apps/cupcake/*",
                          - "arn:aws:lambda:*:*:function:cupcake-*",
                          - "arn:aws:events:*:*:rule/cupcake-*",
                          - "arn:aws:states:*:*:stateMachine:cupcake-*",
                        ]
                      - Sid         = "DenyBadName"
                    },
                ]
              - Version   = "2012-10-17"
            }
        ) -> (known after apply)
        tags             = {
            "Name"     = "javabin-team-cupcake-guardrails"
            "for-team" = "cupcake"
            "team"     = "shared"
        }
        # (6 unchanged attributes hidden)
    }

  # module.iam.aws_iam_policy.team_guardrails["jzwatch"] will be updated in-place
  ~ resource "aws_iam_policy" "team_guardrails" {
        id               = "arn:aws:iam::553637109631:policy/javabin-team-jzwatch-guardrails"
        name             = "javabin-team-jzwatch-guardrails"
      ~ policy           = jsonencode(
            {
              - Statement = [
                  - {
                      - Action   = "*"
                      - Effect   = "Allow"
                      - Resource = "*"
                      - Sid      = "Allow"
                    },
                  - {
                      - Action   = [
                          - "iam:DeletePolicy",
                          - "iam:DeletePolicyVersion",
                          - "iam:CreatePolicyVersion",
                          - "iam:SetDefaultPolicyVersion",
                          - "iam:CreatePolicy",
                          - "iam:TagPolicy",
                          - "iam:UntagPolicy",
                        ]
                      - Effect   = "Deny"
                      - Resource = [
                          - "arn:aws:iam::553637109631:policy/javabin-developer-boundary",
                          - "arn:aws:iam::553637109631:policy/javabin-team-*-guardrails",
                        ]
                      - Sid      = "DenyBndMod"
                    },
                  - {
                      - Action    = [
                          - "iam:CreateRole",
                          - "iam:PutRolePermissionsBoundary",
                        ]
                      - Condition = {
                          - StringNotEquals = {
                              - "iam:PermissionsBoundary" = "arn:aws:iam::553637109631:policy/javabin-developer-boundary"
                            }
                        }
                      - Effect    = "Deny"
                      - Resource  = "*"
                      - Sid       = "DenyNoBnd"
                    },
                  - {
                      - Action   = "iam:DeleteRolePermissionsBoundary"
                      - Effect   = "Deny"
                      - Resource = "*"
                      - Sid      = "DenyStripBnd"
                    },
                  - {
                      - Action   = [
                          - "iam:CreateUser",
                          - "iam:CreateLoginProfile",
                          - "iam:UpdateLoginProfile",
                          - "iam:CreateAccessKey",
                          - "iam:DeactivateMFADevice",
                        ]
                      - Effect   = "Deny"
                      - Resource = "*"
                      - Sid      = "DenyIAMUsers"
                    },
                  - {
                      - Action   = [
                          - "organizations:*",
                          - "sso:*",
                          - "sso-directory:*",
                          - "identitystore:*",
                          - "account:*",
                        ]
                      - Effect   = "Deny"
                      - Resource = "*"
                      - Sid      = "DenyOrgSSO"
                    },
                  - {
                      - Action   = [
                          - "ec2:CreateVpc",
                          - "ec2:DeleteVpc",
                          - "ec2:ModifyVpcAttribute",
                          - "ec2:CreateSubnet",
                          - "ec2:DeleteSubnet",
                          - "ec2:CreateInternetGateway",
                          - "ec2:DeleteInternetGateway",
                          - "ec2:AttachInternetGateway",
                          - "ec2:DetachInternetGateway",
                          - "ec2:CreateNatGateway",
                          - "ec2:DeleteNatGateway",
                          - "ec2:CreateRouteTable",
                          - "ec2:DeleteRouteTable",
                        ]
                      - Effect   = "Deny"
                      - Resource = "*"
                      - Sid      = "DenyNetwork"
                    },
                  - {
                      - Action   = [
                          - "ecs:DeleteCluster",
                          - "ecs:UpdateCluster",
                          - "elasticloadbalancing:DeleteLoadBalancer",
                          - "elasticloadbalancing:ModifyLoadBalancerAttributes",
                          - "s3:DeleteBucket",
                          - "s3:PutBucketPolicy",
                          - "s3:DeleteBucketPolicy",
                          - "s3:PutBucketVersioning",
                          - "s3:PutEncryptionConfiguration",
                          - "dynamodb:DeleteTable",
                          - "dynamodb:UpdateTable",
                        ]
                      - Effect   = "Deny"
                      - Resource = [
                          - "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform",
                          - "arn:aws:elasticloadbalancing:eu-central-1:553637109631:loadbalancer/app/javabin-*",
                          - "arn:aws:s3:::javabin-terraform-*",
                          - "arn:aws:s3:::javabin-ci-*",
                          - "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-*",
                        ]
                      - Sid      = "DenyPlatInfra"
                    },
                  - {
                      - Action   = [
                          - "guardduty:*",
                          - "securityhub:*",
                          - "config:*",
                          - "cloudtrail:*",
                          - "bedrock:Invoke*",
                          - "bedrock:Converse*",
                        ]
                      - Effect   = "Deny"
                      - Resource = "*"
                      - Sid      = "DenySecSvcs"
                    },
                  - {
                      - Action      = "*"
                      - Condition   = {
                          - Null            = {
                              - "aws:ResourceTag/team" = "false"
                            }
                          - StringNotEquals = {
                              - "aws:ResourceTag/team" = [
                                  - "jzwatch",
                                  - "shared",
                                ]
                            }
                        }
                      - Effect      = "Deny"
                      - NotResource = [
                          - "arn:aws:s3:::javabin-terraform-state-553637109631",
                          - "arn:aws:s3:::javabin-terraform-state-553637109631/*",
                          - "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks",
                          - "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631",
                          - "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*",
                        ]
                      - Sid         = "DenyXTeam"
                    },
                  - {
                      - Action    = [
                          - "ec2:Delete*",
                          - "ec2:Modify*",
                          - "elasticloadbalancing:Delete*",
                          - "elasticloadbalancing:Modify*",
                          - "ecs:DeleteCluster",
                          - "ecs:UpdateCluster",
                          - "ecs:UpdateClusterSettings",
                          - "iam:DeleteRole",
                          - "iam:UpdateRole",
                          - "iam:DeleteRolePolicy",
                          - "iam:UpdateAssumeRolePolicy",
                          - "iam:PutRolePolicy",
                          - "iam:AttachRolePolicy",
                          - "iam:DetachRolePolicy",
                          - "route53:DeleteHostedZone",
                          - "sns:DeleteTopic",
                          - "sns:SetTopicAttributes",
                          - "ec2:AuthorizeSecurityGroup*",
                          - "ec2:RevokeSecurityGroup*",
                        ]
                      - Condition = {
                          - StringEquals = {
                              - "aws:ResourceTag/team" = "shared"
                            }
                        }
                      - Effect    = "Deny"
                      - Resource  = "*"
                      - Sid       = "DenyShared"
                    },
                  - {
                      - Action    = [
                          - "ec2:DeleteTags",
                          - "ec2:CreateTags",
                          - "ecs:TagResource",
                          - "ecs:UntagResource",
                          - "elasticloadbalancing:AddTags",
                          - "elasticloadbalancing:RemoveTags",
                          - "iam:TagRole",
                          - "iam:UntagRole",
                          - "sns:TagResource",
                          - "sns:UntagResource",
                        ]
                      - Condition = {
                          - StringEquals = {
                              - "aws:ResourceTag/team" = [
                                  - "shared",
                                  - "platform",
                                ]
                            }
                        }
                      - Effect    = "Deny"
                      - Resource  = "*"
                      - Sid       = "DenyTagStrip"
                    },
                  - {
                      - Action   = [
                          - "ssm:GetParameter*",
                          - "ssm:GetParametersByPath",
                        ]
                      - Effect   = "Deny"
                      - Resource = [
                          - "arn:aws:ssm:*:*:parameter/javabin/platform/*",
                          - "arn:aws:ssm:*:*:parameter/javabin/slack/*",
                          - "arn:aws:ssm:*:*:parameter/javabin/platform-overrides/*",
                        ]
                      - Sid      = "DenyPlatSSM"
                    },
                  - {
                      - Action      = "iam:PassRole"
                      - Effect      = "Deny"
                      - NotResource = [
                          - "arn:aws:iam::*:role/jzwatch-*",
                          - "arn:aws:iam::*:role/javabin-ecs-exec-jzwatch",
                        ]
                      - Sid         = "DenyPassRole"
                    },
                  - {
                      - Action      = [
                          - "logs:GetLogEvents",
                          - "logs:FilterLogEvents",
                          - "logs:StartQuery",
                          - "logs:GetQueryResults",
                        ]
                      - Effect      = "Deny"
                      - NotResource = [
                          - "arn:aws:logs:*:*:log-group:/ecs/jzwatch/*",
                          - "arn:aws:logs:*:*:log-group:/ecs/jzwatch/*:*",
                        ]
                      - Sid         = "DenyXLogs"
                    },
                  - {
                      - Action   = [
                          - "iam:PutRolePolicy",
                          - "iam:DeleteRolePolicy",
                          - "iam:AttachRolePolicy",
                          - "iam:DetachRolePolicy",
                          - "iam:DeleteRole",
                          - "iam:UpdateRole",
                          - "iam:UpdateAssumeRolePolicy",
                        ]
                      - Effect   = "Deny"
                      - Resource = "arn:aws:iam::553637109631:role/javabin-ci-*"
                      - Sid      = "DenyModCI"
                    },
                  - {
                      - Action    = "ec2:CreateSecurityGroup"
                      - Condition = {
                          - StringNotEqualsIfExists = {
                              - "aws:RequestTag/team" = "jzwatch"
                            }
                        }
                      - Effect    = "Deny"
                      - Resource  = "*"
                      - Sid       = "DenyNoTag"
                    },
                  - {
                      - Action      = [
                          - "s3:CreateBucket",
                          - "dynamodb:CreateTable",
                          - "sqs:CreateQueue",
                          - "sns:CreateTopic",
                          - "rds:CreateDBInstance",
                          - "rds:CreateDBSubnetGroup",
                          - "ecs:CreateService",
                          - "ecs:RegisterTaskDefinition",
                          - "ecr:CreateRepository",
                          - "logs:CreateLogGroup",
                          - "ssm:PutParameter",
                          - "lambda:CreateFunction",
                          - "events:PutRule",
                          - "states:CreateStateMachine",
                        ]
                      - Effect      = "Deny"
                      - NotResource = [
                          - "arn:aws:s3:::jzwatch-*",
                          - "arn:aws:dynamodb:*:*:table/jzwatch-*",
                          - "arn:aws:sqs:*:*:jzwatch-*",
                          - "arn:aws:sns:*:*:jzwatch-*",
                          - "arn:aws:rds:*:*:db:jzwatch-*",
                          - "arn:aws:rds:*:*:subgrp:jzwatch-*",
                          - "arn:aws:ecs:*:*:service/*/jzwatch-*",
                          - "arn:aws:ecs:*:*:task-definition/jzwatch-*:*",
                          - "arn:aws:ecr:*:*:repository/jzwatch-*",
                          - "arn:aws:logs:*:*:log-group:/ecs/jzwatch/*",
                          - "arn:aws:ssm:*:*:parameter/javabin/apps/jzwatch/*",
                          - "arn:aws:lambda:*:*:function:jzwatch-*",
                          - "arn:aws:events:*:*:rule/jzwatch-*",
                          - "arn:aws:states:*:*:stateMachine:jzwatch-*",
                        ]
                      - Sid         = "DenyBadName"
                    },
                ]
              - Version   = "2012-10-17"
            }
        ) -> (known after apply)
        tags             = {
            "Name"     = "javabin-team-jzwatch-guardrails"
            "for-team" = "jzwatch"
            "team"     = "shared"
        }
        # (6 unchanged attributes hidden)
    }

  # module.iam.aws_iam_policy.team_guardrails["testteam"] will be updated in-place
  ~ resource "aws_iam_policy" "team_guardrails" {
        id               = "arn:aws:iam::553637109631:policy/javabin-team-testteam-guardrails"
        name             = "javabin-team-testteam-guardrails"
      ~ policy           = jsonencode(
            {
              - Statement = [
                  - {
                      - Action   = "*"
                      - Effect   = "Allow"
                      - Resource = "*"
                      - Sid      = "Allow"
                    },
                  - {
                      - Action   = [
                          - "iam:DeletePolicy",
                          - "iam:DeletePolicyVersion",
                          - "iam:CreatePolicyVersion",
                          - "iam:SetDefaultPolicyVersion",
                          - "iam:CreatePolicy",
                          - "iam:TagPolicy",
                          - "iam:UntagPolicy",
                        ]
                      - Effect   = "Deny"
                      - Resource = [
                          - "arn:aws:iam::553637109631:policy/javabin-developer-boundary",
                          - "arn:aws:iam::553637109631:policy/javabin-team-*-guardrails",
                        ]
                      - Sid      = "DenyBndMod"
                    },
                  - {
                      - Action    = [
                          - "iam:CreateRole",
                          - "iam:PutRolePermissionsBoundary",
                        ]
                      - Condition = {
                          - StringNotEquals = {
                              - "iam:PermissionsBoundary" = "arn:aws:iam::553637109631:policy/javabin-developer-boundary"
                            }
                        }
                      - Effect    = "Deny"
                      - Resource  = "*"
                      - Sid       = "DenyNoBnd"
                    },
                  - {
                      - Action   = "iam:DeleteRolePermissionsBoundary"
                      - Effect   = "Deny"
                      - Resource = "*"
                      - Sid      = "DenyStripBnd"
                    },
                  - {
                      - Action   = [
                          - "iam:CreateUser",
                          - "iam:CreateLoginProfile",
                          - "iam:UpdateLoginProfile",
                          - "iam:CreateAccessKey",
                          - "iam:DeactivateMFADevice",
                        ]
                      - Effect   = "Deny"
                      - Resource = "*"
                      - Sid      = "DenyIAMUsers"
                    },
                  - {
                      - Action   = [
                          - "organizations:*",
                          - "sso:*",
                          - "sso-directory:*",
                          - "identitystore:*",
                          - "account:*",
                        ]
                      - Effect   = "Deny"
                      - Resource = "*"
                      - Sid      = "DenyOrgSSO"
                    },
                  - {
                      - Action   = [
                          - "ec2:CreateVpc",
                          - "ec2:DeleteVpc",
                          - "ec2:ModifyVpcAttribute",
                          - "ec2:CreateSubnet",
                          - "ec2:DeleteSubnet",
                          - "ec2:CreateInternetGateway",
                          - "ec2:DeleteInternetGateway",
                          - "ec2:AttachInternetGateway",
                          - "ec2:DetachInternetGateway",
                          - "ec2:CreateNatGateway",
                          - "ec2:DeleteNatGateway",
                          - "ec2:CreateRouteTable",
                          - "ec2:DeleteRouteTable",
                        ]
                      - Effect   = "Deny"
                      - Resource = "*"
                      - Sid      = "DenyNetwork"
                    },
                  - {
                      - Action   = [
                          - "ecs:DeleteCluster",
                          - "ecs:UpdateCluster",
                          - "elasticloadbalancing:DeleteLoadBalancer",
                          - "elasticloadbalancing:ModifyLoadBalancerAttributes",
                          - "s3:DeleteBucket",
                          - "s3:PutBucketPolicy",
                          - "s3:DeleteBucketPolicy",
                          - "s3:PutBucketVersioning",
                          - "s3:PutEncryptionConfiguration",
                          - "dynamodb:DeleteTable",
                          - "dynamodb:UpdateTable",
                        ]
                      - Effect   = "Deny"
                      - Resource = [
                          - "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform",
                          - "arn:aws:elasticloadbalancing:eu-central-1:553637109631:loadbalancer/app/javabin-*",
                          - "arn:aws:s3:::javabin-terraform-*",
                          - "arn:aws:s3:::javabin-ci-*",
                          - "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-*",
                        ]
                      - Sid      = "DenyPlatInfra"
                    },
                  - {
                      - Action   = [
                          - "guardduty:*",
                          - "securityhub:*",
                          - "config:*",
                          - "cloudtrail:*",
                          - "bedrock:Invoke*",
                          - "bedrock:Converse*",
                        ]
                      - Effect   = "Deny"
                      - Resource = "*"
                      - Sid      = "DenySecSvcs"
                    },
                  - {
                      - Action      = "*"
                      - Condition   = {
                          - Null            = {
                              - "aws:ResourceTag/team" = "false"
                            }
                          - StringNotEquals = {
                              - "aws:ResourceTag/team" = [
                                  - "testteam",
                                  - "shared",
                                ]
                            }
                        }
                      - Effect      = "Deny"
                      - NotResource = [
                          - "arn:aws:s3:::javabin-terraform-state-553637109631",
                          - "arn:aws:s3:::javabin-terraform-state-553637109631/*",
                          - "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks",
                          - "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631",
                          - "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*",
                        ]
                      - Sid         = "DenyXTeam"
                    },
                  - {
                      - Action    = [
                          - "ec2:Delete*",
                          - "ec2:Modify*",
                          - "elasticloadbalancing:Delete*",
                          - "elasticloadbalancing:Modify*",
                          - "ecs:DeleteCluster",
                          - "ecs:UpdateCluster",
                          - "ecs:UpdateClusterSettings",
                          - "iam:DeleteRole",
                          - "iam:UpdateRole",
                          - "iam:DeleteRolePolicy",
                          - "iam:UpdateAssumeRolePolicy",
                          - "iam:PutRolePolicy",
                          - "iam:AttachRolePolicy",
                          - "iam:DetachRolePolicy",
                          - "route53:DeleteHostedZone",
                          - "sns:DeleteTopic",
                          - "sns:SetTopicAttributes",
                          - "ec2:AuthorizeSecurityGroup*",
                          - "ec2:RevokeSecurityGroup*",
                        ]
                      - Condition = {
                          - StringEquals = {
                              - "aws:ResourceTag/team" = "shared"
                            }
                        }
                      - Effect    = "Deny"
                      - Resource  = "*"
                      - Sid       = "DenyShared"
                    },
                  - {
                      - Action    = [
                          - "ec2:DeleteTags",
                          - "ec2:CreateTags",
                          - "ecs:TagResource",
                          - "ecs:UntagResource",
                          - "elasticloadbalancing:AddTags",
                          - "elasticloadbalancing:RemoveTags",
                          - "iam:TagRole",
                          - "iam:UntagRole",
                          - "sns:TagResource",
                          - "sns:UntagResource",
                        ]
                      - Condition = {
                          - StringEquals = {
                              - "aws:ResourceTag/team" = [
                                  - "shared",
                                  - "platform",
                                ]
                            }
                        }
                      - Effect    = "Deny"
                      - Resource  = "*"
                      - Sid       = "DenyTagStrip"
                    },
                  - {
                      - Action   = [
                          - "ssm:GetParameter*",
                          - "ssm:GetParametersByPath",
                        ]
                      - Effect   = "Deny"
                      - Resource = [
                          - "arn:aws:ssm:*:*:parameter/javabin/platform/*",
                          - "arn:aws:ssm:*:*:parameter/javabin/slack/*",
                          - "arn:aws:ssm:*:*:parameter/javabin/platform-overrides/*",
                        ]
                      - Sid      = "DenyPlatSSM"
                    },
                  - {
                      - Action      = "iam:PassRole"
                      - Effect      = "Deny"
                      - NotResource = [
                          - "arn:aws:iam::*:role/testteam-*",
                          - "arn:aws:iam::*:role/javabin-ecs-exec-testteam",
                        ]
                      - Sid         = "DenyPassRole"
                    },
                  - {
                      - Action      = [
                          - "logs:GetLogEvents",
                          - "logs:FilterLogEvents",
                          - "logs:StartQuery",
                          - "logs:GetQueryResults",
                        ]
                      - Effect      = "Deny"
                      - NotResource = [
                          - "arn:aws:logs:*:*:log-group:/ecs/testteam/*",
                          - "arn:aws:logs:*:*:log-group:/ecs/testteam/*:*",
                        ]
                      - Sid         = "DenyXLogs"
                    },
                  - {
                      - Action   = [
                          - "iam:PutRolePolicy",
                          - "iam:DeleteRolePolicy",
                          - "iam:AttachRolePolicy",
                          - "iam:DetachRolePolicy",
                          - "iam:DeleteRole",
                          - "iam:UpdateRole",
                          - "iam:UpdateAssumeRolePolicy",
                        ]
                      - Effect   = "Deny"
                      - Resource = "arn:aws:iam::553637109631:role/javabin-ci-*"
                      - Sid      = "DenyModCI"
                    },
                  - {
                      - Action    = "ec2:CreateSecurityGroup"
                      - Condition = {
                          - StringNotEqualsIfExists = {
                              - "aws:RequestTag/team" = "testteam"
                            }
                        }
                      - Effect    = "Deny"
                      - Resource  = "*"
                      - Sid       = "DenyNoTag"
                    },
                  - {
                      - Action      = [
                          - "s3:CreateBucket",
                          - "dynamodb:CreateTable",
                          - "sqs:CreateQueue",
                          - "sns:CreateTopic",
                          - "rds:CreateDBInstance",
                          - "rds:CreateDBSubnetGroup",
                          - "ecs:CreateService",
                          - "ecs:RegisterTaskDefinition",
                          - "ecr:CreateRepository",
                          - "logs:CreateLogGroup",
                          - "ssm:PutParameter",
                          - "lambda:CreateFunction",
                          - "events:PutRule",
                          - "states:CreateStateMachine",
                        ]
                      - Effect      = "Deny"
                      - NotResource = [
                          - "arn:aws:s3:::testteam-*",
                          - "arn:aws:dynamodb:*:*:table/testteam-*",
                          - "arn:aws:sqs:*:*:testteam-*",
                          - "arn:aws:sns:*:*:testteam-*",
                          - "arn:aws:rds:*:*:db:testteam-*",
                          - "arn:aws:rds:*:*:subgrp:testteam-*",
                          - "arn:aws:ecs:*:*:service/*/testteam-*",
                          - "arn:aws:ecs:*:*:task-definition/testteam-*:*",
                          - "arn:aws:ecr:*:*:repository/testteam-*",
                          - "arn:aws:logs:*:*:log-group:/ecs/testteam/*",
                          - "arn:aws:ssm:*:*:parameter/javabin/apps/testteam/*",
                          - "arn:aws:lambda:*:*:function:testteam-*",
                          - "arn:aws:events:*:*:rule/testteam-*",
                          - "arn:aws:states:*:*:stateMachine:testteam-*",
                        ]
                      - Sid         = "DenyBadName"
                    },
                ]
              - Version   = "2012-10-17"
            }
        ) -> (known after apply)
        tags             = {
            "Name"     = "javabin-team-testteam-guardrails"
            "for-team" = "testteam"
            "team"     = "shared"
        }
        # (6 unchanged attributes hidden)
    }

  # module.lambdas.aws_cloudwatch_event_rule.app_peers_sync_reconcile will be created
  + resource "aws_cloudwatch_event_rule" "app_peers_sync_reconcile" {
      + arn                 = (known after apply)
      + description         = "Daily reconcile and audit of app-to-app calls"
      + event_bus_name      = "default"
      + force_destroy       = false
      + id                  = (known after apply)
      + name                = "javabin-app-peers-sync-reconcile"
      + name_prefix         = (known after apply)
      + schedule_expression = "cron(45 3 * * ? *)"
      + tags_all            = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_event_rule.app_peers_sync_trigger will be created
  + resource "aws_cloudwatch_event_rule" "app_peers_sync_trigger" {
      + arn            = (known after apply)
      + description    = "app.yaml calls/provides request created, changed or deleted"
      + event_bus_name = "default"
      + event_pattern  = jsonencode(
            

✂️ Plan output above is cut at 60000 of 68779 characters (GitHub comment size limit). The full plan is in the workflow log; the LLM review below is not affected by this cut.


LLM Review

Risk: 🔴 HIGH

Plan creates 11 resources (Lambda app-peers-sync with EventBridge rules, a private DNS namespace, and IAM role/policy), updates 5 IAM policies (developer boundary and team guardrails) plus one Lambda code update, and destroys nothing; the IAM policy changes are the main concern.

  • 🔒 [security] The developer boundary and all four team guardrail policies are rewritten in place, and the diff shows the entire old statement set as removed ("- Statement"), including the Allow * statement and all Deny statements. The new policy content is (known after apply), so the net effect cannot be verified from this plan. Because the policies are guardrails for the boundary, this needs explicit review: confirm the new JSON is at least as restrictive, and note that the developer boundary is also attached to developer roles and the team guardrails govern their permissions. This is the reason for the HIGH rating.
  • 🔒 [security] The new Lambda role javabin-app-peers-sync is created with permissions_boundary set to javabin-org-boundary, which is good. Its inline policy content is (known after apply), so the granted actions cannot be verified from the plan. The app_secret_rollout Lambda code is also changing (source_code_hash), which is an unreviewed code deployment.
  • 🔒 [security] The app-peers-sync Lambda is wired to EventBridge rules that react to SSM Parameter Store changes under /javabin/apps///network. Its code can change network-related configuration, so review the handler and the IAM role's inline policy.
  • 💰 [cost] Costs are low: Lambda with reserved concurrency 1 and 128 MB, two CloudWatch event rules, and a Cloud Map private DNS namespace (small hosted zone fee). No NAT gateways, RDS, or load balancers are created.
  • ✅ [routine] New resources: Lambda javabin-app-peers-sync (python3.12), its IAM role and policy attachment, EventBridge rules for a daily 03:45 reconcile and an SSM-change trigger, targets and Lambda permissions, and service discovery namespace javabin.internal bound to vpc-0cd3502de2527a310. Nothing is destroyed.

Denying by the zone's ARN made the whole developer boundary and every team
guardrail "known after apply" (the zone ID only exists after apply), so the
plan review could not see the boundary at all. Use what is known at plan
time instead:

- DenyAppDNS: route53:ChangeResourceRecordSets where any record name
  matches *javabin.internal (Route53 ignores tags; record names are the
  supported condition key). Verified with the IAM policy simulator:
  denied for <app>.javabin.internal and the apex, allowed for
  testteam.java.no.
- DenyNetwork: route53:DeleteHostedZone everywhere (teams only write
  records, zones are platform-managed). Disassociating the zone's only VPC
  is impossible for a private zone.
- Bytes: ec2:*InternetGateway / ec2:*NatGateway replace six explicit
  actions (also denies egress-only IGWs). 6,067 of 6,144.
- app-peers-sync may list records in any zone (read-only), so its policy is
  readable in the plan too.

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

Copy link
Copy Markdown

Terraform Plan

🚧 Changes detected — Plan: 11 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_policy.developer_boundary will be updated in-place
  ~ resource "aws_iam_policy" "developer_boundary" {
        id               = "arn:aws:iam::553637109631:policy/javabin-developer-boundary"
        name             = "javabin-developer-boundary"
      ~ policy           = jsonencode(
          ~ {
              ~ Statement = [
                    # (5 unchanged elements hidden)
                    {
                        Action   = [
                            "organizations:*",
                            "sso:*",
                            "sso-directory:*",
                            "identitystore:*",
                            "account:*",
                        ]
                        Effect   = "Deny"
                        Resource = "*"
                        Sid      = "DenyOrgSSO"
                    },
                  ~ {
                      ~ Action   = [
                            # (4 unchanged elements hidden)
                            "ec2:DeleteSubnet",
                          - "ec2:CreateInternetGateway",
                          - "ec2:DeleteInternetGateway",
                          - "ec2:AttachInternetGateway",
                          - "ec2:DetachInternetGateway",
                          - "ec2:CreateNatGateway",
                          - "ec2:DeleteNatGateway",
                          + "ec2:*InternetGateway",
                          + "ec2:*NatGateway",
                            "ec2:CreateRouteTable",
                            "ec2:DeleteRouteTable",
                          + "route53:DeleteHostedZone",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Action    = [
                          - "ecs:DeleteCluster",
                          - "ecs:UpdateCluster",
                          - "elasticloadbalancing:DeleteLoadBalancer",
                          - "elasticloadbalancing:ModifyLoadBalancerAttributes",
                          - "s3:DeleteBucket",
                          - "s3:PutBucketPolicy",
                          - "s3:DeleteBucketPolicy",
                          - "s3:PutBucketVersioning",
                          - "s3:PutEncryptionConfiguration",
                          - "dynamodb:DeleteTable",
                          - "dynamodb:UpdateTable",
                        ] -> "route53:ChangeResourceRecordSets"
                      + Condition = {
                          + "ForAnyValue:StringLike" = {
                              + "route53:ChangeResourceRecordSetsNormalizedRecordNames" = "*javabin.internal"
                            }
                        }
                      ~ Resource  = [
                          - "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform",
                          - "arn:aws:elasticloadbalancing:eu-central-1:553637109631:loadbalancer/app/javabin-*",
                          - "arn:aws:s3:::javabin-terraform-*",
                          - "arn:aws:s3:::javabin-ci-*",
                          - "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-*",
                        ] -> "*"
                      ~ Sid       = "DenyPlatInfra" -> "DenyAppDNS"
                        # (1 unchanged attribute hidden)
                    },
                  + {
                      + Action   = [
                          + "ecs:DeleteCluster",
                          + "ecs:UpdateCluster",
                          + "elasticloadbalancing:DeleteLoadBalancer",
                          + "elasticloadbalancing:ModifyLoadBalancerAttributes",
                          + "s3:DeleteBucket",
                          + "s3:PutBucketPolicy",
                          + "s3:DeleteBucketPolicy",
                          + "s3:PutBucketVersioning",
                          + "s3:PutEncryptionConfiguration",
                          + "dynamodb:DeleteTable",
                          + "dynamodb:UpdateTable",
                        ]
                      + Effect   = "Deny"
                      + Resource = [
                          + "arn:aws:ecs:*:*:cluster/javabin-platform",
                          + "arn:aws:elasticloadbalancing:*:*:loadbalancer/app/javabin-*",
                          + "arn:aws:s3:::javabin-terraform-*",
                          + "arn:aws:s3:::javabin-ci-*",
                          + "arn:aws:dynamodb:*:*:table/javabin-terraform-*",
                        ]
                      + Sid      = "DenyPlatInfra"
                    },
                    {
                        Action   = [
                            "guardduty:*",
                            "securityhub:*",
                            "config:*",
                            "cloudtrail:*",
                            "bedrock:Invoke*",
                            "bedrock:Converse*",
                        ]
                        Effect   = "Deny"
                        Resource = "*"
                        Sid      = "DenySecSvcs"
                    },
                    {
                        Action      = "*"
                        Condition   = {
                            Null            = {
                                "aws:ResourceTag/team" = "false"
                            }
                            StringNotEquals = {
                                "aws:ResourceTag/team" = [
                                    "${aws:PrincipalTag/team}",
                                    "shared",
                                ]
                            }
                        }
                        Effect      = "Deny"
                        NotResource = [
                            "arn:aws:s3:::javabin-terraform-state-553637109631",
                            "arn:aws:s3:::javabin-terraform-state-553637109631/*",
                            "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*",
                        ]
                        Sid         = "DenyXTeam"
                    },
                  ~ {
                      ~ Action    = [
                            # (13 unchanged elements hidden)
                            "iam:DetachRolePolicy",
                          - "route53:DeleteHostedZone",
                            "sns:DeleteTopic",
                            # (2 unchanged elements hidden)
                            "ec2:RevokeSecurityGroup*",
                          + "servicediscovery:Delete*",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Action    = [
                            # (9 unchanged elements hidden)
                            "sns:UntagResource",
                          + "servicediscovery:TagResource",
                          + "servicediscovery:UntagResource",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "ssm:GetParameter*",
                            "ssm:GetParametersByPath",
                        ]
                        Effect   = "Deny"
                        Resource = [
                            "arn:aws:ssm:*:*:parameter/javabin/platform/*",
                            "arn:aws:ssm:*:*:parameter/javabin/slack/*",
                            "arn:aws:ssm:*:*:parameter/javabin/platform-overrides/*",
                        ]
                        Sid      = "DenyPlatSSM"
                    },
                    # (5 unchanged elements hidden)
                ]
                # (1 unchanged attribute hidden)
            }
        )
        tags             = {
            "Name" = "javabin-developer-boundary"
        }
        # (6 unchanged attributes hidden)
    }

  # module.iam.aws_iam_policy.team_guardrails["cupcake"] will be updated in-place
  ~ resource "aws_iam_policy" "team_guardrails" {
        id               = "arn:aws:iam::553637109631:policy/javabin-team-cupcake-guardrails"
        name             = "javabin-team-cupcake-guardrails"
      ~ policy           = jsonencode(
          ~ {
              ~ Statement = [
                    # (5 unchanged elements hidden)
                    {
                        Action   = [
                            "organizations:*",
                            "sso:*",
                            "sso-directory:*",
                            "identitystore:*",
                            "account:*",
                        ]
                        Effect   = "Deny"
                        Resource = "*"
                        Sid      = "DenyOrgSSO"
                    },
                  ~ {
                      ~ Action   = [
                            # (4 unchanged elements hidden)
                            "ec2:DeleteSubnet",
                          - "ec2:CreateInternetGateway",
                          - "ec2:DeleteInternetGateway",
                          - "ec2:AttachInternetGateway",
                          - "ec2:DetachInternetGateway",
                          - "ec2:CreateNatGateway",
                          - "ec2:DeleteNatGateway",
                          + "ec2:*InternetGateway",
                          + "ec2:*NatGateway",
                            "ec2:CreateRouteTable",
                            "ec2:DeleteRouteTable",
                          + "route53:DeleteHostedZone",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Action    = [
                          - "ecs:DeleteCluster",
                          - "ecs:UpdateCluster",
                          - "elasticloadbalancing:DeleteLoadBalancer",
                          - "elasticloadbalancing:ModifyLoadBalancerAttributes",
                          - "s3:DeleteBucket",
                          - "s3:PutBucketPolicy",
                          - "s3:DeleteBucketPolicy",
                          - "s3:PutBucketVersioning",
                          - "s3:PutEncryptionConfiguration",
                          - "dynamodb:DeleteTable",
                          - "dynamodb:UpdateTable",
                        ] -> "route53:ChangeResourceRecordSets"
                      + Condition = {
                          + "ForAnyValue:StringLike" = {
                              + "route53:ChangeResourceRecordSetsNormalizedRecordNames" = "*javabin.internal"
                            }
                        }
                      ~ Resource  = [
                          - "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform",
                          - "arn:aws:elasticloadbalancing:eu-central-1:553637109631:loadbalancer/app/javabin-*",
                          - "arn:aws:s3:::javabin-terraform-*",
                          - "arn:aws:s3:::javabin-ci-*",
                          - "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-*",
                        ] -> "*"
                      ~ Sid       = "DenyPlatInfra" -> "DenyAppDNS"
                        # (1 unchanged attribute hidden)
                    },
                  + {
                      + Action   = [
                          + "ecs:DeleteCluster",
                          + "ecs:UpdateCluster",
                          + "elasticloadbalancing:DeleteLoadBalancer",
                          + "elasticloadbalancing:ModifyLoadBalancerAttributes",
                          + "s3:DeleteBucket",
                          + "s3:PutBucketPolicy",
                          + "s3:DeleteBucketPolicy",
                          + "s3:PutBucketVersioning",
                          + "s3:PutEncryptionConfiguration",
                          + "dynamodb:DeleteTable",
                          + "dynamodb:UpdateTable",
                        ]
                      + Effect   = "Deny"
                      + Resource = [
                          + "arn:aws:ecs:*:*:cluster/javabin-platform",
                          + "arn:aws:elasticloadbalancing:*:*:loadbalancer/app/javabin-*",
                          + "arn:aws:s3:::javabin-terraform-*",
                          + "arn:aws:s3:::javabin-ci-*",
                          + "arn:aws:dynamodb:*:*:table/javabin-terraform-*",
                        ]
                      + Sid      = "DenyPlatInfra"
                    },
                    {
                        Action   = [
                            "guardduty:*",
                            "securityhub:*",
                            "config:*",
                            "cloudtrail:*",
                            "bedrock:Invoke*",
                            "bedrock:Converse*",
                        ]
                        Effect   = "Deny"
                        Resource = "*"
                        Sid      = "DenySecSvcs"
                    },
                    {
                        Action      = "*"
                        Condition   = {
                            Null            = {
                                "aws:ResourceTag/team" = "false"
                            }
                            StringNotEquals = {
                                "aws:ResourceTag/team" = [
                                    "cupcake",
                                    "shared",
                                ]
                            }
                        }
                        Effect      = "Deny"
                        NotResource = [
                            "arn:aws:s3:::javabin-terraform-state-553637109631",
                            "arn:aws:s3:::javabin-terraform-state-553637109631/*",
                            "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*",
                        ]
                        Sid         = "DenyXTeam"
                    },
                  ~ {
                      ~ Action    = [
                            # (13 unchanged elements hidden)
                            "iam:DetachRolePolicy",
                          - "route53:DeleteHostedZone",
                            "sns:DeleteTopic",
                            # (2 unchanged elements hidden)
                            "ec2:RevokeSecurityGroup*",
                          + "servicediscovery:Delete*",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Action    = [
                            # (9 unchanged elements hidden)
                            "sns:UntagResource",
                          + "servicediscovery:TagResource",
                          + "servicediscovery:UntagResource",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "ssm:GetParameter*",
                            "ssm:GetParametersByPath",
                        ]
                        Effect   = "Deny"
                        Resource = [
                            "arn:aws:ssm:*:*:parameter/javabin/platform/*",
                            "arn:aws:ssm:*:*:parameter/javabin/slack/*",
                            "arn:aws:ssm:*:*:parameter/javabin/platform-overrides/*",
                        ]
                        Sid      = "DenyPlatSSM"
                    },
                    # (5 unchanged elements hidden)
                ]
                # (1 unchanged attribute hidden)
            }
        )
        tags             = {
            "Name"     = "javabin-team-cupcake-guardrails"
            "for-team" = "cupcake"
            "team"     = "shared"
        }
        # (6 unchanged attributes hidden)
    }

  # module.iam.aws_iam_policy.team_guardrails["jzwatch"] will be updated in-place
  ~ resource "aws_iam_policy" "team_guardrails" {
        id               = "arn:aws:iam::553637109631:policy/javabin-team-jzwatch-guardrails"
        name             = "javabin-team-jzwatch-guardrails"
      ~ policy           = jsonencode(
          ~ {
              ~ Statement = [
                    # (5 unchanged elements hidden)
                    {
                        Action   = [
                            "organizations:*",
                            "sso:*",
                            "sso-directory:*",
                            "identitystore:*",
                            "account:*",
                        ]
                        Effect   = "Deny"
                        Resource = "*"
                        Sid      = "DenyOrgSSO"
                    },
                  ~ {
                      ~ Action   = [
                            # (4 unchanged elements hidden)
                            "ec2:DeleteSubnet",
                          - "ec2:CreateInternetGateway",
                          - "ec2:DeleteInternetGateway",
                          - "ec2:AttachInternetGateway",
                          - "ec2:DetachInternetGateway",
                          - "ec2:CreateNatGateway",
                          - "ec2:DeleteNatGateway",
                          + "ec2:*InternetGateway",
                          + "ec2:*NatGateway",
                            "ec2:CreateRouteTable",
                            "ec2:DeleteRouteTable",
                          + "route53:DeleteHostedZone",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Action    = [
                          - "ecs:DeleteCluster",
                          - "ecs:UpdateCluster",
                          - "elasticloadbalancing:DeleteLoadBalancer",
                          - "elasticloadbalancing:ModifyLoadBalancerAttributes",
                          - "s3:DeleteBucket",
                          - "s3:PutBucketPolicy",
                          - "s3:DeleteBucketPolicy",
                          - "s3:PutBucketVersioning",
                          - "s3:PutEncryptionConfiguration",
                          - "dynamodb:DeleteTable",
                          - "dynamodb:UpdateTable",
                        ] -> "route53:ChangeResourceRecordSets"
                      + Condition = {
                          + "ForAnyValue:StringLike" = {
                              + "route53:ChangeResourceRecordSetsNormalizedRecordNames" = "*javabin.internal"
                            }
                        }
                      ~ Resource  = [
                          - "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform",
                          - "arn:aws:elasticloadbalancing:eu-central-1:553637109631:loadbalancer/app/javabin-*",
                          - "arn:aws:s3:::javabin-terraform-*",
                          - "arn:aws:s3:::javabin-ci-*",
                          - "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-*",
                        ] -> "*"
                      ~ Sid       = "DenyPlatInfra" -> "DenyAppDNS"
                        # (1 unchanged attribute hidden)
                    },
                  + {
                      + Action   = [
                          + "ecs:DeleteCluster",
                          + "ecs:UpdateCluster",
                          + "elasticloadbalancing:DeleteLoadBalancer",
                          + "elasticloadbalancing:ModifyLoadBalancerAttributes",
                          + "s3:DeleteBucket",
                          + "s3:PutBucketPolicy",
                          + "s3:DeleteBucketPolicy",
                          + "s3:PutBucketVersioning",
                          + "s3:PutEncryptionConfiguration",
                          + "dynamodb:DeleteTable",
                          + "dynamodb:UpdateTable",
                        ]
                      + Effect   = "Deny"
                      + Resource = [
                          + "arn:aws:ecs:*:*:cluster/javabin-platform",
                          + "arn:aws:elasticloadbalancing:*:*:loadbalancer/app/javabin-*",
                          + "arn:aws:s3:::javabin-terraform-*",
                          + "arn:aws:s3:::javabin-ci-*",
                          + "arn:aws:dynamodb:*:*:table/javabin-terraform-*",
                        ]
                      + Sid      = "DenyPlatInfra"
                    },
                    {
                        Action   = [
                            "guardduty:*",
                            "securityhub:*",
                            "config:*",
                            "cloudtrail:*",
                            "bedrock:Invoke*",
                            "bedrock:Converse*",
                        ]
                        Effect   = "Deny"
                        Resource = "*"
                        Sid      = "DenySecSvcs"
                    },
                    {
                        Action      = "*"
                        Condition   = {
                            Null            = {
                                "aws:ResourceTag/team" = "false"
                            }
                            StringNotEquals = {
                                "aws:ResourceTag/team" = [
                                    "jzwatch",
                                    "shared",
                                ]
                            }
                        }
                        Effect      = "Deny"
                        NotResource = [
                            "arn:aws:s3:::javabin-terraform-state-553637109631",
                            "arn:aws:s3:::javabin-terraform-state-553637109631/*",
                            "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*",
                        ]
                        Sid         = "DenyXTeam"
                    },
                  ~ {
                      ~ Action    = [
                            # (13 unchanged elements hidden)
                            "iam:DetachRolePolicy",
                          - "route53:DeleteHostedZone",
                            "sns:DeleteTopic",
                            # (2 unchanged elements hidden)
                            "ec2:RevokeSecurityGroup*",
                          + "servicediscovery:Delete*",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Action    = [
                            # (9 unchanged elements hidden)
                            "sns:UntagResource",
                          + "servicediscovery:TagResource",
                          + "servicediscovery:UntagResource",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "ssm:GetParameter*",
                            "ssm:GetParametersByPath",
                        ]
                        Effect   = "Deny"
                        Resource = [
                            "arn:aws:ssm:*:*:parameter/javabin/platform/*",
                            "arn:aws:ssm:*:*:parameter/javabin/slack/*",
                            "arn:aws:ssm:*:*:parameter/javabin/platform-overrides/*",
                        ]
                        Sid      = "DenyPlatSSM"
                    },
                    # (5 unchanged elements hidden)
                ]
                # (1 unchanged attribute hidden)
            }
        )
        tags             = {
            "Name"     = "javabin-team-jzwatch-guardrails"
            "for-team" = "jzwatch"
            "team"     = "shared"
        }
        # (6 unchanged attributes hidden)
    }

  # module.iam.aws_iam_policy.team_guardrails["testteam"] will be updated in-place
  ~ resource "aws_iam_policy" "team_guardrails" {
        id               = "arn:aws:iam::553637109631:policy/javabin-team-testteam-guardrails"
        name             = "javabin-team-testteam-guardrails"
      ~ policy           = jsonencode(
          ~ {
              ~ Statement = [
                    # (5 unchanged elements hidden)
                    {
                        Action   = [
                            "organizations:*",
                            "sso:*",
                            "sso-directory:*",
                            "identitystore:*",
                            "account:*",
                        ]
                        Effect   = "Deny"
                        Resource = "*"
                        Sid      = "DenyOrgSSO"
                    },
                  ~ {
                      ~ Action   = [
                            # (4 unchanged elements hidden)
                            "ec2:DeleteSubnet",
                          - "ec2:CreateInternetGateway",
                          - "ec2:DeleteInternetGateway",
                          - "ec2:AttachInternetGateway",
                          - "ec2:DetachInternetGateway",
                          - "ec2:CreateNatGateway",
                          - "ec2:DeleteNatGateway",
                          + "ec2:*InternetGateway",
                          + "ec2:*NatGateway",
                            "ec2:CreateRouteTable",
                            "ec2:DeleteRouteTable",
                          + "route53:DeleteHostedZone",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Action    = [
                          - "ecs:DeleteCluster",
                          - "ecs:UpdateCluster",
                          - "elasticloadbalancing:DeleteLoadBalancer",
                          - "elasticloadbalancing:ModifyLoadBalancerAttributes",
                          - "s3:DeleteBucket",
                          - "s3:PutBucketPolicy",
                          - "s3:DeleteBucketPolicy",
                          - "s3:PutBucketVersioning",
                          - "s3:PutEncryptionConfiguration",
                          - "dynamodb:DeleteTable",
                          - "dynamodb:UpdateTable",
                        ] -> "route53:ChangeResourceRecordSets"
                      + Condition = {
                          + "ForAnyValue:StringLike" = {
                              + "route53:ChangeResourceRecordSetsNormalizedRecordNames" = "*javabin.internal"
                            }
                        }
                      ~ Resource  = [
                          - "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform",
                          - "arn:aws:elasticloadbalancing:eu-central-1:553637109631:loadbalancer/app/javabin-*",
                          - "arn:aws:s3:::javabin-terraform-*",
                          - "arn:aws:s3:::javabin-ci-*",
                          - "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-*",
                        ] -> "*"
                      ~ Sid       = "DenyPlatInfra" -> "DenyAppDNS"
                        # (1 unchanged attribute hidden)
                    },
                  + {
                      + Action   = [
                          + "ecs:DeleteCluster",
                          + "ecs:UpdateCluster",
                          + "elasticloadbalancing:DeleteLoadBalancer",
                          + "elasticloadbalancing:ModifyLoadBalancerAttributes",
                          + "s3:DeleteBucket",
                          + "s3:PutBucketPolicy",
                          + "s3:DeleteBucketPolicy",
                          + "s3:PutBucketVersioning",
                          + "s3:PutEncryptionConfiguration",
                          + "dynamodb:DeleteTable",
                          + "dynamodb:UpdateTable",
                        ]
                      + Effect   = "Deny"
                      + Resource = [
                          + "arn:aws:ecs:*:*:cluster/javabin-platform",
                          + "arn:aws:elasticloadbalancing:*:*:loadbalancer/app/javabin-*",
                          + "arn:aws:s3:::javabin-terraform-*",
                          + "arn:aws:s3:::javabin-ci-*",
                          + "arn:aws:dynamodb:*:*:table/javabin-terraform-*",
                        ]
                      + Sid      = "DenyPlatInfra"
                    },
                    {
                        Action   = [
                            "guardduty:*",
                            "securityhub:*",
                            "config:*",
                            "cloudtrail:*",
                            "bedrock:Invoke*",
                            "bedrock:Converse*",
                        ]
                        Effect   = "Deny"
                        Resource = "*"
                        Sid      = "DenySecSvcs"
                    },
                    {
                        Action      = "*"
                        Condition   = {
                            Null            = {
                                "aws:ResourceTag/team" = "false"
                            }
                            StringNotEquals = {
                                "aws:ResourceTag/team" = [
                                    "testteam",
                                    "shared",
                                ]
                            }
                        }
                        Effect      = "Deny"
                        NotResource = [
                            "arn:aws:s3:::javabin-terraform-state-553637109631",
                            "arn:aws:s3:::javabin-terraform-state-553637109631/*",
                            "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*",
                        ]
                        Sid         = "DenyXTeam"
                    },
                  ~ {
                      ~ Action    = [
                            # (13 unchanged elements hidden)
                            "iam:DetachRolePolicy",
                          - "route53:DeleteHostedZone",
                            "sns:DeleteTopic",
                            # (2 unchanged elements hidden)
                            "ec2:RevokeSecurityGroup*",
                          + "servicediscovery:Delete*",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Action    = [
                            # (9 unchanged elements hidden)
                            "sns:UntagResource",
                          + "servicediscovery:TagResource",
                          + "servicediscovery:UntagResource",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "ssm:GetParameter*",
                            "ssm:GetParametersByPath",
                        ]
                        Effect   = "Deny"
                        Resource = [
                            "arn:aws:ssm:*:*:parameter/javabin/platform/*",
                            "arn:aws:ssm:*:*:parameter/javabin/slack/*",
                            "arn:aws:ssm:*:*:parameter/javabin/platform-overrides/*",
                        ]
                        Sid      = "DenyPlatSSM"
                    },
                    # (5 unchanged elements hidden)
                ]
                # (1 unchanged attribute hidden)
            }
        )
        tags             = {
            "Name"     = "javabin-team-testteam-guardrails"
            "for-team" = "testteam"
            "team"     = "shared"
        }
        # (6 unchanged attributes hidden)
    }

  # module.lambdas.aws_cloudwatch_event_rule.app_peers_sync_reconcile will be created
  + resource "aws_cloudwatch_event_rule" "app_peers_sync_reconcile" {
      + arn                 = (known after apply)
      + description         = "Daily reconcile and audit of app-to-app calls"
      + event_bus_name      = "default"
      + force_destroy       = false
      + id                  = (known after apply)
      + name                = "javabin-app-peers-sync-reconcile"
      + name_prefix         = (known after apply)
      + schedule_expression = "cron(45 3 * * ? *)"
      + tags_all            = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_event_rule.app_peers_sync_trigger will be created
  + resource "aws_cloudwatch_event_rule" "app_peers_sync_trigger" {
      + arn            = (known after apply)
      + description    = "app.yaml calls/provides request created, changed or deleted"
      + event_bus_name = "default"
      + event_pattern  = jsonencode(
            {
              + detail      = {
                  + name      = [
                      + {
                          + wildcard = "/javabin/apps/*/*/network"
                        },
                    ]
                  + operation = [
                      + "Create",
                      + "Update",
                      + "Delete",
                    ]
                }
              + detail-type = [
                  + "Parameter Store Change",
                ]
              + source      = [
                  + "aws.ssm",
                ]
            }
        )
      + force_destroy  = false
      + id             = (known after apply)
      + name           = "javabin-app-peers-sync-trigger"
      + name_prefix    = (known after apply)
      + tags_all       = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_event_target.app_peers_sync will be created
  + resource "aws_cloudwatch_event_target" "app_peers_sync" {
      + arn            = (known after apply)
      + event_bus_name = "default"
      + force_destroy  = false
      + id             = (known after apply)
      + rule           = "javabin-app-peers-sync-trigger"
      + target_id      = "invoke-app-peers-sync"
    }

  # module.lambdas.aws_cloudwatch_event_target.app_peers_sync_reconcile will be created
  + resource "aws_cloudwatch_event_target" "app_peers_sync_reconcile" {
      + arn            = (known after apply)
      + event_bus_name = "default"
      + force_destroy  = false
      + id             = (known after apply)
      + input          = jsonencode(
            {
              + action = "reconcile"
            }
        )
      + rule           = "javabin-app-peers-sync-reconcile"
      + target_id      = "reconcile-app-peers-sync"
    }

  # module.lambdas.aws_iam_role.app_peers_sync will be created
  + resource "aws_iam_role" "app_peers_sync" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "lambda.amazonaws.com"
                        }
                    },
                ]
              + 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-app-peers-sync"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      + tags_all              = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
      + unique_id             = (known after apply)
    }

  # module.lambdas.aws_iam_role_policy.app_peers_sync will be created
  + resource "aws_iam_role_policy" "app_peers_sync" {
      + id          = (known after apply)
      + name        = "javabin-app-peers-sync"
      + name_prefix = (known after apply)
      + policy      = jsonencode(
            {
              + Statement = [
                  + {
                      + Action   = [
                          + "ssm:GetParameter",
                          + "ssm:GetParametersByPath",
                        ]
                      + Effect   = "Allow"
                      + Resource = [
                          + "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/apps",
                          + "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/apps/*",
                        ]
                      + Sid      = "ReadAppRequests"
                    },
                  + {
                      + Action   = [
                          + "ssm:PutParameter",
                          + "ssm:DeleteParameter",
                          + "ssm:DeleteParameters",
                          + "ssm:AddTagsToResource",
                        ]
                      + Effect   = "Allow"
                      + Resource = [
                          + "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/apps/*/*/peer-*",
                          + "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/apps/*/*/m2m-client-*",
                          + "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/apps/*/*/peers-status",
                        ]
                      + Sid      = "WritePeerParameters"
                    },
                  + {
                      + Action   = "ssm:GetParameter"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/slack/platform-resource-alerts-webhook"
                      + Sid      = "ReadSlackWebhook"
                    },
                  + {
                      + Action    = [
                          + "ec2:AuthorizeSecurityGroupIngress",
                          + "ec2:RevokeSecurityGroupIngress",
                        ]
                      + Condition = {
                          + ArnEquals    = {
                              + "ec2:Vpc" = "arn:aws:ec2:eu-central-1:553637109631:vpc/vpc-0cd3502de2527a310"
                            }
                          + StringEquals = {
                              + "aws:ResourceTag/javabin-network" = "app"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "arn:aws:ec2:eu-central-1:553637109631:security-group/*"
                      + Sid       = "EditAppSecurityGroups"
                    },
                  + {
                      + Action   = [
                          + "ec2:AuthorizeSecurityGroupIngress",
                          + "ec2:RevokeSecurityGroupIngress",
                        ]
                      + Effect   = "Allow"
                      + Resource = "arn:aws:ec2:eu-central-1:553637109631:security-group-rule/*"
                      + Sid      = "EditPeerRules"
                    },
                  + {
                      + Action    = "ec2:CreateTags"
                      + Condition = {
                          + StringEquals = {
                              + "ec2:CreateAction" = "AuthorizeSecurityGroupIngress"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "arn:aws:ec2:eu-central-1:553637109631:security-group-rule/*"
                      + Sid       = "TagPeerRulesOnCreate"
                    },
                  + {
                      + Action   = [
                          + "ec2:DescribeSecurityGroups",
                          + "ec2:DescribeSecurityGroupRules",
                          + "ec2:DescribeNetworkInterfaces",
                          + "servicediscovery:ListServices",
                          + "servicediscovery:ListInstances",
                          + "servicediscovery:ListTagsForResource",
                          + "ecs:ListTasks",
                        ]
                      + Effect   = "Allow"
                      + Resource = "*"
                      + Sid      = "Describe"
                    },
                  + {
                      + Action   = "route53:ListResourceRecordSets"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:route53:::hostedzone/*"
                      + Sid      = "AuditPrivateZone"
                    },
                  + {
                      + Action   = "ecs:DescribeTasks"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:ecs:eu-central-1:553637109631:task/javabin-platform/*"
                      + Sid      = "DescribeAppTasks"
                    },
                  + {
                      + Action   = [
                          + "cognito-idp:CreateResourceServer",
                          + "cognito-idp:UpdateResourceServer",
                          + "cognito-idp:DeleteResourceServer",
                          + "cognito-idp:DescribeResourceServer",
                          + "cognito-idp:ListResourceServers",
                          + "cognito-idp:CreateUserPoolClient",
                          + "cognito-idp:UpdateUserPoolClient",
                          + "cognito-idp:DeleteUserPoolClient",
                          + "cognito-idp:DescribeUserPoolClient",
                          + "cognito-idp:ListUserPoolClients",
                        ]
                      + Effect   = "Allow"
                      + Resource = "arn:aws:cognito-idp:eu-central-1:553637109631:userpool/eu-central-1_Icikv3dtD"
                      + Sid      = "ManageM2M"
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + role        = (known after apply)
    }

  # module.lambdas.aws_iam_role_policy_attachment.app_peers_sync_logs will be created
  + resource "aws_iam_role_policy_attachment" "app_peers_sync_logs" {
      + id         = (known after apply)
      + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
      + role       = "javabin-app-peers-sync"
    }

  # module.lambdas.aws_lambda_function.app_peers_sync will be created
  + resource "aws_lambda_function" "app_peers_sync" {
      + architectures                  = (known after apply)
      + arn                            = (known after apply)
      + code_sha256                    = (known after apply)
      + filename                       = "lambdas/builds/app_peers_sync.zip"
      + function_name                  = "javabin-app-peers-sync"
      + handler                        = "handler.handler"
      + id                             = (known after apply)
      + invoke_arn                     = (known after apply)
      + last_modified                  = (known after apply)
      + memory_size                    = 128
      + package_type                   = "Zip"
      + publish                        = false
      + qualified_arn                  = (known after apply)
      + qualified_invoke_arn           = (known after apply)
      + reserved_concurrent_executions = 1
      + role                           = (known after apply)
      + runtime                        = "python3.12"
      + signing_job_arn                = (known after apply)
      + signing_profile_version_arn    = (known after apply)
      + skip_destroy                   = false
      + source_code_hash               = "r1Jr/aVsU5A+pObjpG3UEBocVIHQp3oLprqiChqRf/I="
      + source_code_size               = (known after apply)
      + tags_all                       = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
      + timeout                        = 120
      + version                        = (known after apply)

      + environment {
          + variables = (known after apply)
        }
    }

  # module.lambdas.aws_lambda_function.app_secret_rollout will be updated in-place
  ~ resource "aws_lambda_function" "app_secret_rollout" {
        id                             = "javabin-app-secret-rollout"
      ~ last_modified                  = "2026-10-23T22:11:34.519+0000" -> (known after apply)
      ~ source_code_hash               = "Z+R/0D8XkgfnD5R4woPCl4cGzPFa2VcQlnZkX1wB0e0=" -> "Hnafa/5EpXUJ0Fs1gqLFWKsTj3ZEA0ynsoPNfo3gJP8="
        tags                           = {}
        # (21 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_permission.app_peers_sync_eventbridge will be created
  + resource "aws_lambda_permission" "app_peers_sync_eventbridge" {
      + action              = "lambda:InvokeFunction"
      + function_name       = "javabin-app-peers-sync"
      + id                  = (known after apply)
      + principal           = "events.amazonaws.com"
      + source_arn          = (known after apply)
      + statement_id        = "AllowEventBridge"
      + statement_id_prefix = (known after apply)
    }

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

  # module.networking.aws_service_discovery_private_dns_namespace.internal will be created
  + resource "aws_service_discovery_private_dns_namespace" "internal" {
      + arn         = (known after apply)
      + description = "Private app-to-app DNS (app.yaml calls/provides)"
      + hosted_zone = (known after apply)
      + id          = (known after apply)
      + name        = "javabin.internal"
      + tags        = {
          + "Name" = "javabin.internal"
          + "team" = "shared"
        }
      + tags_all    = {
          + "Name"        = "javabin.internal"
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "shared"
        }
      + vpc         = "vpc-0cd3502de2527a310"
    }

Plan: 11 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: 🔴 HIGH

Plan creates 11 resources (app-peers-sync Lambda, EventBridge rules, IAM role/policy, private DNS namespace javabin.internal) and updates 5 IAM policies and one Lambda, with 0 destroys; the developer boundary and team guardrail policy changes remove existing Deny protections and add broad new permissions, which is a security concern requiring review before apply.

  • 🔒 [security] module.iam developer_boundary and all team_guardrails policies: the 'DenyPlatInfra' Deny statement is replaced by 'DenyAppDNS'. The Deny on ECS cluster/ELB/S3 state-and-CI buckets/DynamoDB terraform tables is removed and reissued as a Deny only on the listed actions with Resource widened from specific ARNs to '' or ':*' wildcards, so it is a different and weaker statement. Developers could now run actions such as ecs:DeleteCluster and s3:DeleteBucket on platform resources through the boundary.
  • 🔒 [security] Developer boundary and guardrails removed Deny on ec2 Internet Gateway and NAT Gateway actions and replaced them with wildcards 'ec2:*InternetGateway' and 'ec2:NatGateway'. Route53 DeleteHostedZone is removed from one Deny list and added to another, so Deny coverage is changed in ways that are hard to audit. Also the 'servicediscovery:Delete' allow and tagging permissions are added to the guardrails.
  • 🔒 [security] The boundary's 'DenyXTeam' tag condition is changed per team (team tag must match principal team tag). The guardrail Deny for cupcake, jzwatch and testteam now permits 'route53:ChangeResourceRecordSets' with a condition limiting it to *.javabin.internal, which is a scoped allow, but the removal of route53:DeleteHostedZone from the blocked list is also a loosening. Confirm intent for each team.
  • 🔒 [security] New app-peers-sync Lambda role has permissions_boundary javabin-org-boundary, which is good. Its inline policy grants ec2:AuthorizeSecurityGroupIngress/RevokeSecurityGroupIngress on any security group in the app VPC tagged javabin-network=app, plus ssm:PutParameter/DeleteParameter on app peer parameters and cognito-idp Create/Update/DeleteUserPoolClient and DeleteResourceServer on the user pool. This is a significant write capability that should be reviewed for least privilege.
  • 💰 [cost] Resources created are mostly serverless and low cost: one Lambda (128 MB, reserved concurrency 1, 120s timeout), two EventBridge rules, one private DNS namespace (Route53 private hosted zone, billed monthly) and an IAM role. No NAT gateway, RDS or load balancer is created.
  • ✅ [routine] app_secret_rollout Lambda code is updated in place (source_code_hash change only). No destroy actions. The Lambda function app_peers_sync is created with source from lambdas/builds/app_peers_sync.zip, so confirm the artifact matches the reviewed source.

… append

Inserting it mid-list shifted every later statement, and the plan showed
DenyPlatInfra being renamed to DenyAppDNS. Same policy, same size.

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

Copy link
Copy Markdown

Terraform Plan

🚧 Changes detected — Plan: 11 to add, 5 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_policy.developer_boundary will be updated in-place
  ~ resource "aws_iam_policy" "developer_boundary" {
        id               = "arn:aws:iam::553637109631:policy/javabin-developer-boundary"
        name             = "javabin-developer-boundary"
      ~ policy           = jsonencode(
          ~ {
              ~ Statement = [
                    # (5 unchanged elements hidden)
                    {
                        Action   = [
                            "organizations:*",
                            "sso:*",
                            "sso-directory:*",
                            "identitystore:*",
                            "account:*",
                        ]
                        Effect   = "Deny"
                        Resource = "*"
                        Sid      = "DenyOrgSSO"
                    },
                  ~ {
                      ~ Action   = [
                            # (4 unchanged elements hidden)
                            "ec2:DeleteSubnet",
                          - "ec2:CreateInternetGateway",
                          - "ec2:DeleteInternetGateway",
                          - "ec2:AttachInternetGateway",
                          - "ec2:DetachInternetGateway",
                          - "ec2:CreateNatGateway",
                          - "ec2:DeleteNatGateway",
                          + "ec2:*InternetGateway",
                          + "ec2:*NatGateway",
                            "ec2:CreateRouteTable",
                            "ec2:DeleteRouteTable",
                          + "route53:DeleteHostedZone",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Resource = [
                          - "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform",
                          - "arn:aws:elasticloadbalancing:eu-central-1:553637109631:loadbalancer/app/javabin-*",
                          + "arn:aws:ecs:*:*:cluster/javabin-platform",
                          + "arn:aws:elasticloadbalancing:*:*:loadbalancer/app/javabin-*",
                            "arn:aws:s3:::javabin-terraform-*",
                            "arn:aws:s3:::javabin-ci-*",
                          - "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-*",
                          + "arn:aws:dynamodb:*:*:table/javabin-terraform-*",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "guardduty:*",
                            "securityhub:*",
                            "config:*",
                            "cloudtrail:*",
                            "bedrock:Invoke*",
                            "bedrock:Converse*",
                        ]
                        Effect   = "Deny"
                        Resource = "*"
                        Sid      = "DenySecSvcs"
                    },
                    {
                        Action      = "*"
                        Condition   = {
                            Null            = {
                                "aws:ResourceTag/team" = "false"
                            }
                            StringNotEquals = {
                                "aws:ResourceTag/team" = [
                                    "${aws:PrincipalTag/team}",
                                    "shared",
                                ]
                            }
                        }
                        Effect      = "Deny"
                        NotResource = [
                            "arn:aws:s3:::javabin-terraform-state-553637109631",
                            "arn:aws:s3:::javabin-terraform-state-553637109631/*",
                            "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*",
                        ]
                        Sid         = "DenyXTeam"
                    },
                  ~ {
                      ~ Action    = [
                            # (13 unchanged elements hidden)
                            "iam:DetachRolePolicy",
                          - "route53:DeleteHostedZone",
                            "sns:DeleteTopic",
                            # (2 unchanged elements hidden)
                            "ec2:RevokeSecurityGroup*",
                          + "servicediscovery:Delete*",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Action    = [
                            # (9 unchanged elements hidden)
                            "sns:UntagResource",
                          + "servicediscovery:TagResource",
                          + "servicediscovery:UntagResource",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "ssm:GetParameter*",
                            "ssm:GetParametersByPath",
                        ]
                        Effect   = "Deny"
                        Resource = [
                            "arn:aws:ssm:*:*:parameter/javabin/platform/*",
                            "arn:aws:ssm:*:*:parameter/javabin/slack/*",
                            "arn:aws:ssm:*:*:parameter/javabin/platform-overrides/*",
                        ]
                        Sid      = "DenyPlatSSM"
                    },
                    # (4 unchanged elements hidden)
                    {
                        Action      = [
                            "s3:CreateBucket",
                            "dynamodb:CreateTable",
                            "sqs:CreateQueue",
                            "sns:CreateTopic",
                            "rds:CreateDBInstance",
                            "rds:CreateDBSubnetGroup",
                            "ecs:CreateService",
                            "ecs:RegisterTaskDefinition",
                            "ecr:CreateRepository",
                            "logs:CreateLogGroup",
                            "ssm:PutParameter",
                            "lambda:CreateFunction",
                            "events:PutRule",
                            "states:CreateStateMachine",
                        ]
                        Effect      = "Deny"
                        NotResource = [
                            "arn:aws:s3:::${aws:PrincipalTag/team}-*",
                            "arn:aws:dynamodb:*:*:table/${aws:PrincipalTag/team}-*",
                            "arn:aws:sqs:*:*:${aws:PrincipalTag/team}-*",
                            "arn:aws:sns:*:*:${aws:PrincipalTag/team}-*",
                            "arn:aws:rds:*:*:db:${aws:PrincipalTag/team}-*",
                            "arn:aws:rds:*:*:subgrp:${aws:PrincipalTag/team}-*",
                            "arn:aws:ecs:*:*:service/*/${aws:PrincipalTag/team}-*",
                            "arn:aws:ecs:*:*:task-definition/${aws:PrincipalTag/team}-*:*",
                            "arn:aws:ecr:*:*:repository/${aws:PrincipalTag/team}-*",
                            "arn:aws:logs:*:*:log-group:/ecs/${aws:PrincipalTag/team}/*",
                            "arn:aws:ssm:*:*:parameter/javabin/apps/${aws:PrincipalTag/team}/*",
                            "arn:aws:lambda:*:*:function:${aws:PrincipalTag/team}-*",
                            "arn:aws:events:*:*:rule/${aws:PrincipalTag/team}-*",
                            "arn:aws:states:*:*:stateMachine:${aws:PrincipalTag/team}-*",
                        ]
                        Sid         = "DenyBadName"
                    },
                  + {
                      + Action    = "route53:ChangeResourceRecordSets"
                      + Condition = {
                          + "ForAnyValue:StringLike" = {
                              + "route53:ChangeResourceRecordSetsNormalizedRecordNames" = "*javabin.internal"
                            }
                        }
                      + Effect    = "Deny"
                      + Resource  = "*"
                      + Sid       = "DenyAppDNS"
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        tags             = {
            "Name" = "javabin-developer-boundary"
        }
        # (6 unchanged attributes hidden)
    }

  # module.iam.aws_iam_policy.team_guardrails["cupcake"] will be updated in-place
  ~ resource "aws_iam_policy" "team_guardrails" {
        id               = "arn:aws:iam::553637109631:policy/javabin-team-cupcake-guardrails"
        name             = "javabin-team-cupcake-guardrails"
      ~ policy           = jsonencode(
          ~ {
              ~ Statement = [
                    # (5 unchanged elements hidden)
                    {
                        Action   = [
                            "organizations:*",
                            "sso:*",
                            "sso-directory:*",
                            "identitystore:*",
                            "account:*",
                        ]
                        Effect   = "Deny"
                        Resource = "*"
                        Sid      = "DenyOrgSSO"
                    },
                  ~ {
                      ~ Action   = [
                            # (4 unchanged elements hidden)
                            "ec2:DeleteSubnet",
                          - "ec2:CreateInternetGateway",
                          - "ec2:DeleteInternetGateway",
                          - "ec2:AttachInternetGateway",
                          - "ec2:DetachInternetGateway",
                          - "ec2:CreateNatGateway",
                          - "ec2:DeleteNatGateway",
                          + "ec2:*InternetGateway",
                          + "ec2:*NatGateway",
                            "ec2:CreateRouteTable",
                            "ec2:DeleteRouteTable",
                          + "route53:DeleteHostedZone",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Resource = [
                          - "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform",
                          - "arn:aws:elasticloadbalancing:eu-central-1:553637109631:loadbalancer/app/javabin-*",
                          + "arn:aws:ecs:*:*:cluster/javabin-platform",
                          + "arn:aws:elasticloadbalancing:*:*:loadbalancer/app/javabin-*",
                            "arn:aws:s3:::javabin-terraform-*",
                            "arn:aws:s3:::javabin-ci-*",
                          - "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-*",
                          + "arn:aws:dynamodb:*:*:table/javabin-terraform-*",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "guardduty:*",
                            "securityhub:*",
                            "config:*",
                            "cloudtrail:*",
                            "bedrock:Invoke*",
                            "bedrock:Converse*",
                        ]
                        Effect   = "Deny"
                        Resource = "*"
                        Sid      = "DenySecSvcs"
                    },
                    {
                        Action      = "*"
                        Condition   = {
                            Null            = {
                                "aws:ResourceTag/team" = "false"
                            }
                            StringNotEquals = {
                                "aws:ResourceTag/team" = [
                                    "cupcake",
                                    "shared",
                                ]
                            }
                        }
                        Effect      = "Deny"
                        NotResource = [
                            "arn:aws:s3:::javabin-terraform-state-553637109631",
                            "arn:aws:s3:::javabin-terraform-state-553637109631/*",
                            "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*",
                        ]
                        Sid         = "DenyXTeam"
                    },
                  ~ {
                      ~ Action    = [
                            # (13 unchanged elements hidden)
                            "iam:DetachRolePolicy",
                          - "route53:DeleteHostedZone",
                            "sns:DeleteTopic",
                            # (2 unchanged elements hidden)
                            "ec2:RevokeSecurityGroup*",
                          + "servicediscovery:Delete*",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Action    = [
                            # (9 unchanged elements hidden)
                            "sns:UntagResource",
                          + "servicediscovery:TagResource",
                          + "servicediscovery:UntagResource",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "ssm:GetParameter*",
                            "ssm:GetParametersByPath",
                        ]
                        Effect   = "Deny"
                        Resource = [
                            "arn:aws:ssm:*:*:parameter/javabin/platform/*",
                            "arn:aws:ssm:*:*:parameter/javabin/slack/*",
                            "arn:aws:ssm:*:*:parameter/javabin/platform-overrides/*",
                        ]
                        Sid      = "DenyPlatSSM"
                    },
                    # (4 unchanged elements hidden)
                    {
                        Action      = [
                            "s3:CreateBucket",
                            "dynamodb:CreateTable",
                            "sqs:CreateQueue",
                            "sns:CreateTopic",
                            "rds:CreateDBInstance",
                            "rds:CreateDBSubnetGroup",
                            "ecs:CreateService",
                            "ecs:RegisterTaskDefinition",
                            "ecr:CreateRepository",
                            "logs:CreateLogGroup",
                            "ssm:PutParameter",
                            "lambda:CreateFunction",
                            "events:PutRule",
                            "states:CreateStateMachine",
                        ]
                        Effect      = "Deny"
                        NotResource = [
                            "arn:aws:s3:::cupcake-*",
                            "arn:aws:dynamodb:*:*:table/cupcake-*",
                            "arn:aws:sqs:*:*:cupcake-*",
                            "arn:aws:sns:*:*:cupcake-*",
                            "arn:aws:rds:*:*:db:cupcake-*",
                            "arn:aws:rds:*:*:subgrp:cupcake-*",
                            "arn:aws:ecs:*:*:service/*/cupcake-*",
                            "arn:aws:ecs:*:*:task-definition/cupcake-*:*",
                            "arn:aws:ecr:*:*:repository/cupcake-*",
                            "arn:aws:logs:*:*:log-group:/ecs/cupcake/*",
                            "arn:aws:ssm:*:*:parameter/javabin/apps/cupcake/*",
                            "arn:aws:lambda:*:*:function:cupcake-*",
                            "arn:aws:events:*:*:rule/cupcake-*",
                            "arn:aws:states:*:*:stateMachine:cupcake-*",
                        ]
                        Sid         = "DenyBadName"
                    },
                  + {
                      + Action    = "route53:ChangeResourceRecordSets"
                      + Condition = {
                          + "ForAnyValue:StringLike" = {
                              + "route53:ChangeResourceRecordSetsNormalizedRecordNames" = "*javabin.internal"
                            }
                        }
                      + Effect    = "Deny"
                      + Resource  = "*"
                      + Sid       = "DenyAppDNS"
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        tags             = {
            "Name"     = "javabin-team-cupcake-guardrails"
            "for-team" = "cupcake"
            "team"     = "shared"
        }
        # (6 unchanged attributes hidden)
    }

  # module.iam.aws_iam_policy.team_guardrails["jzwatch"] will be updated in-place
  ~ resource "aws_iam_policy" "team_guardrails" {
        id               = "arn:aws:iam::553637109631:policy/javabin-team-jzwatch-guardrails"
        name             = "javabin-team-jzwatch-guardrails"
      ~ policy           = jsonencode(
          ~ {
              ~ Statement = [
                    # (5 unchanged elements hidden)
                    {
                        Action   = [
                            "organizations:*",
                            "sso:*",
                            "sso-directory:*",
                            "identitystore:*",
                            "account:*",
                        ]
                        Effect   = "Deny"
                        Resource = "*"
                        Sid      = "DenyOrgSSO"
                    },
                  ~ {
                      ~ Action   = [
                            # (4 unchanged elements hidden)
                            "ec2:DeleteSubnet",
                          - "ec2:CreateInternetGateway",
                          - "ec2:DeleteInternetGateway",
                          - "ec2:AttachInternetGateway",
                          - "ec2:DetachInternetGateway",
                          - "ec2:CreateNatGateway",
                          - "ec2:DeleteNatGateway",
                          + "ec2:*InternetGateway",
                          + "ec2:*NatGateway",
                            "ec2:CreateRouteTable",
                            "ec2:DeleteRouteTable",
                          + "route53:DeleteHostedZone",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Resource = [
                          - "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform",
                          - "arn:aws:elasticloadbalancing:eu-central-1:553637109631:loadbalancer/app/javabin-*",
                          + "arn:aws:ecs:*:*:cluster/javabin-platform",
                          + "arn:aws:elasticloadbalancing:*:*:loadbalancer/app/javabin-*",
                            "arn:aws:s3:::javabin-terraform-*",
                            "arn:aws:s3:::javabin-ci-*",
                          - "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-*",
                          + "arn:aws:dynamodb:*:*:table/javabin-terraform-*",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "guardduty:*",
                            "securityhub:*",
                            "config:*",
                            "cloudtrail:*",
                            "bedrock:Invoke*",
                            "bedrock:Converse*",
                        ]
                        Effect   = "Deny"
                        Resource = "*"
                        Sid      = "DenySecSvcs"
                    },
                    {
                        Action      = "*"
                        Condition   = {
                            Null            = {
                                "aws:ResourceTag/team" = "false"
                            }
                            StringNotEquals = {
                                "aws:ResourceTag/team" = [
                                    "jzwatch",
                                    "shared",
                                ]
                            }
                        }
                        Effect      = "Deny"
                        NotResource = [
                            "arn:aws:s3:::javabin-terraform-state-553637109631",
                            "arn:aws:s3:::javabin-terraform-state-553637109631/*",
                            "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*",
                        ]
                        Sid         = "DenyXTeam"
                    },
                  ~ {
                      ~ Action    = [
                            # (13 unchanged elements hidden)
                            "iam:DetachRolePolicy",
                          - "route53:DeleteHostedZone",
                            "sns:DeleteTopic",
                            # (2 unchanged elements hidden)
                            "ec2:RevokeSecurityGroup*",
                          + "servicediscovery:Delete*",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Action    = [
                            # (9 unchanged elements hidden)
                            "sns:UntagResource",
                          + "servicediscovery:TagResource",
                          + "servicediscovery:UntagResource",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "ssm:GetParameter*",
                            "ssm:GetParametersByPath",
                        ]
                        Effect   = "Deny"
                        Resource = [
                            "arn:aws:ssm:*:*:parameter/javabin/platform/*",
                            "arn:aws:ssm:*:*:parameter/javabin/slack/*",
                            "arn:aws:ssm:*:*:parameter/javabin/platform-overrides/*",
                        ]
                        Sid      = "DenyPlatSSM"
                    },
                    # (4 unchanged elements hidden)
                    {
                        Action      = [
                            "s3:CreateBucket",
                            "dynamodb:CreateTable",
                            "sqs:CreateQueue",
                            "sns:CreateTopic",
                            "rds:CreateDBInstance",
                            "rds:CreateDBSubnetGroup",
                            "ecs:CreateService",
                            "ecs:RegisterTaskDefinition",
                            "ecr:CreateRepository",
                            "logs:CreateLogGroup",
                            "ssm:PutParameter",
                            "lambda:CreateFunction",
                            "events:PutRule",
                            "states:CreateStateMachine",
                        ]
                        Effect      = "Deny"
                        NotResource = [
                            "arn:aws:s3:::jzwatch-*",
                            "arn:aws:dynamodb:*:*:table/jzwatch-*",
                            "arn:aws:sqs:*:*:jzwatch-*",
                            "arn:aws:sns:*:*:jzwatch-*",
                            "arn:aws:rds:*:*:db:jzwatch-*",
                            "arn:aws:rds:*:*:subgrp:jzwatch-*",
                            "arn:aws:ecs:*:*:service/*/jzwatch-*",
                            "arn:aws:ecs:*:*:task-definition/jzwatch-*:*",
                            "arn:aws:ecr:*:*:repository/jzwatch-*",
                            "arn:aws:logs:*:*:log-group:/ecs/jzwatch/*",
                            "arn:aws:ssm:*:*:parameter/javabin/apps/jzwatch/*",
                            "arn:aws:lambda:*:*:function:jzwatch-*",
                            "arn:aws:events:*:*:rule/jzwatch-*",
                            "arn:aws:states:*:*:stateMachine:jzwatch-*",
                        ]
                        Sid         = "DenyBadName"
                    },
                  + {
                      + Action    = "route53:ChangeResourceRecordSets"
                      + Condition = {
                          + "ForAnyValue:StringLike" = {
                              + "route53:ChangeResourceRecordSetsNormalizedRecordNames" = "*javabin.internal"
                            }
                        }
                      + Effect    = "Deny"
                      + Resource  = "*"
                      + Sid       = "DenyAppDNS"
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        tags             = {
            "Name"     = "javabin-team-jzwatch-guardrails"
            "for-team" = "jzwatch"
            "team"     = "shared"
        }
        # (6 unchanged attributes hidden)
    }

  # module.iam.aws_iam_policy.team_guardrails["testteam"] will be updated in-place
  ~ resource "aws_iam_policy" "team_guardrails" {
        id               = "arn:aws:iam::553637109631:policy/javabin-team-testteam-guardrails"
        name             = "javabin-team-testteam-guardrails"
      ~ policy           = jsonencode(
          ~ {
              ~ Statement = [
                    # (5 unchanged elements hidden)
                    {
                        Action   = [
                            "organizations:*",
                            "sso:*",
                            "sso-directory:*",
                            "identitystore:*",
                            "account:*",
                        ]
                        Effect   = "Deny"
                        Resource = "*"
                        Sid      = "DenyOrgSSO"
                    },
                  ~ {
                      ~ Action   = [
                            # (4 unchanged elements hidden)
                            "ec2:DeleteSubnet",
                          - "ec2:CreateInternetGateway",
                          - "ec2:DeleteInternetGateway",
                          - "ec2:AttachInternetGateway",
                          - "ec2:DetachInternetGateway",
                          - "ec2:CreateNatGateway",
                          - "ec2:DeleteNatGateway",
                          + "ec2:*InternetGateway",
                          + "ec2:*NatGateway",
                            "ec2:CreateRouteTable",
                            "ec2:DeleteRouteTable",
                          + "route53:DeleteHostedZone",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Resource = [
                          - "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform",
                          - "arn:aws:elasticloadbalancing:eu-central-1:553637109631:loadbalancer/app/javabin-*",
                          + "arn:aws:ecs:*:*:cluster/javabin-platform",
                          + "arn:aws:elasticloadbalancing:*:*:loadbalancer/app/javabin-*",
                            "arn:aws:s3:::javabin-terraform-*",
                            "arn:aws:s3:::javabin-ci-*",
                          - "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-*",
                          + "arn:aws:dynamodb:*:*:table/javabin-terraform-*",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "guardduty:*",
                            "securityhub:*",
                            "config:*",
                            "cloudtrail:*",
                            "bedrock:Invoke*",
                            "bedrock:Converse*",
                        ]
                        Effect   = "Deny"
                        Resource = "*"
                        Sid      = "DenySecSvcs"
                    },
                    {
                        Action      = "*"
                        Condition   = {
                            Null            = {
                                "aws:ResourceTag/team" = "false"
                            }
                            StringNotEquals = {
                                "aws:ResourceTag/team" = [
                                    "testteam",
                                    "shared",
                                ]
                            }
                        }
                        Effect      = "Deny"
                        NotResource = [
                            "arn:aws:s3:::javabin-terraform-state-553637109631",
                            "arn:aws:s3:::javabin-terraform-state-553637109631/*",
                            "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631",
                            "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*",
                        ]
                        Sid         = "DenyXTeam"
                    },
                  ~ {
                      ~ Action    = [
                            # (13 unchanged elements hidden)
                            "iam:DetachRolePolicy",
                          - "route53:DeleteHostedZone",
                            "sns:DeleteTopic",
                            # (2 unchanged elements hidden)
                            "ec2:RevokeSecurityGroup*",
                          + "servicediscovery:Delete*",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Action    = [
                            # (9 unchanged elements hidden)
                            "sns:UntagResource",
                          + "servicediscovery:TagResource",
                          + "servicediscovery:UntagResource",
                        ]
                        # (4 unchanged attributes hidden)
                    },
                    {
                        Action   = [
                            "ssm:GetParameter*",
                            "ssm:GetParametersByPath",
                        ]
                        Effect   = "Deny"
                        Resource = [
                            "arn:aws:ssm:*:*:parameter/javabin/platform/*",
                            "arn:aws:ssm:*:*:parameter/javabin/slack/*",
                            "arn:aws:ssm:*:*:parameter/javabin/platform-overrides/*",
                        ]
                        Sid      = "DenyPlatSSM"
                    },
                    # (4 unchanged elements hidden)
                    {
                        Action      = [
                            "s3:CreateBucket",
                            "dynamodb:CreateTable",
                            "sqs:CreateQueue",
                            "sns:CreateTopic",
                            "rds:CreateDBInstance",
                            "rds:CreateDBSubnetGroup",
                            "ecs:CreateService",
                            "ecs:RegisterTaskDefinition",
                            "ecr:CreateRepository",
                            "logs:CreateLogGroup",
                            "ssm:PutParameter",
                            "lambda:CreateFunction",
                            "events:PutRule",
                            "states:CreateStateMachine",
                        ]
                        Effect      = "Deny"
                        NotResource = [
                            "arn:aws:s3:::testteam-*",
                            "arn:aws:dynamodb:*:*:table/testteam-*",
                            "arn:aws:sqs:*:*:testteam-*",
                            "arn:aws:sns:*:*:testteam-*",
                            "arn:aws:rds:*:*:db:testteam-*",
                            "arn:aws:rds:*:*:subgrp:testteam-*",
                            "arn:aws:ecs:*:*:service/*/testteam-*",
                            "arn:aws:ecs:*:*:task-definition/testteam-*:*",
                            "arn:aws:ecr:*:*:repository/testteam-*",
                            "arn:aws:logs:*:*:log-group:/ecs/testteam/*",
                            "arn:aws:ssm:*:*:parameter/javabin/apps/testteam/*",
                            "arn:aws:lambda:*:*:function:testteam-*",
                            "arn:aws:events:*:*:rule/testteam-*",
                            "arn:aws:states:*:*:stateMachine:testteam-*",
                        ]
                        Sid         = "DenyBadName"
                    },
                  + {
                      + Action    = "route53:ChangeResourceRecordSets"
                      + Condition = {
                          + "ForAnyValue:StringLike" = {
                              + "route53:ChangeResourceRecordSetsNormalizedRecordNames" = "*javabin.internal"
                            }
                        }
                      + Effect    = "Deny"
                      + Resource  = "*"
                      + Sid       = "DenyAppDNS"
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        tags             = {
            "Name"     = "javabin-team-testteam-guardrails"
            "for-team" = "testteam"
            "team"     = "shared"
        }
        # (6 unchanged attributes hidden)
    }

  # module.lambdas.aws_cloudwatch_event_rule.app_peers_sync_reconcile will be created
  + resource "aws_cloudwatch_event_rule" "app_peers_sync_reconcile" {
      + arn                 = (known after apply)
      + description         = "Daily reconcile and audit of app-to-app calls"
      + event_bus_name      = "default"
      + force_destroy       = false
      + id                  = (known after apply)
      + name                = "javabin-app-peers-sync-reconcile"
      + name_prefix         = (known after apply)
      + schedule_expression = "cron(45 3 * * ? *)"
      + tags_all            = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_event_rule.app_peers_sync_trigger will be created
  + resource "aws_cloudwatch_event_rule" "app_peers_sync_trigger" {
      + arn            = (known after apply)
      + description    = "app.yaml calls/provides request created, changed or deleted"
      + event_bus_name = "default"
      + event_pattern  = jsonencode(
            {
              + detail      = {
                  + name      = [
                      + {
                          + wildcard = "/javabin/apps/*/*/network"
                        },
                    ]
                  + operation = [
                      + "Create",
                      + "Update",
                      + "Delete",
                    ]
                }
              + detail-type = [
                  + "Parameter Store Change",
                ]
              + source      = [
                  + "aws.ssm",
                ]
            }
        )
      + force_destroy  = false
      + id             = (known after apply)
      + name           = "javabin-app-peers-sync-trigger"
      + name_prefix    = (known after apply)
      + tags_all       = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.lambdas.aws_cloudwatch_event_target.app_peers_sync will be created
  + resource "aws_cloudwatch_event_target" "app_peers_sync" {
      + arn            = (known after apply)
      + event_bus_name = "default"
      + force_destroy  = false
      + id             = (known after apply)
      + rule           = "javabin-app-peers-sync-trigger"
      + target_id      = "invoke-app-peers-sync"
    }

  # module.lambdas.aws_cloudwatch_event_target.app_peers_sync_reconcile will be created
  + resource "aws_cloudwatch_event_target" "app_peers_sync_reconcile" {
      + arn            = (known after apply)
      + event_bus_name = "default"
      + force_destroy  = false
      + id             = (known after apply)
      + input          = jsonencode(
            {
              + action = "reconcile"
            }
        )
      + rule           = "javabin-app-peers-sync-reconcile"
      + target_id      = "reconcile-app-peers-sync"
    }

  # module.lambdas.aws_iam_role.app_peers_sync will be created
  + resource "aws_iam_role" "app_peers_sync" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "lambda.amazonaws.com"
                        }
                    },
                ]
              + 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-app-peers-sync"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      + tags_all              = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
      + unique_id             = (known after apply)
    }

  # module.lambdas.aws_iam_role_policy.app_peers_sync will be created
  + resource "aws_iam_role_policy" "app_peers_sync" {
      + id          = (known after apply)
      + name        = "javabin-app-peers-sync"
      + name_prefix = (known after apply)
      + policy      = jsonencode(
            {
              + Statement = [
                  + {
                      + Action   = [
                          + "ssm:GetParameter",
                          + "ssm:GetParametersByPath",
                        ]
                      + Effect   = "Allow"
                      + Resource = [
                          + "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/apps",
                          + "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/apps/*",
                        ]
                      + Sid      = "ReadAppRequests"
                    },
                  + {
                      + Action   = [
                          + "ssm:PutParameter",
                          + "ssm:DeleteParameter",
                          + "ssm:DeleteParameters",
                          + "ssm:AddTagsToResource",
                        ]
                      + Effect   = "Allow"
                      + Resource = [
                          + "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/apps/*/*/peer-*",
                          + "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/apps/*/*/m2m-client-*",
                          + "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/apps/*/*/peers-status",
                        ]
                      + Sid      = "WritePeerParameters"
                    },
                  + {
                      + Action   = "ssm:GetParameter"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/slack/platform-resource-alerts-webhook"
                      + Sid      = "ReadSlackWebhook"
                    },
                  + {
                      + Action    = [
                          + "ec2:AuthorizeSecurityGroupIngress",
                          + "ec2:RevokeSecurityGroupIngress",
                        ]
                      + Condition = {
                          + ArnEquals    = {
                              + "ec2:Vpc" = "arn:aws:ec2:eu-central-1:553637109631:vpc/vpc-0cd3502de2527a310"
                            }
                          + StringEquals = {
                              + "aws:ResourceTag/javabin-network" = "app"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "arn:aws:ec2:eu-central-1:553637109631:security-group/*"
                      + Sid       = "EditAppSecurityGroups"
                    },
                  + {
                      + Action   = [
                          + "ec2:AuthorizeSecurityGroupIngress",
                          + "ec2:RevokeSecurityGroupIngress",
                        ]
                      + Effect   = "Allow"
                      + Resource = "arn:aws:ec2:eu-central-1:553637109631:security-group-rule/*"
                      + Sid      = "EditPeerRules"
                    },
                  + {
                      + Action    = "ec2:CreateTags"
                      + Condition = {
                          + StringEquals = {
                              + "ec2:CreateAction" = "AuthorizeSecurityGroupIngress"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "arn:aws:ec2:eu-central-1:553637109631:security-group-rule/*"
                      + Sid       = "TagPeerRulesOnCreate"
                    },
                  + {
                      + Action   = [
                          + "ec2:DescribeSecurityGroups",
                          + "ec2:DescribeSecurityGroupRules",
                          + "ec2:DescribeNetworkInterfaces",
                          + "servicediscovery:ListServices",
                          + "servicediscovery:ListInstances",
                          + "servicediscovery:ListTagsForResource",
                          + "ecs:ListTasks",
                        ]
                      + Effect   = "Allow"
                      + Resource = "*"
                      + Sid      = "Describe"
                    },
                  + {
                      + Action   = "route53:ListResourceRecordSets"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:route53:::hostedzone/*"
                      + Sid      = "AuditPrivateZone"
                    },
                  + {
                      + Action   = "ecs:DescribeTasks"
                      + Effect   = "Allow"
                      + Resource = "arn:aws:ecs:eu-central-1:553637109631:task/javabin-platform/*"
                      + Sid      = "DescribeAppTasks"
                    },
                  + {
                      + Action   = [
                          + "cognito-idp:CreateResourceServer",
                          + "cognito-idp:UpdateResourceServer",
                          + "cognito-idp:DeleteResourceServer",
                          + "cognito-idp:DescribeResourceServer",
                          + "cognito-idp:ListResourceServers",
                          + "cognito-idp:CreateUserPoolClient",
                          + "cognito-idp:UpdateUserPoolClient",
                          + "cognito-idp:DeleteUserPoolClient",
                          + "cognito-idp:DescribeUserPoolClient",
                          + "cognito-idp:ListUserPoolClients",
                        ]
                      + Effect   = "Allow"
                      + Resource = "arn:aws:cognito-idp:eu-central-1:553637109631:userpool/eu-central-1_Icikv3dtD"
                      + Sid      = "ManageM2M"
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + role        = (known after apply)
    }

  # module.lambdas.aws_iam_role_policy_attachment.app_peers_sync_logs will be created
  + resource "aws_iam_role_policy_attachment" "app_peers_sync_logs" {
      + id         = (known after apply)
      + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
      + role       = "javabin-app-peers-sync"
    }

  # module.lambdas.aws_lambda_function.app_peers_sync will be created
  + resource "aws_lambda_function" "app_peers_sync" {
      + architectures                  = (known after apply)
      + arn                            = (known after apply)
      + code_sha256                    = (known after apply)
      + filename                       = "lambdas/builds/app_peers_sync.zip"
      + function_name                  = "javabin-app-peers-sync"
      + handler                        = "handler.handler"
      + id                             = (known after apply)
      + invoke_arn                     = (known after apply)
      + last_modified                  = (known after apply)
      + memory_size                    = 128
      + package_type                   = "Zip"
      + publish                        = false
      + qualified_arn                  = (known after apply)
      + qualified_invoke_arn           = (known after apply)
      + reserved_concurrent_executions = 1
      + role                           = (known after apply)
      + runtime                        = "python3.12"
      + signing_job_arn                = (known after apply)
      + signing_profile_version_arn    = (known after apply)
      + skip_destroy                   = false
      + source_code_hash               = "r1Jr/aVsU5A+pObjpG3UEBocVIHQp3oLprqiChqRf/I="
      + source_code_size               = (known after apply)
      + tags_all                       = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
      + timeout                        = 120
      + version                        = (known after apply)

      + environment {
          + variables = (known after apply)
        }
    }

  # module.lambdas.aws_lambda_function.app_secret_rollout will be updated in-place
  ~ resource "aws_lambda_function" "app_secret_rollout" {
        id                             = "javabin-app-secret-rollout"
      ~ last_modified                  = "2026-10-23T22:11:34.519+0000" -> (known after apply)
      ~ source_code_hash               = "Z+R/0D8XkgfnD5R4woPCl4cGzPFa2VcQlnZkX1wB0e0=" -> "Hnafa/5EpXUJ0Fs1gqLFWKsTj3ZEA0ynsoPNfo3gJP8="
        tags                           = {}
        # (21 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_permission.app_peers_sync_eventbridge will be created
  + resource "aws_lambda_permission" "app_peers_sync_eventbridge" {
      + action              = "lambda:InvokeFunction"
      + function_name       = "javabin-app-peers-sync"
      + id                  = (known after apply)
      + principal           = "events.amazonaws.com"
      + source_arn          = (known after apply)
      + statement_id        = "AllowEventBridge"
      + statement_id_prefix = (known after apply)
    }

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

  # module.networking.aws_service_discovery_private_dns_namespace.internal will be created
  + resource "aws_service_discovery_private_dns_namespace" "internal" {
      + arn         = (known after apply)
      + description = "Private app-to-app DNS (app.yaml calls/provides)"
      + hosted_zone = (known after apply)
      + id          = (known after apply)
      + name        = "javabin.internal"
      + tags        = {
          + "Name" = "javabin.internal"
          + "team" = "shared"
        }
      + tags_all    = {
          + "Name"        = "javabin.internal"
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "shared"
        }
      + vpc         = "vpc-0cd3502de2527a310"
    }

Plan: 11 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: 🔴 HIGH

Plan creates 11 resources (app-peers-sync Lambda, EventBridge rules, IAM role/policies, Cloud Map namespace) and updates 5 (developer boundary, three team guardrail policies, one Lambda code change), with no destroys; however, the IAM policy edits weaken guardrails and the new Lambda role has security-group editing rights.

  • 🔒 [security] Developer boundary and all three team guardrail policies (cupcake, jzwatch, testteam) are changed to remove explicit Deny-type restrictions. Removed from the ec2 list: CreateInternetGateway, DeleteInternetGateway, AttachInternetGateway, DetachInternetGateway, CreateNatGateway, DeleteNatGateway (replaced by wildcards ec2:*InternetGateway and ec2:*NatGateway in the same statement). Also route53:DeleteHostedZone is moved out of one statement and into another, and ecs/elbv2/dynamodb resource ARNs are widened from eu-central-1 account-specific to :. These are boundary changes that broaden what developers can do, so they need explicit security review before apply.
  • 🔒 [security] The new Lambda role javabin-app-peers-sync is granted ec2:AuthorizeSecurityGroupIngress and RevokeSecurityGroupIngress on app security groups (scoped by tag and VPC condition), plus ssm:PutParameter/DeleteParameter on peer parameters and Cognito resource server and user pool client management. It has a permissions boundary (javabin-org-boundary), which is good. Confirm the tag condition and the scoping are sufficient, since it can change inbound rules on app SGs.
  • 🔒 [security] A new DenyAppDNS statement is added to the developer boundary and all team guardrails. It denies route53:ChangeResourceRecordSets for javabin.internal names. This is a restriction and is a positive change. The Cloud Map namespace javabin.internal is created in VPC vpc-0cd3502de2527a310, which is internal only.
  • 💥 [destruction] Plan has 0 destroys. The Lambda app_secret_rollout is an in-place code update (source_code_hash change only). The three guardrail policies and developer boundary are in-place policy updates. No databases, security groups, or networking resources are removed or replaced.
  • 💰 [cost] New billable resources are minor: one Lambda (128 MB, reserved concurrency 1), EventBridge rules, and a Cloud Map private DNS namespace (small per-zone and query charges). No NAT gateways, RDS instances, or load balancers are created.

@Alexanderamiri

Copy link
Copy Markdown
Member Author

About the HIGH risk rating on the latest plan (753ca02)

The review reads the boundary edits as loosening. They tighten the boundary:

  • ec2:*InternetGateway and ec2:*NatGateway sit in DenyNetwork, a Deny statement with Resource = "*". The wildcards match the six actions that were listed by name, and also the egress-only IGW actions, so they deny more, not less.
  • The ECS, ELB and DynamoDB ARNs in DenyPlatInfra (a Deny) changed from eu-central-1:<account> to *:*. A Deny with a wider resource denies more. DenyBadName already uses the same pattern.
  • route53:DeleteHostedZone moved from DenyShared to DenyNetwork. In DenyShared it never took effect, because it was conditioned on aws:ResourceTag/team, which Route53 does not support. In DenyNetwork it has no condition, so it now actually applies.
  • The new DenyAppDNS is appended as the last statement and denies record changes under *javabin.internal (review finding 1).
  • servicediscovery:Delete*, TagResource and UntagResource are added to Deny statements (DenyShared and DenyTagStrip), not granted.

Checked with the IAM policy simulator on the rendered statements:

Action Resource / record name Result
ChangeResourceRecordSets cupcake-cupcake.javabin.internal explicitDeny
ChangeResourceRecordSets javabin.internal (apex) explicitDeny
ChangeResourceRecordSets testteam.java.no allowed
DeleteHostedZone any zone explicitDeny
CreateEgressOnlyInternetGateway, AttachInternetGateway, DeleteNatGateway — explicitDeny
DescribeNatGateways — allowed

The size is 6,067 of 6,144 bytes.

🤖 Generated with Claude Code

Alexanderamiri added a commit that referenced this pull request Oct 24, 2026
…th doctor (#167)

## Why

Part of #157. Internal-pool sign-in goes through the `GoogleSAML` app,
which always answers for the browser's active Google account. A personal
Gmail gets a dead-end `403 app_not_configured_for_user` page with no
account chooser. The classic hosted UI drops `prompt=`, so Cognito can't
ask SAML for one either. The fix is a Google OIDC IdP, with an
Internal-consent OAuth client and `prompt=select_account`.

The investigation also found that teams can't see why sign-in fails, and
can't recover:
- invalid requests and console drift are visible only in CloudWatch;
- a replaced client leaves running tasks on a deleted client ID;
- callback typos show up only as `redirect_mismatch` in the browser;
- an `auth.groups` typo denies everyone.

**Merge #166 (internal-pool lockdown) first.** This branch is rebased on
it and reuses its per-pool IdP list, so until #166 merges this PR's diff
and plan also contain #166's two commits. After #166 lands, GitHub shows
only the last commit.

This PR is safe to merge before the Google credentials exist. Every
Google behaviour change is gated, and with #166 the internal default is
`["GoogleSAML"]` (what cupcake's client has live today).

## What

**identity (Terraform)**
- `aws_cognito_identity_provider.internal_google` is now gated on SSM
`/javabin/platform/google-oauth/internal/{client-id,client-secret}`,
read with `aws_ssm_parameters_by_path`, instead of a tfvar. Its count
stays 0 until both exist.
- `authorize_scopes = "openid email profile"`, and mapping `email`,
`email_verified`, `name`, `username=sub`.
- `authorize_url = …/v2/auth?hd=java.no&prompt=select_account`. You can
turn this off with `google_authorize_url_params = false` for the
managed-login fallback.
- Uses #166's per-pool lists `internal_identity_providers` (default
`["GoogleSAML"]`, COGNITO refused) and `external_identity_providers`
(`["COGNITO"]`), env
`INTERNAL_IDENTITY_PROVIDERS`/`EXTERNAL_IDENTITY_PROVIDERS`. The same
internal list is auth-claims' `FEDERATED_IDENTITY_PROVIDERS`, so the
cutover changes clients and the trust list together.
  - Adding the Google IdP changes no client until this list names it.
- An output precondition fails the plan if `Google` is listed without
the SSM credentials or with an empty `google_workspace_domain`
(auth-claims' `REQUIRED_EMAIL_DOMAIN`).
- auth-claims: IdPs in `EMAIL_VERIFIED_IDENTITY_PROVIDERS` (default
`Google`) are not trusted on the IdP name alone; their users also need
`email_verified=true`, mapped from Google's claim.
- The external pool keeps its `google_client_id` tfvar path. It is
unchanged and still empty.

**app-auth-sync**
- `cognito-client-id`, `-secret` and the new `-identity-provider` are
written **only when the value changes**.
  - The secret is not decrypted. It changes only with a new client ID.
- The client ID is written last, so restarted tasks find the matching
secret.
- Writes `/javabin/apps/{team}/{app}/auth-status` (String JSON, always
valid and under 4096 bytes: list fields are dropped, never sliced).
Fields: `status` (synced/invalid/error), `error`, `client_id`,
`client_id_changed_at` (carried over between syncs),
`identity_providers`, `pool_domain`, URLs, `drift_corrected` (fields a
reconcile reverted; also logged as `WARN drift`),
`unknown_groups`/`empty_groups` (checked against `group#` items in the
memberships table), `request_version`, `synced_at`.
- A declared IdP missing from the pool is #166's `PoolMisconfigured`
skip: status `error` in auth-status, the client is left as it is.
- Reconcile no longer stops at a failing app, and returns normally
instead of raising (an async invoke would retry the whole run twice).
The failure is logged as `Sync failed for …`, which #166's
`javabin-app-auth-sync-skipped` metric filter now also counts.
- Callback/logout URLs with a `#fragment` are rejected by the Lambda
too.
- IAM: `dynamodb:GetItem` on the memberships table, plus write access to
`*/cognito-client-identity-provider` and `*/auth-status`.

**Team access to auth-status**
- Teams read it through their existing `/javabin/apps/{team}/*` read
grant.
- Writes are denied for `auth-status` in the team-provisioner
permission-set policy (CI-applied), and in
`terraform/org/identity-center.tf` (human-applied; see user actions).

**app-secret-rollout**
- `cognito-client-id` and `cognito-client-identity-provider` are no
longer ignored. A replaced client (case 9/15) or an IdP cutover now
restarts the service.
- `auth-status` and `cognito-client-secret` are ignored: auth-status is
rewritten on every sync, and a new secret always comes with a new ID
written right after it, so restarting on the secret would deploy once
with the old ID.
- There is no restart loop, because unchanged values are never
rewritten.

**generate-modules.py**
- Callback/logout URLs must:
  - use `https`, except `http://localhost` / `127.0.0.1`;
  - have no `#fragment`;
- have a host equal to `routing.host` or listed in the new optional
`auth.extra_hosts`.
- `client: none` combined with `callback_urls`, `logout_urls`, `scopes`
or `extra_hosts` is rejected; these were silently ignored before.
- `COGNITO_IDENTITY_PROVIDER` is injected (an ECS secret from
`module.auth.identity_provider_arn`) and can be aliased through
`auth.env`.
- Auth resolution is factored into `resolve_auth()`, which the doctor
reuses.
- Both live apps (frosting, cupcake-backend) still generate. Tests use
their verbatim app.yaml.

**cognito-app-client module**
- New outputs `identity_provider_arn` and `auth_status_parameter_name`.

**scripts/auth-doctor.py**
- Uses team credentials only: app.yaml, the app's own SSM parameters,
and unauthenticated HTTP.
- Checks:
  - the request matches app.yaml;
- auth-status is synced for the current request version, and its client
ID matches `cognito-client-id`;
  - drift and unknown/empty groups are reported;
- `/oauth2/authorize` for every callback URL redirects to the IdP, not
`/error?error=redirect_mismatch` (it warns if Google isn't given
`prompt=select_account`);
- `/logout` for every logout URL redirects back (a mismatch lands on the
hosted UI `/login`, verified live);
- best effort, WARN only: the service's PRIMARY-deployment tasks started
after `client_id_changed_at` (skipped until auth-status records one;
`cognito-client-id`'s LastModifiedDate is useless because the old code
rewrote it on every reconcile).
- A non-JSON auth-status is a FAIL, not a traceback. When auth-status is
missing altogether, the doctor waits at most 30 s instead of the full
`--wait`.
- `--json` is the hook for a future `javabin auth doctor`. javabin-cli
is not changed here.
- `tf-apply.yml` gets a non-blocking `Auth doctor` step
(`continue-on-error`, `--wait 180`, skipped for apps without auth).

**platform-ci**
- After apply, `scripts/reconcile-app-auth.sh` queues an async
reconcile. This writes the new `cognito-client-identity-provider` and
`auth-status` parameters right away, so an app redeploy that picks up
the new `COGNITO_IDENTITY_PROVIDER` secret doesn't hit
`ParameterNotFound`. It is non-blocking, and the 03:30 reconcile is the
backstop.

**Docs**
- `docs/cognito-google-setup.md` is rewritten:
- exact GCP steps (Internal consent screen, a separate Web OAuth client,
redirect
`https://javabin-internal.auth.eu-central-1.amazoncognito.com/oauth2/idpresponse`;
the old doc listed a nonexistent `auth-internal.javazone.no`);
  - SSM commands;
  - the post-apply curl probe and the managed-login fallback;
  - cutover and user plan.
- `docs/cognito-saml-setup.md` is marked superseded. It also noted that
its groups mapping never existed.
- `docs/app-yaml-reference.md`: IdP list, URL rules and `extra_hosts`,
the `/logout` exact-match rule, group denial (`error_description`),
`COGNITO_IDENTITY_PROVIDER`, auth-status and the doctor.
- `docs/lambda-functions.md`, `docs/reusable-modules.md` and `CLAUDE.md`
are updated.

## How verified

- `python3 -m unittest discover -s terraform/lambda-src/tests` passes
(111, including #166's), and `-s scripts/tests` passes (67, including
the fmt check of generated TF). New tests cover:
  - no-op syncs write only auth-status;
  - secret written before ID on replacement;
- status content: invalid, error, drift vs request change, unknown/empty
groups;
- declared IdP list, cutover switching the IdP param without touching
the client ID, a missing IdP failing cleanly;
  - reconcile continuing past a failing app;
  - rollout restarting on `cognito-client-*`;
  - URL validation;
- doctor scenarios, including non-JSON status, the short wait for a
missing status, and draining/stale tasks;
- auth-status size limit (bytes, valid JSON), `client_id_changed_at`
carry-over, fragments rejected;
  - auth-claims refusing Google users without `email_verified=true`.
- `terraform fmt -recursive -check` and `terraform validate` (platform
root) pass.
- Live, read-only: the doctor against frosting's app.yaml.
- Request check passes. auth-status doesn't exist yet, as expected
before deploy.
- Probes against the live `cupcake-cupcake` client: callback →
GoogleSAML ✔, `/nope` → `redirect_mismatch` ✔, logout exact ✔, logout
without trailing slash → hosted UI `/login` (this is why the logout
check looks for "redirects back").
- Live pool state checked: 4 users, all `GoogleSAML_*` external-provider
users, no native users. `/javabin/apps/*` has only cupcake's `auth` and
`cognito-client-id`.

## Rollout & user actions

Merging (after #166) changes nothing about Google sign-in:
- The Google IdP isn't created without SSM credentials.
- The internal IdP list default is #166's `["GoogleSAML"]`, which equals
cupcake's live client (`SupportedIdentityProviders=[GoogleSAML]`). The
post-apply reconcile therefore does not put COGNITO back; on main
without #166, the old 03:30 reconcile would.
- On the first reconcile, cupcake gets `cognito-client-identity-provider
= GoogleSAML` (Create, so no restart) and an `auth-status`.

Expected plan: the `aws_ssm_parameters_by_path` read, and in-place
updates to the app-auth-sync role policy, function env/code,
app-secret-rollout code and team-provisioner code. No destroys.

User actions:
1. **Human apply `terraform/org`** (identity-center.tf). It adds
`auth-status` to the `javabin-developer` permission set's
platform-managed deny. Per-team permission sets get it from
team-provisioner on its next run.
2. **GCP (when ready):** create the Internal-consent OAuth client and
store it in SSM, following `docs/cognito-google-setup.md` §1–2. Then
re-run platform CI on main. The plan creates the `Google` IdP, and
clients are untouched.
3. **Verify:** run the §3 curl probe (or the doctor after cutover) to
confirm Cognito keeps `prompt=select_account`. If it doesn't, use the
documented managed-login fallback.
4. **Cutover (separate PR):** `internal_identity_providers =
["Google"]`. #166 (server-side domain check, no self sign-up) is a hard
prerequisite, which stacking on it guarantees.
- Users get a new `sub`. Platform memberships and groups are keyed by
email, so nothing to migrate there; cupcake should check for `sub`-keyed
data.
- Delete the 4 `GoogleSAML_*` users and the `GoogleSAML` IdP a week
later, and turn off the Google Admin SAML app.

## Interactions

- **#166 (lockdown):** this PR is stacked on it and reuses its variable,
output, env names and `identity_providers(pool, upid)`. It adds to
#166's code: the Google precondition on `internal_identity_providers`,
`EMAIL_VERIFIED_IDENTITY_PROVIDERS` in auth-claims, and a second pattern
on the skipped-sync metric filter. If #166 is squash-merged, I rebase
this branch onto main with `--onto`.
- **#158 / `feat/alb-login`:** both change `app_auth_sync`
(`client_settings`, `sync`, `_put_param`, `reconcile`) and
`generate-modules.py` auth handling. Expect textual conflicts; the slot
threading composes with this.
- Login slots should write `auth-status` and
`cognito-client-identity-provider` next to the slot request
(`…/login-N/`). The rollout regex already ignores slot params.
- service-routing's hardcoded `internal_identity_provider =
"GoogleSAML"` should go away: with a single IdP per client it is
unnecessary, and after cutover it points at a deleted IdP. Otherwise
read the slot's `cognito-client-identity-provider`.
- **#156:** also edits `app_secret_rollout/handler.py`, a small
conflict.

## Follow-ups

- The developer boundary (team CI roles, gate-vended apply credentials)
doesn't deny writing `auth-status`/`cognito-client-*`. It is at
5,946/6,144 bytes and #156 adds to it, so I left it alone; the
limitation is now documented in `docs/lambda-functions.md` and
`docs/app-yaml-reference.md`. The impact stays inside the team's own
app: a forged status, or one extra restart before the next sync puts a
credential back. A Deny in the team CI role policies is the follow-up if
we want it enforced.
- Drop `ALLOW_USER_SRP_AUTH` from internal clients. With #166 there is
no COGNITO and no self sign-up, so it is unused there, but changing
client auth flows is #166-adjacent scope.
- `javabin auth doctor` in javabin-cli, wrapping `scripts/auth-doctor.py
--json`.
- Doctor check that the task definition contains every `auth.env` alias;
canary-client probe in platform CI after identity applies.
- `auth.accept_clients` for `client: none` backends (case 13).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>

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