From a72a738a58398527555f28db59cb877e2f4bedcd Mon Sep 17 00:00:00 2001 From: Alexander Amiri Date: Thu, 26 Mar 2026 21:04:14 +0100 Subject: [PATCH 1/2] Keep original boundary description to avoid ForceNew replacement AWS provider treats IAM policy description changes as ForceNew, which would destroy and recreate the policy while it's still attached to roles. --- terraform/platform/iam/boundary.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terraform/platform/iam/boundary.tf b/terraform/platform/iam/boundary.tf index eaa2d79..7f998ad 100644 --- a/terraform/platform/iam/boundary.tf +++ b/terraform/platform/iam/boundary.tf @@ -23,7 +23,7 @@ locals { resource "aws_iam_policy" "developer_boundary" { name = "${var.project}-developer-boundary" - description = "Team permission boundary: org denies + ABAC isolation + naming enforcement." + description = "Permission boundary for all non-platform roles. Self-replicating: roles with this boundary can only create roles that also carry it." policy = jsonencode({ Version = "2012-10-17" From 4b7985717197a7d7f1c0f67994904ea954bec4be Mon Sep 17 00:00:00 2001 From: Alexander Amiri Date: Thu, 26 Mar 2026 21:07:13 +0100 Subject: [PATCH 2/2] Shorten boundary ARNs with wildcards to free 500+ bytes Use *:* for region:account in NotResource patterns since the boundary is account-scoped anyway. Frees ~500 bytes of headroom for future statements (was 2 bytes, now 508). --- terraform/platform/iam/boundary.tf | 42 ++++++++++++++++-------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/terraform/platform/iam/boundary.tf b/terraform/platform/iam/boundary.tf index 7f998ad..68c29c0 100644 --- a/terraform/platform/iam/boundary.tf +++ b/terraform/platform/iam/boundary.tf @@ -173,9 +173,9 @@ resource "aws_iam_policy" "developer_boundary" { Effect = "Deny" Action = ["ssm:GetParameter*", "ssm:GetParametersByPath"] Resource = [ - "arn:aws:ssm:${var.region}:${var.aws_account_id}:parameter/${var.project}/platform/*", - "arn:aws:ssm:${var.region}:${var.aws_account_id}:parameter/${var.project}/slack/*", - "arn:aws:ssm:${var.region}:${var.aws_account_id}:parameter/${var.project}/platform-overrides/*", + "arn:aws:ssm:*:*:parameter/${var.project}/platform/*", + "arn:aws:ssm:*:*:parameter/${var.project}/slack/*", + "arn:aws:ssm:*:*:parameter/${var.project}/platform-overrides/*", ] }, { @@ -183,8 +183,8 @@ resource "aws_iam_policy" "developer_boundary" { Effect = "Deny" Action = "iam:PassRole" NotResource = [ - "arn:aws:iam::${var.aws_account_id}:role/$${aws:PrincipalTag/team}-*", - "arn:aws:iam::${var.aws_account_id}:role/${var.project}-ecs-execution", + "arn:aws:iam::*:role/$${aws:PrincipalTag/team}-*", + "arn:aws:iam::*:role/${var.project}-ecs-execution", ] }, { @@ -192,8 +192,8 @@ resource "aws_iam_policy" "developer_boundary" { Effect = "Deny" Action = ["logs:GetLogEvents", "logs:FilterLogEvents", "logs:StartQuery", "logs:GetQueryResults"] NotResource = [ - "arn:aws:logs:${var.region}:${var.aws_account_id}:log-group:/ecs/$${aws:PrincipalTag/team}/*", - "arn:aws:logs:${var.region}:${var.aws_account_id}:log-group:/ecs/$${aws:PrincipalTag/team}/*:*", + "arn:aws:logs:*:*:log-group:/ecs/$${aws:PrincipalTag/team}/*", + "arn:aws:logs:*:*:log-group:/ecs/$${aws:PrincipalTag/team}/*:*", ] }, { @@ -232,20 +232,22 @@ resource "aws_iam_policy" "developer_boundary" { "lambda:CreateFunction", "events:PutRule", "states:CreateStateMachine", ] NotResource = [ + # S3 ARNs have no region/account "arn:aws:s3:::$${aws:PrincipalTag/team}-*", - "arn:aws:dynamodb:${var.region}:${var.aws_account_id}:table/$${aws:PrincipalTag/team}-*", - "arn:aws:sqs:${var.region}:${var.aws_account_id}:$${aws:PrincipalTag/team}-*", - "arn:aws:sns:${var.region}:${var.aws_account_id}:$${aws:PrincipalTag/team}-*", - "arn:aws:rds:${var.region}:${var.aws_account_id}:db:$${aws:PrincipalTag/team}-*", - "arn:aws:rds:${var.region}:${var.aws_account_id}:subgrp:$${aws:PrincipalTag/team}-*", - "arn:aws:ecs:${var.region}:${var.aws_account_id}:service/*/$${aws:PrincipalTag/team}-*", - "arn:aws:ecs:${var.region}:${var.aws_account_id}:task-definition/$${aws:PrincipalTag/team}-*:*", - "arn:aws:ecr:${var.region}:${var.aws_account_id}:repository/$${aws:PrincipalTag/team}-*", - "arn:aws:logs:${var.region}:${var.aws_account_id}:log-group:/ecs/$${aws:PrincipalTag/team}/*", - "arn:aws:ssm:${var.region}:${var.aws_account_id}:parameter/${var.project}/apps/$${aws:PrincipalTag/team}/*", - "arn:aws:lambda:${var.region}:${var.aws_account_id}:function:$${aws:PrincipalTag/team}-*", - "arn:aws:events:${var.region}:${var.aws_account_id}:rule/$${aws:PrincipalTag/team}-*", - "arn:aws:states:${var.region}:${var.aws_account_id}:stateMachine:$${aws:PrincipalTag/team}-*", + # Single-region account — wildcard region:account saves ~350 bytes + "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/${var.project}/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}-*", ] }, ]