@@ -295,3 +295,144 @@ resource "aws_iam_policy" "permissions_boundary" {
295295 }
296296 )
297297}
298+
299+ data "aws_iam_policy_document" "iam_bootstrap_permissions_boundary" {
300+ # Allow IAM operations on project-scoped resources
301+ statement {
302+ sid = " AllowProjectIamOperations"
303+ effect = " Allow"
304+ actions = [
305+ " iam:GetRole*" ,
306+ " iam:GetPolicy*" ,
307+ " iam:ListRole*" ,
308+ " iam:ListPolicies" ,
309+ " iam:ListAttachedRolePolicies" ,
310+ " iam:ListPolicyVersions" ,
311+ " iam:ListPolicyTags" ,
312+ " iam:ListOpenIDConnectProviders" ,
313+ " iam:ListOpenIDConnectProviderTags" ,
314+ " iam:GetOpenIDConnectProvider" ,
315+ " iam:CreateRole" ,
316+ " iam:DeleteRole" ,
317+ " iam:UpdateRole" ,
318+ " iam:UpdateAssumeRolePolicy" ,
319+ " iam:PutRolePolicy" ,
320+ " iam:PutRolePermissionsBoundary" ,
321+ " iam:AttachRolePolicy" ,
322+ " iam:DetachRolePolicy" ,
323+ " iam:CreatePolicy*" ,
324+ " iam:DeletePolicy*" ,
325+ " iam:TagRole" ,
326+ " iam:TagPolicy" ,
327+ " iam:UntagRole" ,
328+ " iam:UntagPolicy" ,
329+ " iam:PassRole" ,
330+ " iam:CreateServiceLinkedRole" ,
331+ " iam:TagOpenIDConnectProvider" ,
332+ " iam:UntagOpenIDConnectProvider" ,
333+ " iam:CreateOpenIDConnectProvider" ,
334+ " iam:DeleteOpenIDConnectProvider" ,
335+ " iam:UpdateOpenIDConnectProviderThumbprint" ,
336+ " iam:AddClientIDToOpenIDConnectProvider" ,
337+ " iam:RemoveClientIDFromOpenIDConnectProvider" ,
338+ ]
339+ resources = [
340+ " arn:aws:iam::${ data . aws_caller_identity . current . account_id } :role/service-roles/github-actions-api-deployment-role" ,
341+ " arn:aws:iam::${ data . aws_caller_identity . current . account_id } :role/service-roles/github-actions-iam-bootstrap-role" ,
342+ " arn:aws:iam::${ data . aws_caller_identity . current . account_id } :role/${ var . project_name } -terraform-developer-role" ,
343+ " arn:aws:iam::${ data . aws_caller_identity . current . account_id } :role/terraform-developer-role" ,
344+ " arn:aws:iam::${ data . aws_caller_identity . current . account_id } :policy/${ upper (var. project_name )} -*" ,
345+ " arn:aws:iam::${ data . aws_caller_identity . current . account_id } :policy/${ lower (var. project_name )} -*" ,
346+ " arn:aws:iam::${ data . aws_caller_identity . current . account_id } :policy/service-policies/*" ,
347+ " arn:aws:iam::${ data . aws_caller_identity . current . account_id } :policy/${ local . stack_name } -*" ,
348+ " arn:aws:iam::${ data . aws_caller_identity . current . account_id } :oidc-provider/token.actions.githubusercontent.com" ,
349+ ]
350+ }
351+
352+ # Allow read-only IAM access for Terraform plan/state discovery
353+ statement {
354+ sid = " AllowIamReadAccess"
355+ effect = " Allow"
356+ actions = [
357+ " iam:Get*" ,
358+ " iam:List*" ,
359+ ]
360+ resources = [" *" ]
361+ }
362+
363+ # Allow Terraform state bucket access
364+ statement {
365+ sid = " AllowTerraformStateAccess"
366+ effect = " Allow"
367+ actions = [
368+ " s3:ListBucket" ,
369+ " s3:GetObject" ,
370+ " s3:PutObject" ,
371+ " s3:DeleteObject" ,
372+ ]
373+ resources = [
374+ " ${ local . terraform_state_bucket_arn } " ,
375+ " ${ local . terraform_state_bucket_arn } /*" ,
376+ ]
377+ }
378+
379+ # Allow Terraform state locking via DynamoDB
380+ statement {
381+ sid = " AllowTerraformStateLocking"
382+ effect = " Allow"
383+ actions = [
384+ " dynamodb:GetItem" ,
385+ " dynamodb:PutItem" ,
386+ " dynamodb:DeleteItem" ,
387+ ]
388+ resources = [
389+ " arn:aws:dynamodb:${ var . default_aws_region } :${ data . aws_caller_identity . current . account_id } :table/${ var . project_name } -*-terraform-lock" ,
390+ ]
391+ }
392+
393+ # DENY: Prevent the bootstrap role from modifying its own policies
394+ statement {
395+ sid = " DenyBootstrapSelfModification"
396+ effect = " Deny"
397+ actions = [
398+ " iam:AttachRolePolicy" ,
399+ " iam:DetachRolePolicy" ,
400+ " iam:PutRolePolicy" ,
401+ " iam:DeleteRolePolicy" ,
402+ " iam:UpdateAssumeRolePolicy" ,
403+ " iam:PutRolePermissionsBoundary" ,
404+ " iam:DeleteRolePermissionsBoundary" ,
405+ ]
406+ resources = [
407+ " arn:aws:iam::${ data . aws_caller_identity . current . account_id } :role/service-roles/github-actions-iam-bootstrap-role" ,
408+ ]
409+ }
410+
411+ # DENY: Prevent the bootstrap role from modifying its own permissions boundary
412+ statement {
413+ sid = " DenyBootstrapBoundaryModification"
414+ effect = " Deny"
415+ actions = [
416+ " iam:CreatePolicyVersion" ,
417+ " iam:DeletePolicy" ,
418+ " iam:DeletePolicyVersion" ,
419+ " iam:SetDefaultPolicyVersion" ,
420+ ]
421+ resources = [
422+ " arn:aws:iam::${ data . aws_caller_identity . current . account_id } :policy/${ lower (var. project_name )} -iam-bootstrap-permissions-boundary" ,
423+ ]
424+ }
425+ }
426+
427+ resource "aws_iam_policy" "iam_bootstrap_permissions_boundary" {
428+ name = " ${ lower (var. project_name )} -iam-bootstrap-permissions-boundary"
429+ description = " Permissions boundary for the GitHub Actions IAM Bootstrap role - scoped to IAM and Terraform state only"
430+ policy = data. aws_iam_policy_document . iam_bootstrap_permissions_boundary . json
431+
432+ tags = merge (
433+ local. tags ,
434+ {
435+ Stack = " iams-developer-roles"
436+ }
437+ )
438+ }
0 commit comments