Skip to content

Commit 87e10ca

Browse files
Move permission boundary to terraform/org/ (human-applied) (#82)
## Summary The permission boundary's self-protection (`DenyBoundaryTampering`) prevents the CI pipeline from modifying it — the `ci-infra` role carries the boundary and is blocked from `iam:CreatePolicyVersion` on the boundary policy itself. This PR adds the boundary resource to `terraform/org/` (human-applied with admin credentials) and marks the `platform/iam/boundary.tf` copy for removal after state migration. ## Migration steps (manual, with `--profile javabin`) **Step 1**: Merge this PR (CI plan will show no infra changes since the platform copy is unchanged) **Step 2**: Import into org state ```bash cd terraform/org terraform init terraform import aws_iam_policy.developer_boundary \ arn:aws:iam::553637109631:policy/javabin-developer-boundary terraform plan # verify no changes ``` **Step 3**: Remove from platform state ```bash cd terraform/platform terraform state rm module.iam.aws_iam_policy.developer_boundary ``` **Step 4**: Separate PR to replace `platform/iam/boundary.tf` resource with data source ## Why not remove the platform copy now? If we remove it before the state migration, CI will plan a **destroy** of the boundary policy, which would break all IAM roles in the account. The platform copy is kept as a safety net until the state move is complete. ## Test plan - [ ] CI plan shows no changes (platform boundary unchanged) - [ ] Manual: import into org state succeeds - [ ] Manual: org plan shows no changes after import - [ ] Manual: state rm from platform succeeds - [ ] Follow-up PR: replace with data source
1 parent 1980d74 commit 87e10ca

2 files changed

Lines changed: 253 additions & 16 deletions

File tree

‎terraform/org/boundary.tf‎

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
################################################################################
2+
# Permission Boundary: javabin-developer-boundary
3+
#
4+
# This is the cornerstone of the IAM security model. Any role carrying this
5+
# boundary can only create roles that also carry it (self-replicating).
6+
#
7+
# The boundary defines the maximum possible permissions for any non-platform
8+
# role. Hard denies here cannot be overridden by any policy attached to
9+
# roles that carry this boundary.
10+
#
11+
# IMPORTANT: This resource lives in terraform/org/ (human-applied) because
12+
# the boundary's self-protection denies iam:CreatePolicyVersion on itself.
13+
# The CI pipeline (which carries the boundary) cannot modify it.
14+
################################################################################
15+
16+
resource "aws_iam_policy" "developer_boundary" {
17+
name = "${var.project}-developer-boundary"
18+
description = "Permission boundary for all non-platform roles. Self-replicating: roles with this boundary can only create roles that also carry it."
19+
20+
policy = jsonencode({
21+
Version = "2012-10-17"
22+
Statement = [
23+
########################################################################
24+
# Allow everything not explicitly denied below
25+
########################################################################
26+
{
27+
Sid = "AllowAll"
28+
Effect = "Allow"
29+
Action = "*"
30+
Resource = "*"
31+
},
32+
33+
########################################################################
34+
# Self-replicating: deny creating/modifying roles without this boundary
35+
########################################################################
36+
{
37+
Sid = "DenyRolesWithoutBoundary"
38+
Effect = "Deny"
39+
Action = [
40+
"iam:CreateRole",
41+
"iam:PutRolePermissionsBoundary"
42+
]
43+
Resource = "*"
44+
Condition = {
45+
StringNotEquals = {
46+
"iam:PermissionsBoundary" = "arn:aws:iam::${var.aws_account_id}:policy/${var.project}-developer-boundary"
47+
}
48+
}
49+
},
50+
51+
########################################################################
52+
# Deny creating IAM users and access keys (console/programmatic)
53+
########################################################################
54+
{
55+
Sid = "DenyIAMUserCreation"
56+
Effect = "Deny"
57+
Action = [
58+
"iam:CreateUser",
59+
"iam:CreateLoginProfile",
60+
"iam:UpdateLoginProfile",
61+
"iam:CreateAccessKey",
62+
"iam:DeleteAccountPasswordPolicy",
63+
"iam:CreateVirtualMFADevice",
64+
"iam:DeactivateMFADevice"
65+
]
66+
Resource = "*"
67+
},
68+
69+
########################################################################
70+
# Deny modifying or deleting this boundary policy itself
71+
########################################################################
72+
{
73+
Sid = "DenyBoundaryTampering"
74+
Effect = "Deny"
75+
Action = [
76+
"iam:DeletePolicy",
77+
"iam:DeletePolicyVersion",
78+
"iam:CreatePolicyVersion",
79+
"iam:SetDefaultPolicyVersion"
80+
]
81+
Resource = "arn:aws:iam::${var.aws_account_id}:policy/${var.project}-developer-boundary"
82+
},
83+
{
84+
Sid = "DenyDeleteRoleBoundary"
85+
Effect = "Deny"
86+
Action = [
87+
"iam:DeleteRolePermissionsBoundary"
88+
]
89+
Resource = "*"
90+
},
91+
92+
########################################################################
93+
# Deny IAM Identity Center, Organizations, SCPs
94+
########################################################################
95+
{
96+
Sid = "DenyIdentityCenterAndOrgs"
97+
Effect = "Deny"
98+
Action = [
99+
"organizations:*",
100+
"sso:*",
101+
"sso-directory:*",
102+
"identitystore:*",
103+
"account:*"
104+
]
105+
Resource = "*"
106+
},
107+
108+
########################################################################
109+
# Deny disabling/deleting protective services
110+
########################################################################
111+
{
112+
Sid = "DenyProtectiveServicesTampering"
113+
Effect = "Deny"
114+
Action = [
115+
"guardduty:DeleteDetector",
116+
"guardduty:DeleteMembers",
117+
"guardduty:DisassociateFromMasterAccount",
118+
"guardduty:UpdateDetector",
119+
"config:DeleteConfigurationRecorder",
120+
"config:StopConfigurationRecorder",
121+
"config:DeleteDeliveryChannel",
122+
"config:DeleteRetentionConfiguration",
123+
"securityhub:DisableSecurityHub",
124+
"securityhub:DeleteMembers",
125+
"securityhub:DisassociateFromMasterAccount",
126+
"cloudtrail:DeleteTrail",
127+
"cloudtrail:StopLogging",
128+
"cloudtrail:UpdateTrail",
129+
"cloudtrail:PutEventSelectors"
130+
]
131+
Resource = "*"
132+
},
133+
134+
########################################################################
135+
# Deny platform networking: VPC, subnets, IGW, NAT gateway
136+
########################################################################
137+
{
138+
Sid = "DenyPlatformNetworking"
139+
Effect = "Deny"
140+
Action = [
141+
"ec2:CreateVpc",
142+
"ec2:DeleteVpc",
143+
"ec2:ModifyVpcAttribute",
144+
"ec2:CreateSubnet",
145+
"ec2:DeleteSubnet",
146+
"ec2:CreateInternetGateway",
147+
"ec2:DeleteInternetGateway",
148+
"ec2:AttachInternetGateway",
149+
"ec2:DetachInternetGateway",
150+
"ec2:CreateNatGateway",
151+
"ec2:DeleteNatGateway",
152+
"ec2:CreateRouteTable",
153+
"ec2:DeleteRouteTable"
154+
]
155+
Resource = "*"
156+
},
157+
158+
########################################################################
159+
# Protect platform security groups
160+
#
161+
# Teams CAN create security groups (needed for RDS, custom services).
162+
# Teams CANNOT modify or delete platform-owned security groups.
163+
# Platform SGs are named javabin-* (e.g., javabin-alb-sg, javabin-ecs-tasks-sg).
164+
# App SGs use app-name prefix (e.g., moresleep-rds-sg).
165+
########################################################################
166+
{
167+
Sid = "DenyPlatformSecurityGroups"
168+
Effect = "Deny"
169+
Action = [
170+
"ec2:DeleteSecurityGroup",
171+
"ec2:AuthorizeSecurityGroupIngress",
172+
"ec2:RevokeSecurityGroupIngress",
173+
"ec2:AuthorizeSecurityGroupEgress",
174+
"ec2:RevokeSecurityGroupEgress",
175+
"ec2:ModifySecurityGroupRules",
176+
]
177+
Resource = "arn:aws:ec2:${var.region}:${var.aws_account_id}:security-group/*"
178+
Condition = {
179+
StringLike = {
180+
"ec2:ResourceTag/Name" = "${var.project}-*"
181+
}
182+
}
183+
},
184+
185+
########################################################################
186+
# Deny platform ECS cluster, ALB, ACM certs
187+
########################################################################
188+
{
189+
Sid = "DenyPlatformECSCluster"
190+
Effect = "Deny"
191+
Action = [
192+
"ecs:DeleteCluster",
193+
"ecs:UpdateCluster"
194+
]
195+
Resource = "arn:aws:ecs:${var.region}:${var.aws_account_id}:cluster/${var.project}-platform"
196+
},
197+
{
198+
Sid = "DenyPlatformALB"
199+
Effect = "Deny"
200+
Action = [
201+
"elasticloadbalancing:DeleteLoadBalancer",
202+
"elasticloadbalancing:ModifyLoadBalancerAttributes"
203+
]
204+
Resource = "arn:aws:elasticloadbalancing:${var.region}:${var.aws_account_id}:loadbalancer/app/${var.project}-*"
205+
},
206+
{
207+
Sid = "DenyPlatformACM"
208+
Effect = "Deny"
209+
Action = [
210+
"acm:DeleteCertificate"
211+
]
212+
Resource = "arn:aws:acm:${var.region}:${var.aws_account_id}:certificate/*"
213+
},
214+
215+
########################################################################
216+
# Deny access to state and CI artifact buckets/tables
217+
########################################################################
218+
{
219+
Sid = "DenyStateBuckets"
220+
Effect = "Deny"
221+
Action = [
222+
"s3:DeleteBucket",
223+
"s3:PutBucketPolicy",
224+
"s3:DeleteBucketPolicy",
225+
"s3:PutBucketVersioning",
226+
"s3:PutEncryptionConfiguration"
227+
]
228+
Resource = [
229+
"arn:aws:s3:::${var.project}-terraform-*",
230+
"arn:aws:s3:::${var.project}-ci-*"
231+
]
232+
},
233+
{
234+
Sid = "DenyStateTables"
235+
Effect = "Deny"
236+
Action = [
237+
"dynamodb:DeleteTable",
238+
"dynamodb:UpdateTable"
239+
]
240+
Resource = "arn:aws:dynamodb:${var.region}:${var.aws_account_id}:table/${var.project}-terraform-*"
241+
}
242+
]
243+
})
244+
245+
tags = {
246+
Name = "${var.project}-developer-boundary"
247+
}
248+
}

‎terraform/platform/iam/boundary.tf‎

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
################################################################################
22
# Permission Boundary: javabin-developer-boundary
33
#
4-
# This is the cornerstone of the IAM security model. Any role carrying this
5-
# boundary can only create roles that also carry it (self-replicating).
4+
# MIGRATION NOTE: This resource is being moved to terraform/org/boundary.tf
5+
# because the boundary's self-protection (DenyBoundaryTampering) prevents
6+
# the CI pipeline from modifying it. See docs/org-runbook.md for migration steps.
67
#
7-
# The boundary defines the maximum possible permissions for any non-platform
8-
# role. Hard denies here cannot be overridden by any policy attached to
9-
# roles that carry this boundary.
8+
# Once the state migration is complete, this file will be replaced with a
9+
# data source reference. Until then, keep this resource to avoid CI destroying it.
1010
################################################################################
1111

1212
resource "aws_iam_policy" "developer_boundary" {
@@ -103,12 +103,6 @@ resource "aws_iam_policy" "developer_boundary" {
103103

104104
########################################################################
105105
# Deny disabling/deleting protective services
106-
# All roles (infra + app) can create/read/manage these services,
107-
# but no role can disable or delete them. This lets the infra CI role
108-
# manage GuardDuty/Config/SecurityHub while preventing any role from
109-
# turning them off. App roles are further restricted by tag-based
110-
# isolation on their Allow policy (they can't create these resources
111-
# because they lack the platform tags).
112106
########################################################################
113107
{
114108
Sid = "DenyProtectiveServicesTampering"
@@ -159,11 +153,6 @@ resource "aws_iam_policy" "developer_boundary" {
159153

160154
########################################################################
161155
# Protect platform security groups
162-
#
163-
# Teams CAN create security groups (needed for RDS, custom services).
164-
# Teams CANNOT modify or delete platform-owned security groups.
165-
# Platform SGs are named javabin-* (e.g., javabin-alb-sg, javabin-ecs-tasks-sg).
166-
# App SGs use app-name prefix (e.g., moresleep-rds-sg).
167156
########################################################################
168157
{
169158
Sid = "DenyPlatformSecurityGroups"

0 commit comments

Comments
 (0)