From e80bdb9be53fca1ff2e9c76748cf3a786c3c17fc Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:47:10 +0000 Subject: [PATCH] feat(iam): import hcptf roles into app Terraform (PLAT-146) (#56) * feat(iam): import hcptf roles into app Terraform (PLAT-146) Move the existing hcptf pair into this repo so app Terraform owns prod IAM after the substrate handoff. * fix(iam): add apply-role IAM list permissions (PLAT-146) IamReadOnly omitted ListRoleTags needed to refresh imported roles after detaching the substrate guardrail. --- terraform/hcp_iam.tf | 455 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 455 insertions(+) create mode 100644 terraform/hcp_iam.tf diff --git a/terraform/hcp_iam.tf b/terraform/hcp_iam.tf new file mode 100644 index 0000000..93f25b5 --- /dev/null +++ b/terraform/hcp_iam.tf @@ -0,0 +1,455 @@ +# HCP plan/apply roles imported from seahaven-terraform-substrate (PLAT-146). +# Import, do not recreate. Role names stay hcptf-seahaven-site / hcptf-seahaven-site-plan. +# +# Live seahaven-hcptf-iam-management DenySelfMutation blocks DetachRolePolicy +# and PutRolePolicy on hcptf-* (including this role). Import apply sequence: +# 1. seahaven-org-baseline scripts/create-hcptf-bootstrap-roles.sh +# --account prod --allow-workspace seahaven-site-prod +# 2. Point this workspace's TFC_AWS_* at hcptf-bootstrap / +# hcptf-bootstrap-plan (workspace vars, never a project set). +# 3. One Manual apply (import + detach seahaven-hcptf-iam-management + +# put scoped inline). +# 4. Point TFC_AWS_* back at hcptf-seahaven-site / hcptf-seahaven-site-plan. +# 5. Re-run the script without --allow-workspace to pin trust back to +# iam-bootstrap-prod only. +# This stack has no Lambda execution-role boundary pin. + +import { + to = aws_iam_role.hcptf_apply + id = "hcptf-seahaven-site" +} + +import { + to = aws_iam_role.hcptf_plan + id = "hcptf-seahaven-site-plan" +} + +import { + to = aws_iam_role_policy.hcptf_apply_services + id = "hcptf-seahaven-site:seahaven-site-services" +} + +import { + to = aws_iam_role_policy.hcptf_plan_refresh + id = "hcptf-seahaven-site-plan:seahaven-site-plan-refresh" +} + +import { + to = aws_iam_role_policy_attachments_exclusive.hcptf_apply + id = "hcptf-seahaven-site" +} + +import { + to = aws_iam_role_policy_attachment.hcptf_plan_viewonly + id = "hcptf-seahaven-site-plan/arn:aws:iam::aws:policy/job-function/ViewOnlyAccess" +} + +import { + to = aws_iam_role_policy_attachments_exclusive.hcptf_plan + id = "hcptf-seahaven-site-plan" +} + +data "aws_iam_policy_document" "hcptf_apply_trust" { + statement { + sid = "HcpApply" + effect = "Allow" + actions = ["sts:AssumeRoleWithWebIdentity"] + + principals { + type = "Federated" + identifiers = ["arn:aws:iam::${local.account_id}:oidc-provider/app.terraform.io"] + } + + condition { + test = "StringEquals" + variable = "app.terraform.io:aud" + values = ["aws.workload.identity"] + } + + condition { + test = "StringEquals" + variable = "app.terraform.io:sub" + values = [ + "organization:seahaven:project:seahaven-prod:workspace:seahaven-site-prod:run_phase:apply", + ] + } + } +} + +data "aws_iam_policy_document" "hcptf_plan_trust" { + statement { + sid = "HcpPlan" + effect = "Allow" + actions = ["sts:AssumeRoleWithWebIdentity"] + + principals { + type = "Federated" + identifiers = ["arn:aws:iam::${local.account_id}:oidc-provider/app.terraform.io"] + } + + condition { + test = "StringEquals" + variable = "app.terraform.io:aud" + values = ["aws.workload.identity"] + } + + condition { + test = "StringEquals" + variable = "app.terraform.io:sub" + values = [ + "organization:seahaven:project:seahaven-prod:workspace:seahaven-site-prod:run_phase:plan", + ] + } + } +} + +data "aws_iam_policy_document" "hcptf_scoped_iam" { + statement { + sid = "DenyCreatePolicy" + effect = "Deny" + actions = ["iam:CreatePolicy", "iam:CreatePolicyVersion"] + resources = ["*"] + } + + statement { + sid = "WriteDeployRoles" + effect = "Allow" + actions = [ + "iam:AttachRolePolicy", + "iam:DeleteRolePolicy", + "iam:DetachRolePolicy", + "iam:PutRolePolicy", + "iam:TagRole", + "iam:UntagRole", + "iam:UpdateAssumeRolePolicy", + "iam:UpdateRole", + "iam:UpdateRoleDescription", + ] + resources = ["arn:aws:iam::${local.account_id}:role/tf-managed/githubdeploy-seahaven-site"] + } + + statement { + sid = "IamReadOnly" + effect = "Allow" + actions = [ + "iam:GetPolicy", + "iam:GetPolicyVersion", + "iam:GetRole", + "iam:GetRolePolicy", + "iam:ListAttachedRolePolicies", + "iam:ListInstanceProfilesForRole", + "iam:ListPolicies", + "iam:ListPolicyVersions", + "iam:ListRolePolicies", + "iam:ListRoleTags", + "iam:ListRoles", + ] + resources = ["*"] + } + + statement { + sid = "DenySelfMutation" + effect = "Deny" + actions = [ + "iam:AttachRolePolicy", + "iam:DeleteRole", + "iam:DeleteRolePolicy", + "iam:DeleteRolePermissionsBoundary", + "iam:DetachRolePolicy", + "iam:PutRolePolicy", + "iam:PutRolePermissionsBoundary", + "iam:UpdateAssumeRolePolicy", + "iam:UpdateRole", + "iam:UpdateRoleDescription", + ] + resources = [ + "arn:aws:iam::${local.account_id}:role/hcptf-*", + "arn:aws:iam::${local.account_id}:role/github-cfn-execution-role", + "arn:aws:iam::${local.account_id}:role/githubdeploy-*", + "arn:aws:iam::${local.account_id}:role/cdk-hnb659fds-*", + "arn:aws:iam::${local.account_id}:role/OrganizationAccountAccessRole", + "arn:aws:iam::${local.account_id}:role/seahaven-*", + ] + } + + statement { + sid = "DenyBoundaryTampering" + effect = "Deny" + actions = [ + "iam:DeleteRolePermissionsBoundary", + "iam:DeleteUserPermissionsBoundary", + ] + resources = [ + "arn:aws:iam::${local.account_id}:role/*", + "arn:aws:iam::${local.account_id}:user/*", + ] + } + + statement { + sid = "DenyBoundaryPolicyEdit" + effect = "Deny" + actions = [ + "iam:CreatePolicyVersion", + "iam:DeletePolicy", + "iam:DeletePolicyVersion", + "iam:SetDefaultPolicyVersion", + ] + resources = ["arn:aws:iam::${local.account_id}:policy/seahaven-*"] + } +} + +resource "aws_iam_role_policy" "hcptf_apply_services" { + name = "seahaven-site-services" + role = aws_iam_role.hcptf_apply.id + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + "s3:*", + ] + Resource = [ + "arn:aws:s3:::seahaven-site-prod", + "arn:aws:s3:::seahaven-site-prod/*", + ] + Effect = "Allow" + Sid = "OriginBucket" + }, + { + Action = [ + "iam:GetOpenIDConnectProvider", + ] + Resource = [ + "arn:aws:iam::${local.account_id}:oidc-provider/token.actions.githubusercontent.com", + ] + Effect = "Allow" + Sid = "ReadGithubOidcProvider" + }, + { + Action = [ + "cloudfront:*", + ] + Resource = "*" + Effect = "Allow" + Sid = "CloudFrontManage" + }, + { + Condition = { + StringEquals = { + "aws:RequestTag/Project" = "seahaven-site" + } + } + Action = [ + "acm:RequestCertificate", + ] + Resource = "*" + Effect = "Allow" + Sid = "AcmCreate" + }, + { + Action = [ + "acm:ListCertificates", + "acm:ListTagsForCertificate", + ] + Resource = "*" + Effect = "Allow" + Sid = "AcmList" + }, + { + Condition = { + StringEquals = { + "aws:ResourceTag/Project" = "seahaven-site" + } + } + Action = [ + "acm:DescribeCertificate", + "acm:GetCertificate", + "acm:DeleteCertificate", + "acm:AddTagsToCertificate", + "acm:RemoveTagsFromCertificate", + "acm:RenewCertificate", + ] + Resource = "*" + Effect = "Allow" + Sid = "AcmManageTagged" + }, + { + Action = [ + "ssm:GetParameter", + "ssm:GetParameters", + ] + Resource = [ + "arn:aws:ssm:us-east-1:${local.account_id}:parameter/seahaven/waf/app-web-acl-arn", + ] + Effect = "Allow" + Sid = "ReadAppWebAclSsm" + }, + { + Action = [ + "wafv2:GetWebACL", + "wafv2:GetWebACLForResource", + "wafv2:ListWebACLs", + "wafv2:ListResourcesForWebACL", + ] + Resource = "*" + Effect = "Allow" + Sid = "ReadWafWebAcl" + }, + ] + }) +} + +resource "aws_iam_role_policy" "hcptf_plan_refresh" { + name = "seahaven-site-plan-refresh" + role = aws_iam_role.hcptf_plan.id + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + "iam:GetRole", + "iam:GetRolePolicy", + "iam:ListRolePolicies", + "iam:ListAttachedRolePolicies", + "iam:ListRoleTags", + ] + Resource = [ + "arn:aws:iam::${local.account_id}:role/tf-managed/githubdeploy-seahaven-site", + "arn:aws:iam::${local.account_id}:role/hcptf-seahaven-site", + "arn:aws:iam::${local.account_id}:role/hcptf-seahaven-site-plan", + ] + Effect = "Allow" + Sid = "RefreshDeployRole" + }, + { + Action = [ + "iam:GetOpenIDConnectProvider", + ] + Resource = [ + "arn:aws:iam::${local.account_id}:oidc-provider/token.actions.githubusercontent.com", + ] + Effect = "Allow" + Sid = "RefreshGithubOidcProvider" + }, + { + Action = [ + "iam:GetPolicy", + "iam:GetPolicyVersion", + ] + Resource = "*" + Effect = "Allow" + Sid = "RefreshManagedPolicies" + }, + { + Action = [ + "s3:Get*", + "s3:ListBucket", + ] + Resource = [ + "arn:aws:s3:::seahaven-site-prod", + "arn:aws:s3:::seahaven-site-prod/*", + ] + Effect = "Allow" + Sid = "RefreshOriginBucket" + }, + { + Action = [ + "cloudfront:Get*", + "cloudfront:List*", + ] + Resource = "*" + Effect = "Allow" + Sid = "RefreshCloudFront" + }, + { + Action = [ + "cloudfront:DescribeFunction", + ] + Resource = [ + "arn:aws:cloudfront::${local.account_id}:function/seahaven-site-prod-directory-index", + ] + Effect = "Allow" + Sid = "RefreshCloudFrontFunction" + }, + { + Action = [ + "acm:DescribeCertificate", + "acm:ListCertificates", + "acm:ListTagsForCertificate", + "acm:GetCertificate", + ] + Resource = "*" + Effect = "Allow" + Sid = "RefreshAcm" + }, + { + Action = [ + "ssm:GetParameter", + "ssm:GetParameters", + ] + Resource = [ + "arn:aws:ssm:us-east-1:${local.account_id}:parameter/seahaven/waf/app-web-acl-arn", + ] + Effect = "Allow" + Sid = "RefreshAppWebAclSsm" + }, + { + Action = [ + "wafv2:GetWebACL", + "wafv2:ListWebACLs", + ] + Resource = "*" + Effect = "Allow" + Sid = "RefreshWafWebAcl" + }, + ] + }) +} + +resource "aws_iam_role" "hcptf_apply" { + name = "hcptf-seahaven-site" + assume_role_policy = data.aws_iam_policy_document.hcptf_apply_trust.json + max_session_duration = 3600 + + tags = { + Project = "seahaven-site" + Owner = "adam@seahavenind.com" + ManagedBy = "terraform" + } +} + +# Empty exclusive set keeps seahaven-hcptf-iam-management detached. +resource "aws_iam_role_policy_attachments_exclusive" "hcptf_apply" { + role_name = aws_iam_role.hcptf_apply.name + policy_arns = [] +} + +resource "aws_iam_role" "hcptf_plan" { + name = "hcptf-seahaven-site-plan" + assume_role_policy = data.aws_iam_policy_document.hcptf_plan_trust.json + max_session_duration = 3600 + + tags = { + Project = "seahaven-site" + Owner = "adam@seahavenind.com" + ManagedBy = "terraform" + } +} + +resource "aws_iam_role_policy_attachment" "hcptf_plan_viewonly" { + role = aws_iam_role.hcptf_plan.name + policy_arn = "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess" +} + +resource "aws_iam_role_policy_attachments_exclusive" "hcptf_plan" { + role_name = aws_iam_role.hcptf_plan.name + policy_arns = [ + aws_iam_role_policy_attachment.hcptf_plan_viewonly.policy_arn, + ] +} + +resource "aws_iam_role_policy" "hcptf_scoped_iam" { + name = "scoped-iam-management" + role = aws_iam_role.hcptf_apply.id + policy = data.aws_iam_policy_document.hcptf_scoped_iam.json +}