From 0c129c3906c25a8cbf0f5ee0235fa78e9b22495c Mon Sep 17 00:00:00 2001 From: Alexander Amiri Date: Tue, 17 Mar 2026 22:32:40 +0100 Subject: [PATCH] Move permission boundary to terraform/org/ (human-applied) The boundary's self-protection (DenyBoundaryTampering) prevents the CI pipeline from modifying it. Move the resource to terraform/org/ which is human-applied with admin credentials. Migration steps (must be done manually with --profile javabin): Step 1: Import boundary into org state cd terraform/org terraform import aws_iam_policy.developer_boundary \ arn:aws:iam::553637109631:policy/javabin-developer-boundary Step 2: Apply org to verify no changes terraform plan # should show no changes terraform apply Step 3: Remove boundary from platform state cd terraform/platform terraform state rm module.iam.aws_iam_policy.developer_boundary Step 4: Replace resource with data source in platform/iam/boundary.tf (separate PR after state migration) The platform/iam/boundary.tf resource is kept temporarily to prevent CI from destroying it. It will be replaced with a data source in step 4. --- terraform/org/boundary.tf | 248 +++++++++++++++++++++++++++++ terraform/platform/iam/boundary.tf | 21 +-- 2 files changed, 253 insertions(+), 16 deletions(-) create mode 100644 terraform/org/boundary.tf diff --git a/terraform/org/boundary.tf b/terraform/org/boundary.tf new file mode 100644 index 0000000..e5d7181 --- /dev/null +++ b/terraform/org/boundary.tf @@ -0,0 +1,248 @@ +################################################################################ +# Permission Boundary: javabin-developer-boundary +# +# This is the cornerstone of the IAM security model. Any role carrying this +# boundary can only create roles that also carry it (self-replicating). +# +# The boundary defines the maximum possible permissions for any non-platform +# role. Hard denies here cannot be overridden by any policy attached to +# roles that carry this boundary. +# +# IMPORTANT: This resource lives in terraform/org/ (human-applied) because +# the boundary's self-protection denies iam:CreatePolicyVersion on itself. +# The CI pipeline (which carries the boundary) cannot modify it. +################################################################################ + +resource "aws_iam_policy" "developer_boundary" { + name = "${var.project}-developer-boundary" + 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" + Statement = [ + ######################################################################## + # Allow everything not explicitly denied below + ######################################################################## + { + Sid = "AllowAll" + Effect = "Allow" + Action = "*" + Resource = "*" + }, + + ######################################################################## + # Self-replicating: deny creating/modifying roles without this boundary + ######################################################################## + { + Sid = "DenyRolesWithoutBoundary" + Effect = "Deny" + Action = [ + "iam:CreateRole", + "iam:PutRolePermissionsBoundary" + ] + Resource = "*" + Condition = { + StringNotEquals = { + "iam:PermissionsBoundary" = "arn:aws:iam::${var.aws_account_id}:policy/${var.project}-developer-boundary" + } + } + }, + + ######################################################################## + # Deny creating IAM users and access keys (console/programmatic) + ######################################################################## + { + Sid = "DenyIAMUserCreation" + Effect = "Deny" + Action = [ + "iam:CreateUser", + "iam:CreateLoginProfile", + "iam:UpdateLoginProfile", + "iam:CreateAccessKey", + "iam:DeleteAccountPasswordPolicy", + "iam:CreateVirtualMFADevice", + "iam:DeactivateMFADevice" + ] + Resource = "*" + }, + + ######################################################################## + # Deny modifying or deleting this boundary policy itself + ######################################################################## + { + Sid = "DenyBoundaryTampering" + Effect = "Deny" + Action = [ + "iam:DeletePolicy", + "iam:DeletePolicyVersion", + "iam:CreatePolicyVersion", + "iam:SetDefaultPolicyVersion" + ] + Resource = "arn:aws:iam::${var.aws_account_id}:policy/${var.project}-developer-boundary" + }, + { + Sid = "DenyDeleteRoleBoundary" + Effect = "Deny" + Action = [ + "iam:DeleteRolePermissionsBoundary" + ] + Resource = "*" + }, + + ######################################################################## + # Deny IAM Identity Center, Organizations, SCPs + ######################################################################## + { + Sid = "DenyIdentityCenterAndOrgs" + Effect = "Deny" + Action = [ + "organizations:*", + "sso:*", + "sso-directory:*", + "identitystore:*", + "account:*" + ] + Resource = "*" + }, + + ######################################################################## + # Deny disabling/deleting protective services + ######################################################################## + { + Sid = "DenyProtectiveServicesTampering" + Effect = "Deny" + Action = [ + "guardduty:DeleteDetector", + "guardduty:DeleteMembers", + "guardduty:DisassociateFromMasterAccount", + "guardduty:UpdateDetector", + "config:DeleteConfigurationRecorder", + "config:StopConfigurationRecorder", + "config:DeleteDeliveryChannel", + "config:DeleteRetentionConfiguration", + "securityhub:DisableSecurityHub", + "securityhub:DeleteMembers", + "securityhub:DisassociateFromMasterAccount", + "cloudtrail:DeleteTrail", + "cloudtrail:StopLogging", + "cloudtrail:UpdateTrail", + "cloudtrail:PutEventSelectors" + ] + Resource = "*" + }, + + ######################################################################## + # Deny platform networking: VPC, subnets, IGW, NAT gateway + ######################################################################## + { + Sid = "DenyPlatformNetworking" + Effect = "Deny" + 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" + ] + Resource = "*" + }, + + ######################################################################## + # Protect platform security groups + # + # Teams CAN create security groups (needed for RDS, custom services). + # Teams CANNOT modify or delete platform-owned security groups. + # Platform SGs are named javabin-* (e.g., javabin-alb-sg, javabin-ecs-tasks-sg). + # App SGs use app-name prefix (e.g., moresleep-rds-sg). + ######################################################################## + { + Sid = "DenyPlatformSecurityGroups" + Effect = "Deny" + Action = [ + "ec2:DeleteSecurityGroup", + "ec2:AuthorizeSecurityGroupIngress", + "ec2:RevokeSecurityGroupIngress", + "ec2:AuthorizeSecurityGroupEgress", + "ec2:RevokeSecurityGroupEgress", + "ec2:ModifySecurityGroupRules", + ] + Resource = "arn:aws:ec2:${var.region}:${var.aws_account_id}:security-group/*" + Condition = { + StringLike = { + "ec2:ResourceTag/Name" = "${var.project}-*" + } + } + }, + + ######################################################################## + # Deny platform ECS cluster, ALB, ACM certs + ######################################################################## + { + Sid = "DenyPlatformECSCluster" + Effect = "Deny" + Action = [ + "ecs:DeleteCluster", + "ecs:UpdateCluster" + ] + Resource = "arn:aws:ecs:${var.region}:${var.aws_account_id}:cluster/${var.project}-platform" + }, + { + Sid = "DenyPlatformALB" + Effect = "Deny" + Action = [ + "elasticloadbalancing:DeleteLoadBalancer", + "elasticloadbalancing:ModifyLoadBalancerAttributes" + ] + Resource = "arn:aws:elasticloadbalancing:${var.region}:${var.aws_account_id}:loadbalancer/app/${var.project}-*" + }, + { + Sid = "DenyPlatformACM" + Effect = "Deny" + Action = [ + "acm:DeleteCertificate" + ] + Resource = "arn:aws:acm:${var.region}:${var.aws_account_id}:certificate/*" + }, + + ######################################################################## + # Deny access to state and CI artifact buckets/tables + ######################################################################## + { + Sid = "DenyStateBuckets" + Effect = "Deny" + Action = [ + "s3:DeleteBucket", + "s3:PutBucketPolicy", + "s3:DeleteBucketPolicy", + "s3:PutBucketVersioning", + "s3:PutEncryptionConfiguration" + ] + Resource = [ + "arn:aws:s3:::${var.project}-terraform-*", + "arn:aws:s3:::${var.project}-ci-*" + ] + }, + { + Sid = "DenyStateTables" + Effect = "Deny" + Action = [ + "dynamodb:DeleteTable", + "dynamodb:UpdateTable" + ] + Resource = "arn:aws:dynamodb:${var.region}:${var.aws_account_id}:table/${var.project}-terraform-*" + } + ] + }) + + tags = { + Name = "${var.project}-developer-boundary" + } +} diff --git a/terraform/platform/iam/boundary.tf b/terraform/platform/iam/boundary.tf index 57fb82d..65fca8c 100644 --- a/terraform/platform/iam/boundary.tf +++ b/terraform/platform/iam/boundary.tf @@ -1,12 +1,12 @@ ################################################################################ # Permission Boundary: javabin-developer-boundary # -# This is the cornerstone of the IAM security model. Any role carrying this -# boundary can only create roles that also carry it (self-replicating). +# MIGRATION NOTE: This resource is being moved to terraform/org/boundary.tf +# because the boundary's self-protection (DenyBoundaryTampering) prevents +# the CI pipeline from modifying it. See docs/org-runbook.md for migration steps. # -# The boundary defines the maximum possible permissions for any non-platform -# role. Hard denies here cannot be overridden by any policy attached to -# roles that carry this boundary. +# Once the state migration is complete, this file will be replaced with a +# data source reference. Until then, keep this resource to avoid CI destroying it. ################################################################################ resource "aws_iam_policy" "developer_boundary" { @@ -103,12 +103,6 @@ resource "aws_iam_policy" "developer_boundary" { ######################################################################## # Deny disabling/deleting protective services - # All roles (infra + app) can create/read/manage these services, - # but no role can disable or delete them. This lets the infra CI role - # manage GuardDuty/Config/SecurityHub while preventing any role from - # turning them off. App roles are further restricted by tag-based - # isolation on their Allow policy (they can't create these resources - # because they lack the platform tags). ######################################################################## { Sid = "DenyProtectiveServicesTampering" @@ -159,11 +153,6 @@ resource "aws_iam_policy" "developer_boundary" { ######################################################################## # Protect platform security groups - # - # Teams CAN create security groups (needed for RDS, custom services). - # Teams CANNOT modify or delete platform-owned security groups. - # Platform SGs are named javabin-* (e.g., javabin-alb-sg, javabin-ecs-tasks-sg). - # App SGs use app-name prefix (e.g., moresleep-rds-sg). ######################################################################## { Sid = "DenyPlatformSecurityGroups"