Skip to content

Harden ABAC with two-tier boundary and team isolation#97

Merged
Alexanderamiri merged 2 commits into
mainfrom
feat/abac-hardening
Mar 26, 2026
Merged

Harden ABAC with two-tier boundary and team isolation#97
Alexanderamiri merged 2 commits into
mainfrom
feat/abac-hardening

Conversation

@Alexanderamiri
Copy link
Copy Markdown
Member

Summary

  • Split permission boundary into two tiers: org boundary (human-applied, structural denies) and developer boundary (CI-applied, org denies + team ABAC + naming enforcement)
  • Developer boundary adds 12 deny statements: DenyXTeam, DenyShared, DenyTagStrip, DenyPassRole, DenyXLogs, DenyModCI, DenyBadName, DenyNoTag, DenySecSvcs, DenyPlatSSM, DenyPlatInfra, plus structural denies
  • CI team roles simplified to AllowAll + backend access (boundary handles all deny logic)
  • Deploy roles trust changed from direct OIDC to broker-mediated (apply-gate Lambda only)
  • Deploy role scoping tightened: ECR {team}-*, PassRole {team}-*, logs /ecs/{team}/*
  • Bedrock denied for team roles (platform-only for plan review/cost narratives)
  • SG creation requires team tag via aws:RequestTag
  • Policy fits in 6,142/6,144 bytes

Test plan

  • terraform plan on terraform/platform/ — verify no unexpected destroys (moved block handles boundary rename)
  • terraform plan on terraform/org/ — verify org boundary changes
  • Verify developer boundary JSON < 6,144 bytes after variable substitution
  • Test team CI role can create resources with {team}-* prefix
  • Test team CI role CANNOT create resources with wrong prefix
  • Test deploy role can push to {team}-* ECR repo
  • Test deploy role CANNOT push to other team's ECR repo
  • Verify platform SSM secrets are unreadable by team roles

Two-tier permission boundary:
- Org boundary (human-applied): structural denies for all roles
- Developer boundary (CI-applied): org denies + team ABAC + naming enforcement

Developer boundary adds: DenyXTeam (cross-team tag isolation), DenyShared,
DenyTagStrip, DenyPassRole, DenyXLogs, DenyModCI, DenyBadName (naming),
DenyNoTag (SG tag enforcement), DenySecSvcs (security + Bedrock),
DenyPlatSSM, DenyPlatInfra

CI role changes:
- Team roles use developer boundary, AllowAll + backend (boundary does denies)
- Deploy roles trust via broker only (not direct OIDC)
- ECR scoped to {team}-*, PassRole to {team}-*, logs to /ecs/{team}/*
- Removed redundant inline deny policies (boundary handles it)
@Alexanderamiri Alexanderamiri requested a review from a team as a code owner March 26, 2026 19:37
The org boundary doesn't exist yet (human-applied), so the data source
fails during plan. Construct the ARN from variables instead.
@github-actions
Copy link
Copy Markdown

Terraform Plan

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

Plan output

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

Terraform will perform the following actions:

  # module.compute.aws_ecr_repository.ci["jvm"] will be updated in-place
  ~ resource "aws_ecr_repository" "ci" {
        id                   = "javabin-ci-jvm"
        name                 = "javabin-ci-jvm"
      ~ tags                 = {
            "Name" = "javabin-ci-jvm"
          - "team" = "javabin" -> null
        }
      ~ tags_all             = {
          ~ "team"        = "javabin" -> "platform"
            # (5 unchanged elements hidden)
        }
        # (4 unchanged attributes hidden)

        # (2 unchanged blocks hidden)
    }

  # module.compute.aws_ecr_repository.ci["platform"] will be updated in-place
  ~ resource "aws_ecr_repository" "ci" {
        id                   = "javabin-ci-platform"
        name                 = "javabin-ci-platform"
      ~ tags                 = {
            "Name" = "javabin-ci-platform"
          - "team" = "javabin" -> null
        }
      ~ tags_all             = {
          ~ "team"        = "javabin" -> "platform"
            # (5 unchanged elements hidden)
        }
        # (4 unchanged attributes hidden)

        # (2 unchanged blocks hidden)
    }

  # module.compute.aws_ecr_repository.ci["ts"] will be updated in-place
  ~ resource "aws_ecr_repository" "ci" {
        id                   = "javabin-ci-ts"
        name                 = "javabin-ci-ts"
      ~ tags                 = {
            "Name" = "javabin-ci-ts"
          - "team" = "javabin" -> null
        }
      ~ tags_all             = {
          ~ "team"        = "javabin" -> "platform"
            # (5 unchanged elements hidden)
        }
        # (4 unchanged attributes hidden)

        # (2 unchanged blocks hidden)
    }

  # module.iam.aws_iam_policy.developer_boundary will be created
  + resource "aws_iam_policy" "developer_boundary" {
      + arn              = (known after apply)
      + attachment_count = (known after apply)
      + description      = "Team permission boundary: org denies + ABAC isolation + naming enforcement."
      + id               = (known after apply)
      + name             = "javabin-developer-boundary"
      + name_prefix      = (known after apply)
      + path             = "/"
      + policy           = jsonencode(
            {
              + Statement = [
                  + {
                      + Action   = "*"
                      + Effect   = "Allow"
                      + Resource = "*"
                      + Sid      = "Allow"
                    },
                  + {
                      + Action   = [
                          + "iam:DeletePolicy",
                          + "iam:DeletePolicyVersion",
                          + "iam:CreatePolicyVersion",
                          + "iam:SetDefaultPolicyVersion",
                        ]
                      + Effect   = "Deny"
                      + Resource = "arn:aws:iam::553637109631:policy/javabin-developer-boundary"
                      + 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:PutRolePolicy",
                          + "iam:AttachRolePolicy",
                          + "iam:DetachRolePolicy",
                          + "route53:DeleteHostedZone",
                          + "sns:DeleteTopic",
                          + "sns:SetTopicAttributes",
                        ]
                      + 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:eu-central-1:553637109631:parameter/javabin/platform/*",
                          + "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/slack/*",
                          + "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/platform-overrides/*",
                        ]
                      + Sid      = "DenyPlatSSM"
                    },
                  + {
                      + Action      = "iam:PassRole"
                      + Effect      = "Deny"
                      + NotResource = [
                          + "arn:aws:iam::553637109631:role/${aws:PrincipalTag/team}-*",
                          + "arn:aws:iam::553637109631:role/javabin-ecs-execution",
                        ]
                      + Sid         = "DenyPassRole"
                    },
                  + {
                      + Action      = [
                          + "logs:GetLogEvents",
                          + "logs:FilterLogEvents",
                          + "logs:StartQuery",
                          + "logs:GetQueryResults",
                        ]
                      + Effect      = "Deny"
                      + NotResource = [
                          + "arn:aws:logs:eu-central-1:553637109631:log-group:/ecs/${aws:PrincipalTag/team}/*",
                          + "arn:aws:logs:eu-central-1:553637109631: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:eu-central-1:553637109631:table/${aws:PrincipalTag/team}-*",
                          + "arn:aws:sqs:eu-central-1:553637109631:${aws:PrincipalTag/team}-*",
                          + "arn:aws:sns:eu-central-1:553637109631:${aws:PrincipalTag/team}-*",
                          + "arn:aws:rds:eu-central-1:553637109631:db:${aws:PrincipalTag/team}-*",
                          + "arn:aws:rds:eu-central-1:553637109631:subgrp:${aws:PrincipalTag/team}-*",
                          + "arn:aws:ecs:eu-central-1:553637109631:service/*/${aws:PrincipalTag/team}-*",
                          + "arn:aws:ecs:eu-central-1:553637109631:task-definition/${aws:PrincipalTag/team}-*:*",
                          + "arn:aws:ecr:eu-central-1:553637109631:repository/${aws:PrincipalTag/team}-*",
                          + "arn:aws:logs:eu-central-1:553637109631:log-group:/ecs/${aws:PrincipalTag/team}/*",
                          + "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/apps/${aws:PrincipalTag/team}/*",
                          + "arn:aws:lambda:eu-central-1:553637109631:function:${aws:PrincipalTag/team}-*",
                          + "arn:aws:events:eu-central-1:553637109631:rule/${aws:PrincipalTag/team}-*",
                          + "arn:aws:states:eu-central-1:553637109631:stateMachine:${aws:PrincipalTag/team}-*",
                        ]
                      + Sid         = "DenyBadName"
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + policy_id        = (known after apply)
      + tags             = {
          + "Name" = "javabin-developer-boundary"
        }
      + tags_all         = {
          + "Name"        = "javabin-developer-boundary"
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.iam.aws_iam_role.ci_app_broker will be updated in-place
  ~ resource "aws_iam_role" "ci_app_broker" {
        id                    = "javabin-ci-app-broker"
        name                  = "javabin-ci-app-broker"
      ~ permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-developer-boundary" -> (known after apply)
      ~ tags                  = {
          - "team" = "javabin" -> null
        }
      ~ tags_all              = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (8 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.iam.aws_iam_role.ci_apply_gate will be updated in-place
  ~ resource "aws_iam_role" "ci_apply_gate" {
        id                    = "javabin-ci-apply-gate"
        name                  = "javabin-ci-apply-gate"
      ~ permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-developer-boundary" -> "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      ~ tags                  = {
            "Name" = "javabin-ci-apply-gate"
          - "team" = "javabin" -> null
        }
      ~ tags_all              = {
          ~ "team"        = "javabin" -> "platform"
            # (5 unchanged elements hidden)
        }
        # (8 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.iam.aws_iam_role.ci_deploy["testteam"] will be updated in-place
  ~ resource "aws_iam_role" "ci_deploy" {
        id                    = "javabin-ci-deploy-testteam"
        name                  = "javabin-ci-deploy-testteam"
      ~ permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-developer-boundary" -> (known after apply)
        tags                  = {
            "Name" = "javabin-ci-deploy-testteam"
            "team" = "testteam"
        }
        # (9 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.iam.aws_iam_role.ci_infra will be updated in-place
  ~ resource "aws_iam_role" "ci_infra" {
        id                    = "javabin-ci-infra"
        name                  = "javabin-ci-infra"
      ~ permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-developer-boundary" -> "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      ~ tags                  = {
            "Name" = "javabin-ci-infra"
          - "team" = "javabin" -> null
        }
      ~ tags_all              = {
          ~ "team"        = "javabin" -> "platform"
            # (5 unchanged elements hidden)
        }
        # (8 unchanged attributes hidden)

        # (2 unchanged blocks hidden)
    }

  # module.iam.aws_iam_role.ci_infra_plan will be updated in-place
  ~ resource "aws_iam_role" "ci_infra_plan" {
        id                    = "javabin-ci-infra-plan"
        name                  = "javabin-ci-infra-plan"
      ~ permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-developer-boundary" -> "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      ~ tags                  = {
            "Name" = "javabin-ci-infra-plan"
          - "team" = "javabin" -> null
        }
      ~ tags_all              = {
          ~ "team"        = "javabin" -> "platform"
            # (5 unchanged elements hidden)
        }
        # (8 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.iam.aws_iam_role.ci_override_approver will be updated in-place
  ~ resource "aws_iam_role" "ci_override_approver" {
        id                    = "javabin-ci-override-approver"
        name                  = "javabin-ci-override-approver"
      ~ permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-developer-boundary" -> "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      ~ tags                  = {
            "Name" = "javabin-ci-override-approver"
          - "team" = "javabin" -> null
        }
      ~ tags_all              = {
          ~ "team"        = "javabin" -> "platform"
            # (5 unchanged elements hidden)
        }
        # (8 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.iam.aws_iam_role.ci_registry will be updated in-place
  ~ resource "aws_iam_role" "ci_registry" {
        id                    = "javabin-ci-registry"
        name                  = "javabin-ci-registry"
      ~ permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-developer-boundary" -> "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      ~ tags                  = {
            "Name" = "javabin-ci-registry"
          - "team" = "javabin" -> null
        }
      ~ tags_all              = {
          ~ "team"        = "javabin" -> "platform"
            # (5 unchanged elements hidden)
        }
        # (8 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.iam.aws_iam_role.ci_team["testteam"] will be updated in-place
  ~ resource "aws_iam_role" "ci_team" {
        id                    = "javabin-ci-team-testteam"
        name                  = "javabin-ci-team-testteam"
      ~ permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-developer-boundary" -> (known after apply)
        tags                  = {
            "Name" = "javabin-ci-team-testteam"
            "team" = "testteam"
        }
        # (9 unchanged attributes hidden)

        # (2 unchanged blocks hidden)
    }

  # module.iam.aws_iam_role_policy.ci_deploy_ecr["testteam"] will be updated in-place
  ~ resource "aws_iam_role_policy" "ci_deploy_ecr" {
        id     = "javabin-ci-deploy-testteam:ecr-push"
        name   = "ecr-push"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                    {
                        Action   = [
                            "ecr:GetAuthorizationToken",
                        ]
                        Effect   = "Allow"
                        Resource = "*"
                        Sid      = "EcrAuth"
                    },
                  ~ {
                      ~ Resource = "arn:aws:ecr:eu-central-1:553637109631:repository/*" -> "arn:aws:ecr:eu-central-1:553637109631:repository/testteam-*"
                        # (3 unchanged attributes hidden)
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.iam.aws_iam_role_policy.ci_deploy_ecs["testteam"] will be updated in-place
  ~ resource "aws_iam_role_policy" "ci_deploy_ecs" {
        id     = "javabin-ci-deploy-testteam:ecs-deploy"
        name   = "ecs-deploy"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                    # (1 unchanged element hidden)
                    {
                        Action   = [
                            "ecs:DescribeTaskDefinition",
                            "ecs:RegisterTaskDefinition",
                            "ecs:DeregisterTaskDefinition",
                        ]
                        Effect   = "Allow"
                        Resource = "*"
                        Sid      = "EcsTaskDefinitionManagement"
                    },
                  ~ {
                      ~ Resource = [
                            "arn:aws:iam::553637109631:role/javabin-ecs-execution",
                          - "arn:aws:iam::553637109631:role/javabin-*",
                          + "arn:aws:iam::553637109631:role/testteam-*",
                        ]
                        # (3 unchanged attributes hidden)
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.iam.aws_iam_role_policy.ci_deploy_logs["testteam"] will be updated in-place
  ~ resource "aws_iam_role_policy" "ci_deploy_logs" {
        id     = "javabin-ci-deploy-testteam:cloudwatch-logs"
        name   = "cloudwatch-logs"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                  ~ {
                      ~ Resource = "arn:aws:logs:eu-central-1:553637109631:log-group:/ecs/javabin/*" -> [
                          + "arn:aws:logs:eu-central-1:553637109631:log-group:/ecs/testteam/*",
                          + "arn:aws:logs:eu-central-1:553637109631:log-group:/ecs/testteam/*:*",
                        ]
                        # (2 unchanged attributes hidden)
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.iam.aws_iam_role_policy.ci_team_allow["testteam"] will be updated in-place
  ~ resource "aws_iam_role_policy" "ci_team_allow" {
        id     = "javabin-ci-team-testteam:team-management"
        name   = "team-management"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                    {
                        Action   = "*"
                        Effect   = "Allow"
                        Resource = "*"
                        Sid      = "AllowAll"
                    },
                  - {
                      - 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         = "DenyCrossTeamAccess"
                    },
                  - {
                      - Action    = [
                          - "ec2:Delete*",
                          - "ec2:Modify*",
                          - "elasticloadbalancing:Delete*",
                          - "elasticloadbalancing:Modify*",
                          - "ecs:DeleteCluster",
                          - "ecs:UpdateCluster",
                          - "ecs:UpdateClusterSettings",
                          - "iam:DeleteRole",
                          - "iam:UpdateRole",
                          - "iam:DeleteRolePolicy",
                          - "iam:PutRolePolicy",
                          - "iam:AttachRolePolicy",
                          - "iam:DetachRolePolicy",
                          - "route53:DeleteHostedZone",
                          - "sns:DeleteTopic",
                          - "sns:SetTopicAttributes",
                        ]
                      - Condition = {
                          - StringEquals = {
                              - "aws:ResourceTag/team" = "shared"
                            }
                        }
                      - Effect    = "Deny"
                      - Resource  = "*"
                      - Sid       = "DenyMutateSharedInfra"
                    },
                    {
                        Action   = [
                            "s3:GetObject",
                            "s3:PutObject",
                            "s3:DeleteObject",
                            "s3:ListBucket",
                        ]
                        Effect   = "Allow"
                        Resource = [
                            "arn:aws:s3:::javabin-terraform-state-553637109631",
                            "arn:aws:s3:::javabin-terraform-state-553637109631/apps/testteam/*",
                        ]
                        Sid      = "AllowTerraformBackend"
                    },
                    # (1 unchanged element hidden)
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.iam.aws_iam_role_policy.ci_team_deny["testteam"] will be destroyed
  # (because aws_iam_role_policy.ci_team_deny is not in configuration)
  - resource "aws_iam_role_policy" "ci_team_deny" {
      - id     = "javabin-ci-team-testteam:deny-platform-operations" -> null
      - name   = "deny-platform-operations" -> null
      - policy = jsonencode(
            {
              - Statement = [
                  - {
                      - Action   = [
                          - "ec2:CreateVpc",
                          - "ec2:DeleteVpc",
                          - "ec2:ModifyVpcAttribute",
                          - "ec2:CreateSubnet",
                          - "ec2:DeleteSubnet",
                          - "ec2:CreateNatGateway",
                          - "ec2:DeleteNatGateway",
                          - "ec2:CreateInternetGateway",
                          - "ec2:DeleteInternetGateway",
                          - "ec2:AttachInternetGateway",
                          - "ec2:DetachInternetGateway",
                          - "ec2:CreateRouteTable",
                          - "ec2:DeleteRouteTable",
                        ]
                      - Effect   = "Deny"
                      - Resource = "*"
                      - Sid      = "DenyNetworkInfra"
                    },
                  - {
                      - Action   = [
                          - "guardduty:*",
                          - "securityhub:*",
                          - "config:*",
                          - "cloudtrail:*",
                        ]
                      - Effect   = "Deny"
                      - Resource = "*"
                      - Sid      = "DenySecurityServices"
                    },
                  - {
                      - Action   = [
                          - "organizations:*",
                          - "account:*",
                        ]
                      - Effect   = "Deny"
                      - Resource = "*"
                      - Sid      = "DenyOrgAndAccount"
                    },
                  - {
                      - Action   = [
                          - "iam:CreateUser",
                          - "iam:CreateAccessKey",
                          - "iam:CreateLoginProfile",
                        ]
                      - Effect   = "Deny"
                      - Resource = "*"
                      - Sid      = "DenyDangerousIAM"
                    },
                  - {
                      - Action   = [
                          - "sns:DeleteTopic",
                          - "sns:SetTopicAttributes",
                        ]
                      - Effect   = "Deny"
                      - Resource = [
                          - "arn:aws:sns:eu-central-1:553637109631:javabin-alerts",
                          - "arn:aws:sns:eu-central-1:553637109631:javabin-security",
                          - "arn:aws:sns:eu-central-1:553637109631:javabin-budget-enforcement",
                        ]
                      - Sid      = "DenyPlatformSNS"
                    },
                  - {
                      - Action   = [
                          - "s3:DeleteBucket",
                          - "s3:PutBucketPolicy",
                          - "s3:DeleteBucketPolicy",
                        ]
                      - Effect   = "Deny"
                      - Resource = [
                          - "arn:aws:s3:::javabin-terraform-state-553637109631",
                          - "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631",
                        ]
                      - Sid      = "DenyPlatformS3"
                    },
                  - {
                      - Action   = [
                          - "ecs:DeleteCluster",
                          - "ecs:UpdateCluster",
                          - "elasticloadbalancingv2:DeleteLoadBalancer",
                          - "elasticloadbalancingv2:ModifyLoadBalancerAttributes",
                        ]
                      - Effect   = "Deny"
                      - Resource = [
                          - "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform",
                          - "arn:aws:elasticloadbalancingv2:eu-central-1:553637109631:loadbalancer/app/javabin-*",
                        ]
                      - Sid      = "DenyPlatformCompute"
                    },
                ]
              - Version   = "2012-10-17"
            }
        ) -> null
      - role   = "javabin-ci-team-testteam" -> null
    }

  # module.identity.aws_cognito_user_pool.external will be updated in-place
  ~ resource "aws_cognito_user_pool" "external" {
        id                        = "eu-central-1_gdFOsE4EM"
        name                      = "javabin-external"
      ~ tags                      = {
            "Name" = "javabin-external"
          - "team" = "javabin" -> null
        }
      ~ tags_all                  = {
          ~ "team"        = "javabin" -> "platform"
            # (5 unchanged elements hidden)
        }
        # (10 unchanged attributes hidden)

        # (7 unchanged blocks hidden)
    }

  # module.identity.aws_cognito_user_pool.internal will be updated in-place
  ~ resource "aws_cognito_user_pool" "internal" {
        id                        = "eu-central-1_Icikv3dtD"
        name                      = "javabin-internal"
      ~ tags                      = {
            "Name" = "javabin-internal"
          - "team" = "javabin" -> null
        }
      ~ tags_all                  = {
          ~ "team"        = "javabin" -> "platform"
            # (5 unchanged elements hidden)
        }
        # (11 unchanged attributes hidden)

        # (7 unchanged blocks hidden)
    }

  # module.ingress.aws_acm_certificate.wildcard will be updated in-place
  ~ resource "aws_acm_certificate" "wildcard" {
        id                        = "arn:aws:acm:eu-central-1:553637109631:certificate/9b79f56a-3719-4c62-8970-6f08985a7e5b"
      ~ tags                      = {
            "Name" = "javazone.no-wildcard"
          - "team" = "javabin" -> null
        }
      ~ tags_all                  = {
          ~ "team"        = "javabin" -> "platform"
            # (5 unchanged elements hidden)
        }
        # (14 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.ingress.aws_lb_listener.http_redirect will be updated in-place
  ~ resource "aws_lb_listener" "http_redirect" {
        id                                   = "arn:aws:elasticloadbalancing:eu-central-1:553637109631:listener/app/javabin-platform-alb/bec1dd43ab8341b9/1d92e19ae75aa59b"
      ~ tags                                 = {
          - "team" = "javabin" -> null
        }
      ~ tags_all                             = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (5 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.lambdas.aws_cloudwatch_event_rule.compliance_reporter_trigger will be updated in-place
  ~ resource "aws_cloudwatch_event_rule" "compliance_reporter_trigger" {
        id             = "javabin-compliance-reporter-trigger"
        name           = "javabin-compliance-reporter-trigger"
      ~ tags           = {
          - "team" = "javabin" -> null
        }
      ~ tags_all       = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (7 unchanged attributes hidden)
    }

  # module.lambdas.aws_cloudwatch_event_rule.cost_report_schedule will be updated in-place
  ~ resource "aws_cloudwatch_event_rule" "cost_report_schedule" {
        id                  = "javabin-cost-report-schedule"
        name                = "javabin-cost-report-schedule"
      ~ tags                = {
          - "team" = "javabin" -> null
        }
      ~ tags_all            = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (7 unchanged attributes hidden)
    }

  # module.lambdas.aws_cloudwatch_event_rule.daily_cost_check_schedule will be updated in-place
  ~ resource "aws_cloudwatch_event_rule" "daily_cost_check_schedule" {
        id                  = "javabin-daily-cost-check-schedule"
        name                = "javabin-daily-cost-check-schedule"
      ~ tags                = {
          - "team" = "javabin" -> null
        }
      ~ tags_all            = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (7 unchanged attributes hidden)
    }

  # module.lambdas.aws_cloudwatch_event_rule.override_cleanup_schedule will be updated in-place
  ~ resource "aws_cloudwatch_event_rule" "override_cleanup_schedule" {
        id                  = "javabin-override-cleanup-schedule"
        name                = "javabin-override-cleanup-schedule"
      ~ tags                = {
          - "team" = "javabin" -> null
        }
      ~ tags_all            = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (7 unchanged attributes hidden)
    }

  # module.lambdas.aws_cloudwatch_event_rule.resource_tagger_trigger will be updated in-place
  ~ resource "aws_cloudwatch_event_rule" "resource_tagger_trigger" {
        id             = "javabin-resource-tagger-trigger"
        name           = "javabin-resource-tagger-trigger"
      ~ tags           = {
          - "team" = "javabin" -> null
        }
      ~ tags_all       = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (7 unchanged attributes hidden)
    }

  # module.lambdas.aws_cloudwatch_event_rule.securityhub_summary_schedule will be updated in-place
  ~ resource "aws_cloudwatch_event_rule" "securityhub_summary_schedule" {
        id                  = "javabin-securityhub-summary-schedule"
        name                = "javabin-securityhub-summary-schedule"
      ~ tags                = {
          - "team" = "javabin" -> null
        }
      ~ tags_all            = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (7 unchanged attributes hidden)
    }

  # module.lambdas.aws_iam_role.apply_gate will be updated in-place
  ~ resource "aws_iam_role" "apply_gate" {
        id                    = "javabin-apply-gate"
        name                  = "javabin-apply-gate"
      ~ permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-developer-boundary" -> "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      ~ tags                  = {
          - "team" = "javabin" -> null
        }
      ~ tags_all              = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (8 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.lambdas.aws_iam_role.budget_enforcer will be updated in-place
  ~ resource "aws_iam_role" "budget_enforcer" {
        id                    = "javabin-budget-enforcer"
        name                  = "javabin-budget-enforcer"
      ~ permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-developer-boundary" -> "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      ~ tags                  = {
          - "team" = "javabin" -> null
        }
      ~ tags_all              = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (8 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.lambdas.aws_iam_role.ci_broker will be updated in-place
  ~ resource "aws_iam_role" "ci_broker" {
        id                    = "javabin-ci-broker"
        name                  = "javabin-ci-broker"
      ~ permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-developer-boundary" -> "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      ~ tags                  = {
          - "team" = "javabin" -> null
        }
      ~ tags_all              = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (8 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.lambdas.aws_iam_role.compliance_reporter will be updated in-place
  ~ resource "aws_iam_role" "compliance_reporter" {
        id                    = "javabin-compliance-reporter"
        name                  = "javabin-compliance-reporter"
      ~ tags                  = {
          - "team" = "javabin" -> null
        }
      ~ tags_all              = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (8 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.lambdas.aws_iam_role.cost_report will be updated in-place
  ~ resource "aws_iam_role" "cost_report" {
        id                    = "javabin-cost-report"
        name                  = "javabin-cost-report"
      ~ tags                  = {
          - "team" = "javabin" -> null
        }
      ~ tags_all              = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (8 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.lambdas.aws_iam_role.daily_cost_check will be updated in-place
  ~ resource "aws_iam_role" "daily_cost_check" {
        id                    = "javabin-daily-cost-check"
        name                  = "javabin-daily-cost-check"
      ~ tags                  = {
          - "team" = "javabin" -> null
        }
      ~ tags_all              = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (8 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.lambdas.aws_iam_role.override_cleanup will be updated in-place
  ~ resource "aws_iam_role" "override_cleanup" {
        id                    = "javabin-override-cleanup"
        name                  = "javabin-override-cleanup"
      ~ tags                  = {
          - "team" = "javabin" -> null
        }
      ~ tags_all              = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (8 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.lambdas.aws_iam_role.password_set will be updated in-place
  ~ resource "aws_iam_role" "password_set" {
        id                    = "javabin-password-set"
        name                  = "javabin-password-set"
      ~ permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-developer-boundary" -> "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      ~ tags                  = {
          - "team" = "javabin" -> null
        }
      ~ tags_all              = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (8 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.lambdas.aws_iam_role.resource_tagger will be updated in-place
  ~ resource "aws_iam_role" "resource_tagger" {
        id                    = "javabin-resource-tagger"
        name                  = "javabin-resource-tagger"
      ~ permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-developer-boundary" -> "arn:aws:iam::553637109631:policy/javabin-org-boundary"
      ~ tags                  = {
          - "team" = "javabin" -> null
        }
      ~ tags_all              = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (8 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.lambdas.aws_iam_role.slack_alert will be updated in-place
  ~ resource "aws_iam_role" "slack_alert" {
        id                    = "javabin-slack-alert"
        name                  = "javabin-slack-alert"
      ~ tags                  = {
          - "team" = "javabin" -> null
        }
      ~ tags_all              = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (8 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.lambdas.aws_iam_role.team_provisioner will be updated in-place
  ~ resource "aws_iam_role" "team_provisioner" {
        id                    = "javabin-team-provisioner"
        name                  = "javabin-team-provisioner"
      ~ tags                  = {
          - "team" = "javabin" -> null
        }
      ~ tags_all              = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (8 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # module.lambdas.aws_lambda_function.apply_gate will be updated in-place
  ~ resource "aws_lambda_function" "apply_gate" {
        id                             = "javabin-apply-gate"
      ~ last_modified                  = "2026-03-18T00:33:55.000+0000" -> (known after apply)
      ~ source_code_hash               = "UIWBB5JjUuxNGDEzzQ+vqN2HzkpT9TXXMugFBFyQtJA=" -> "pyoe3WRu45ZNOgrQOFgtdKaWnjrGd3WUlNifp7b/XCo="
      ~ tags                           = {
          - "team" = "javabin" -> null
        }
      ~ tags_all                       = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (20 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_function.budget_enforcer will be updated in-place
  ~ resource "aws_lambda_function" "budget_enforcer" {
        id                             = "javabin-budget-enforcer"
      ~ tags                           = {
          - "team" = "javabin" -> null
        }
      ~ tags_all                       = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (22 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_function.ci_broker will be updated in-place
  ~ resource "aws_lambda_function" "ci_broker" {
        id                             = "javabin-ci-broker"
      ~ last_modified                  = "2026-03-18T00:33:49.000+0000" -> (known after apply)
      ~ source_code_hash               = "7RJbLLtcXuSIUkIYZbx+bcKW17c0IARtwitmYk5mCoU=" -> "kSbI1haf+LJvONXl80gqkMLsGb1HPvvrqy6aC7mWXZQ="
      ~ tags                           = {
          - "team" = "javabin" -> null
        }
      ~ tags_all                       = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (20 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_function.compliance_reporter will be updated in-place
  ~ resource "aws_lambda_function" "compliance_reporter" {
        id                             = "javabin-compliance-reporter"
      ~ last_modified                  = "2026-03-17T19:19:43.132+0000" -> (known after apply)
      ~ source_code_hash               = "HmB1vbZCo8eBUTqNnKhoz0LzYQ/QBeSXN+KF390S/2c=" -> "NkzoGnYQCnG8BbKoHzVSvDqxAipFexXz/n+v2/6ZgrU="
      ~ tags                           = {
          - "team" = "javabin" -> null
        }
      ~ tags_all                       = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (20 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_function.cost_report will be updated in-place
  ~ resource "aws_lambda_function" "cost_report" {
        id                             = "javabin-cost-report"
      ~ tags                           = {
          - "team" = "javabin" -> null
        }
      ~ tags_all                       = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (22 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_function.daily_cost_check will be updated in-place
  ~ resource "aws_lambda_function" "daily_cost_check" {
        id                             = "javabin-daily-cost-check"
      ~ tags                           = {
          - "team" = "javabin" -> null
        }
      ~ tags_all                       = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (22 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_function.override_cleanup will be updated in-place
  ~ resource "aws_lambda_function" "override_cleanup" {
        id                             = "javabin-override-cleanup"
      ~ tags                           = {
          - "team" = "javabin" -> null
        }
      ~ tags_all                       = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (22 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_function.password_set will be updated in-place
  ~ resource "aws_lambda_function" "password_set" {
        id                             = "javabin-password-set"
      ~ tags                           = {
          - "team" = "javabin" -> null
        }
      ~ tags_all                       = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (22 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_function.resource_tagger will be updated in-place
  ~ resource "aws_lambda_function" "resource_tagger" {
        id                             = "javabin-resource-tagger"
      ~ tags                           = {
          - "team" = "javabin" -> null
        }
      ~ tags_all                       = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (22 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_function.securityhub_summary will be updated in-place
  ~ resource "aws_lambda_function" "securityhub_summary" {
        id                             = "javabin-securityhub-summary"
      ~ last_modified                  = "2026-03-17T19:19:55.000+0000" -> (known after apply)
      ~ source_code_hash               = "LEUBnHrV2lMxv4O53MGv3xojmz1Pwru9JCHtO6Kg4f0=" -> "//EAo2qOP1ujBcWSwzIrY7e+x0ayJTBlQZWAO/RzIO4="
      ~ tags                           = {
          - "team" = "javabin" -> null
        }
      ~ tags_all                       = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (20 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_function.slack_alert will be updated in-place
  ~ resource "aws_lambda_function" "slack_alert" {
        id                             = "javabin-slack-alert"
      ~ last_modified                  = "2026-03-17T19:20:01.000+0000" -> (known after apply)
      ~ source_code_hash               = "LEUBnHrV2lMxv4O53MGv3xojmz1Pwru9JCHtO6Kg4f0=" -> "//EAo2qOP1ujBcWSwzIrY7e+x0ayJTBlQZWAO/RzIO4="
      ~ tags                           = {
          - "team" = "javabin" -> null
        }
      ~ tags_all                       = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (20 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_function.team_provisioner will be updated in-place
  ~ resource "aws_lambda_function" "team_provisioner" {
        id                             = "javabin-team-provisioner"
      ~ tags                           = {
          - "team" = "javabin" -> null
        }
      ~ tags_all                       = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (22 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lb_listener_rule.password_set will be updated in-place
  ~ resource "aws_lb_listener_rule" "password_set" {
        id           = "arn:aws:elasticloadbalancing:eu-central-1:553637109631:listener-rule/app/javabin-platform-alb/bec1dd43ab8341b9/500c9c2b4186bf45/38c91f257c910d33"
      ~ tags         = {
          - "team" = "javabin" -> null
        }
      ~ tags_all     = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (3 unchanged attributes hidden)

        # (2 unchanged blocks hidden)
    }

  # module.lambdas.aws_lb_target_group.password_set will be updated in-place
  ~ resource "aws_lb_target_group" "password_set" {
        id                                 = "arn:aws:elasticloadbalancing:eu-central-1:553637109631:targetgroup/javabin-password-set/23a67a9e9f399518"
        name                               = "javabin-password-set"
      ~ tags                               = {
          - "team" = "javabin" -> null
        }
      ~ tags_all                           = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (9 unchanged attributes hidden)

        # (5 unchanged blocks hidden)
    }

  # module.lambdas.aws_sns_topic.budget_enforcement will be updated in-place
  ~ resource "aws_sns_topic" "budget_enforcement" {
        id                                       = "arn:aws:sns:eu-central-1:553637109631:javabin-budget-enforcement"
        name                                     = "javabin-budget-enforcement"
      ~ tags                                     = {
          - "team" = "javabin" -> null
        }
      ~ tags_all                                 = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (11 unchanged attributes hidden)
    }

  # module.lambdas.aws_ssm_parameter.password_set_function_url will be updated in-place
  ~ resource "aws_ssm_parameter" "password_set_function_url" {
        id        = "/javabin/platform/password-set-function-url"
        name      = "/javabin/platform/password-set-function-url"
      ~ tags      = {
          - "team" = "javabin" -> null
        }
      ~ tags_all  = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (6 unchanged attributes hidden)
    }

  # module.monitoring.aws_ce_anomaly_monitor.main will be updated in-place
  ~ resource "aws_ce_anomaly_monitor" "main" {
        id                = "arn:aws:ce::553637109631:anomalymonitor/3609b3f1-c834-444e-a218-02ac6da1cb4d"
        name              = "javabin-anomaly-monitor"
      ~ tags              = {
          - "team" = "javabin" -> null
        }
      ~ tags_all          = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (3 unchanged attributes hidden)
    }

  # module.monitoring.aws_ce_anomaly_subscription.alerts will be updated in-place
  ~ resource "aws_ce_anomaly_subscription" "alerts" {
        id               = "arn:aws:ce::553637109631:anomalysubscription/f6b079c9-5174-43b7-85f3-dde533995482"
        name             = "javabin-anomaly-alerts"
      ~ tags             = {
          - "team" = "javabin" -> null
        }
      ~ tags_all         = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (4 unchanged attributes hidden)

        # (2 unchanged blocks hidden)
    }

  # module.monitoring.aws_cloudwatch_event_rule.config_compliance will be updated in-place
  ~ resource "aws_cloudwatch_event_rule" "config_compliance" {
        id             = "javabin-config-compliance-change"
        name           = "javabin-config-compliance-change"
      ~ tags           = {
          - "team" = "javabin" -> null
        }
      ~ tags_all       = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (7 unchanged attributes hidden)
    }

  # module.monitoring.aws_cloudwatch_event_rule.console_login will be updated in-place
  ~ resource "aws_cloudwatch_event_rule" "console_login" {
        id             = "javabin-console-login"
        name           = "javabin-console-login"
      ~ tags           = {
          - "team" = "javabin" -> null
        }
      ~ tags_all       = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (7 unchanged attributes hidden)
    }

  # module.monitoring.aws_cloudwatch_event_rule.guardduty_findings will be updated in-place
  ~ resource "aws_cloudwatch_event_rule" "guardduty_findings" {
        id             = "javabin-guardduty-findings"
        name           = "javabin-guardduty-findings"
      ~ tags           = {
          - "team" = "javabin" -> null
        }
      ~ tags_all       = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (7 unchanged attributes hidden)
    }

  # module.monitoring.aws_cloudwatch_event_rule.iam_changes will be updated in-place
  ~ resource "aws_cloudwatch_event_rule" "iam_changes" {
        id             = "javabin-iam-changes"
        name           = "javabin-iam-changes"
      ~ tags           = {
          - "team" = "javabin" -> null
        }
      ~ tags_all       = {
          ~ "team"        = "javabin" -> "platform"
            # (4 unchanged elements hidden)
        }
        # (7 unchanged attributes hidden)
    }

  # module.monitoring.aws_cloudwatch_event_rule.resource_creation will be updated in-place
  ~ resource "aws_cloudwatch_event_rule" "resource_creation" {
        id             = "javabin-resource-creat

LLM Review

Risk: 🟡 MEDIUM

Terraform plan updates tags across 50+ resources from "javabin" to "platform", creates a new developer permission boundary policy, updates IAM role permission boundaries, and removes a team-specific deny policy.

  • 🔒 [security] New developer permission boundary policy created with comprehensive deny statements. While well-structured with ABAC isolation and naming enforcement, the broad 'Allow *' statement combined with specific denies requires careful review to ensure it doesn't inadvertently grant unintended permissions.
  • 🔒 [security] Multiple IAM roles switching permission boundaries from 'javabin-developer-boundary' to 'javabin-org-boundary' (ci_infra, ci_apply_gate, ci_override_approver, ci_registry, password_set, resource_tagger, budget_enforcer, ci_broker, apply_gate). This represents a significant permission model change that should be validated.
  • 💥 [destruction] Removal of 'ci_team_deny' policy for testteam role. This policy contained explicit denies for network infrastructure, security services, and dangerous IAM operations. Removal shifts enforcement to the new boundary policy—verify the new policy provides equivalent protection.
  • [routine] Bulk tag updates changing 'team' tag from 'javabin' to 'platform' across ~50 resources (ECR repos, Lambda functions, Cognito pools, load balancers, SNS topics, etc.). This is a tagging/organizational change with no functional impact.
  • [routine] Lambda function source code hash updates for apply_gate, ci_broker, compliance_reporter, securityhub_summary, and slack_alert indicate code changes. These are routine updates with no infrastructure risk.

@Alexanderamiri Alexanderamiri merged commit 8db9406 into main Mar 26, 2026
3 checks passed
@Alexanderamiri Alexanderamiri deleted the feat/abac-hardening branch March 26, 2026 19:53
Alexanderamiri added a commit that referenced this pull request May 9, 2026
## Summary
- Split permission boundary into two tiers: **org boundary**
(human-applied, structural denies) and **developer boundary**
(CI-applied, org denies + team ABAC + naming enforcement)
- Developer boundary adds 12 deny statements: DenyXTeam, DenyShared,
DenyTagStrip, DenyPassRole, DenyXLogs, DenyModCI, DenyBadName,
DenyNoTag, DenySecSvcs, DenyPlatSSM, DenyPlatInfra, plus structural
denies
- CI team roles simplified to AllowAll + backend access (boundary
handles all deny logic)
- Deploy roles trust changed from direct OIDC to broker-mediated
(apply-gate Lambda only)
- Deploy role scoping tightened: ECR `{team}-*`, PassRole `{team}-*`,
logs `/ecs/{team}/*`
- Bedrock denied for team roles (platform-only for plan review/cost
narratives)
- SG creation requires team tag via `aws:RequestTag`
- Policy fits in 6,142/6,144 bytes

## Test plan
- [ ] `terraform plan` on `terraform/platform/` — verify no unexpected
destroys (moved block handles boundary rename)
- [ ] `terraform plan` on `terraform/org/` — verify org boundary changes
- [ ] Verify developer boundary JSON < 6,144 bytes after variable
substitution
- [ ] Test team CI role can create resources with `{team}-*` prefix
- [ ] Test team CI role CANNOT create resources with wrong prefix
- [ ] Test deploy role can push to `{team}-*` ECR repo
- [ ] Test deploy role CANNOT push to other team's ECR repo
- [ ] Verify platform SSM secrets are unreadable by team roles
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