From 6bfd231ebcaad6b54de769b9433c39613757a875 Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Wed, 2 Sep 2026 11:33:17 -0400 Subject: [PATCH 1/6] feat(iam): import hcptf roles into app Terraform (PLAT-146) Move the existing prod plan/apply pair into this repo. First apply uses the bootstrap window because DenySelfMutation blocks self-detach of seahaven-hcptf-iam-management. --- terraform/hcp_iam.tf | 396 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 396 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..cbe7be6 --- /dev/null +++ b/terraform/hcp_iam.tf @@ -0,0 +1,396 @@ +# HCP plan/apply roles imported from seahaven-terraform-substrate (PLAT-146). +# Import, do not recreate. Role names stay hcptf-afi-backup-monitor / +# hcptf-afi-backup-monitor-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 afi-backup-monitor-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-afi-backup-monitor / +# hcptf-afi-backup-monitor-plan. +# 5. Re-run the script without --allow-workspace to pin trust back to +# iam-bootstrap-prod only. +# Lambda permissions_boundary remains +# seahaven-lambda-execution-boundary-afi-backup-monitor. + +import { + to = aws_iam_role.hcptf_apply + id = "hcptf-afi-backup-monitor" +} + +import { + to = aws_iam_role.hcptf_plan + id = "hcptf-afi-backup-monitor-plan" +} + +import { + to = aws_iam_role_policy.hcptf_apply_services + id = "hcptf-afi-backup-monitor:afi-backup-monitor-services" +} + +import { + to = aws_iam_role_policy.hcptf_plan_refresh + id = "hcptf-afi-backup-monitor-plan:afi-backup-monitor-plan-refresh" +} + +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:afi-backup-monitor-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:afi-backup-monitor-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 = "CreateExecRoleWithBoundary" + effect = "Allow" + actions = ["iam:CreateRole"] + resources = ["arn:aws:iam::${local.account_id}:role/tf-managed/afi-*"] + + condition { + test = "StringLike" + variable = "iam:PermissionsBoundary" + values = [ + "arn:aws:iam::${local.account_id}:policy/tf-managed/afi-*", + "arn:aws:iam::${local.account_id}:policy/seahaven-lambda-execution-boundary", + "arn:aws:iam::${local.account_id}:policy/seahaven-lambda-execution-boundary-afi-backup-monitor", + ] + } + } + + statement { + sid = "MutateExecRoleWithBoundary" + effect = "Allow" + actions = [ + "iam:AttachRolePolicy", + "iam:PutRolePolicy", + "iam:PutRolePermissionsBoundary", + ] + resources = ["arn:aws:iam::${local.account_id}:role/tf-managed/afi-*"] + + condition { + test = "StringLike" + variable = "iam:PermissionsBoundary" + values = [ + "arn:aws:iam::${local.account_id}:policy/tf-managed/afi-*", + "arn:aws:iam::${local.account_id}:policy/seahaven-lambda-execution-boundary", + "arn:aws:iam::${local.account_id}:policy/seahaven-lambda-execution-boundary-afi-backup-monitor", + ] + } + } + + statement { + sid = "WriteExecRoles" + effect = "Allow" + actions = [ + "iam:DeleteRole", + "iam:DeleteRolePolicy", + "iam:DetachRolePolicy", + "iam:TagRole", + "iam:UntagRole", + "iam:UpdateAssumeRolePolicy", + "iam:UpdateRole", + "iam:UpdateRoleDescription", + ] + resources = ["arn:aws:iam::${local.account_id}:role/tf-managed/afi-*"] + } + + statement { + sid = "PassExecRolesToLambda" + effect = "Allow" + actions = ["iam:PassRole"] + resources = ["arn:aws:iam::${local.account_id}:role/tf-managed/afi-*"] + + condition { + test = "StringEquals" + variable = "iam:PassedToService" + values = ["lambda.amazonaws.com"] + } + } + + statement { + sid = "IamReadOnly" + effect = "Allow" + actions = [ + "iam:GetPolicy", + "iam:GetPolicyVersion", + "iam:GetRole", + "iam:GetRolePolicy", + "iam:ListAttachedRolePolicies", + "iam:ListPolicies", + "iam:ListPolicyVersions", + "iam:ListRolePolicies", + "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-*"] + } +} + +data "aws_iam_policy_document" "hcptf_apply_services" { + statement { + sid = "LambdaAll" + effect = "Allow" + actions = ["lambda:*"] + resources = [ + "arn:aws:lambda:us-east-1:${local.account_id}:function:afi-*", + "arn:aws:lambda:us-east-1:${local.account_id}:layer:afi-shared*", + ] + } + + statement { + sid = "LambdaList" + effect = "Allow" + actions = ["lambda:ListFunctions", "lambda:ListLayers", "lambda:GetAccountSettings"] + resources = ["*"] + } + + statement { + sid = "EventBridgeRules" + effect = "Allow" + actions = ["events:*"] + resources = ["arn:aws:events:us-east-1:${local.account_id}:rule/afi-*"] + } + + statement { + sid = "CloudWatchLogs" + effect = "Allow" + actions = [ + "logs:CreateLogGroup", + "logs:DeleteLogGroup", + "logs:PutRetentionPolicy", + "logs:DeleteRetentionPolicy", + "logs:TagResource", + "logs:UntagResource", + "logs:ListTagsForResource", + ] + resources = ["arn:aws:logs:us-east-1:${local.account_id}:log-group:/aws/lambda/afi-*"] + } + + statement { + sid = "CloudWatchLogsDescribe" + effect = "Allow" + actions = ["logs:DescribeLogGroups"] + resources = ["*"] + } + + statement { + sid = "LambdaArtifactsBucket" + effect = "Allow" + actions = ["s3:*"] + resources = [ + "arn:aws:s3:::afi-backup-monitor-artifacts-${local.account_id}", + "arn:aws:s3:::afi-backup-monitor-artifacts-${local.account_id}/*", + ] + } +} + +data "aws_iam_policy_document" "hcptf_plan_refresh" { + statement { + sid = "RefreshIamRoles" + effect = "Allow" + actions = [ + "iam:GetRole", + "iam:GetRolePolicy", + "iam:ListRolePolicies", + "iam:ListAttachedRolePolicies", + ] + resources = ["arn:aws:iam::${local.account_id}:role/tf-managed/afi-*"] + } + + statement { + sid = "RefreshManagedPolicies" + effect = "Allow" + actions = ["iam:GetPolicy", "iam:GetPolicyVersion"] + resources = ["*"] + } + + statement { + sid = "RefreshEventBridge" + effect = "Allow" + actions = [ + "events:DescribeRule", + "events:ListTargetsByRule", + "events:ListTagsForResource", + ] + resources = ["arn:aws:events:us-east-1:${local.account_id}:rule/afi-*"] + } + + statement { + sid = "RefreshLambda" + effect = "Allow" + actions = ["lambda:*"] + resources = [ + "arn:aws:lambda:us-east-1:${local.account_id}:function:afi-*", + "arn:aws:lambda:us-east-1:${local.account_id}:layer:afi-shared*", + ] + } + + statement { + sid = "RefreshArtifactsBucket" + effect = "Allow" + actions = ["s3:Get*", "s3:ListBucket"] + resources = [ + "arn:aws:s3:::afi-backup-monitor-artifacts-${local.account_id}", + "arn:aws:s3:::afi-backup-monitor-artifacts-${local.account_id}/*", + ] + } + + statement { + sid = "RefreshLogs" + effect = "Allow" + actions = ["logs:DescribeLogGroups", "logs:ListTagsForResource"] + resources = ["*"] + } +} + +resource "aws_iam_role" "hcptf_apply" { + name = "hcptf-afi-backup-monitor" + assume_role_policy = data.aws_iam_policy_document.hcptf_apply_trust.json + max_session_duration = 3600 + managed_policy_arns = [] + + tags = { + Project = "afi-backup-monitor" + Owner = "adam@seahavenind.com" + ManagedBy = "terraform" + } +} + +resource "aws_iam_role" "hcptf_plan" { + name = "hcptf-afi-backup-monitor-plan" + assume_role_policy = data.aws_iam_policy_document.hcptf_plan_trust.json + max_session_duration = 3600 + managed_policy_arns = ["arn:aws:iam::aws:policy/job-function/ViewOnlyAccess"] + + tags = { + Project = "afi-backup-monitor" + Owner = "adam@seahavenind.com" + ManagedBy = "terraform" + } +} + +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 +} + +resource "aws_iam_role_policy" "hcptf_apply_services" { + name = "afi-backup-monitor-services" + role = aws_iam_role.hcptf_apply.id + policy = data.aws_iam_policy_document.hcptf_apply_services.json +} + +resource "aws_iam_role_policy" "hcptf_plan_refresh" { + name = "afi-backup-monitor-plan-refresh" + role = aws_iam_role.hcptf_plan.id + policy = data.aws_iam_policy_document.hcptf_plan_refresh.json +} From 3b5db56f377d6d8ecd66004c2645260877ea30a4 Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Wed, 2 Sep 2026 11:41:19 -0400 Subject: [PATCH 2/6] fix(iam): let the plan role refresh imported hcptf roles (PLAT-146) The scoped plan-refresh sidecar only covered tf-managed/afi-*. After import, plans need GetRole on the hcptf pair in this workspace. --- terraform/hcp_iam.tf | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/terraform/hcp_iam.tf b/terraform/hcp_iam.tf index cbe7be6..963dac5 100644 --- a/terraform/hcp_iam.tf +++ b/terraform/hcp_iam.tf @@ -302,7 +302,11 @@ data "aws_iam_policy_document" "hcptf_plan_refresh" { "iam:ListRolePolicies", "iam:ListAttachedRolePolicies", ] - resources = ["arn:aws:iam::${local.account_id}:role/tf-managed/afi-*"] + resources = [ + "arn:aws:iam::${local.account_id}:role/tf-managed/afi-*", + "arn:aws:iam::${local.account_id}:role/hcptf-afi-backup-monitor", + "arn:aws:iam::${local.account_id}:role/hcptf-afi-backup-monitor-plan", + ] } statement { From 3240b9e47f6ce4ee7612d0fa5dc22080b7c33661 Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Wed, 2 Sep 2026 11:53:55 -0400 Subject: [PATCH 3/6] style(iam): terraform fmt hcp_iam.tf (PLAT-146) CI terraform fmt -check failed on alignment in the apply-services document. --- terraform/hcp_iam.tf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/terraform/hcp_iam.tf b/terraform/hcp_iam.tf index 963dac5..365b127 100644 --- a/terraform/hcp_iam.tf +++ b/terraform/hcp_iam.tf @@ -236,9 +236,9 @@ data "aws_iam_policy_document" "hcptf_scoped_iam" { data "aws_iam_policy_document" "hcptf_apply_services" { statement { - sid = "LambdaAll" - effect = "Allow" - actions = ["lambda:*"] + sid = "LambdaAll" + effect = "Allow" + actions = ["lambda:*"] resources = [ "arn:aws:lambda:us-east-1:${local.account_id}:function:afi-*", "arn:aws:lambda:us-east-1:${local.account_id}:layer:afi-shared*", From 7fbff09f6607a6eee4a37ee043563e7e439f930b Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Wed, 2 Sep 2026 12:19:46 -0400 Subject: [PATCH 4/6] fix(iam): tighten plan-refresh and scoped boundary pin (PLAT-146) Plan-refresh copied lambda:* from apply, so a plan session could mutate afi functions. Drop the unsuffixed org-wide Lambda boundary so scoped apply cannot retarget exec roles onto that ceiling. --- terraform/hcp_iam.tf | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/terraform/hcp_iam.tf b/terraform/hcp_iam.tf index 365b127..040d56b 100644 --- a/terraform/hcp_iam.tf +++ b/terraform/hcp_iam.tf @@ -110,7 +110,6 @@ data "aws_iam_policy_document" "hcptf_scoped_iam" { variable = "iam:PermissionsBoundary" values = [ "arn:aws:iam::${local.account_id}:policy/tf-managed/afi-*", - "arn:aws:iam::${local.account_id}:policy/seahaven-lambda-execution-boundary", "arn:aws:iam::${local.account_id}:policy/seahaven-lambda-execution-boundary-afi-backup-monitor", ] } @@ -131,7 +130,6 @@ data "aws_iam_policy_document" "hcptf_scoped_iam" { variable = "iam:PermissionsBoundary" values = [ "arn:aws:iam::${local.account_id}:policy/tf-managed/afi-*", - "arn:aws:iam::${local.account_id}:policy/seahaven-lambda-execution-boundary", "arn:aws:iam::${local.account_id}:policy/seahaven-lambda-execution-boundary-afi-backup-monitor", ] } @@ -328,9 +326,10 @@ data "aws_iam_policy_document" "hcptf_plan_refresh" { } statement { - sid = "RefreshLambda" - effect = "Allow" - actions = ["lambda:*"] + sid = "RefreshLambda" + effect = "Allow" + # Read-only refresh for plan. Mutate APIs stay on the apply role. + actions = ["lambda:Get*", "lambda:List*"] resources = [ "arn:aws:lambda:us-east-1:${local.account_id}:function:afi-*", "arn:aws:lambda:us-east-1:${local.account_id}:layer:afi-shared*", From d2b7857e3ac5cef89b6b40d535313c31f6fa8c0a Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Wed, 2 Sep 2026 12:26:00 -0400 Subject: [PATCH 5/6] fix(iam): replace deprecated managed_policy_arns (PLAT-146) Keep exclusive attachment control so the apply role stays empty and the plan role keeps ViewOnlyAccess. --- terraform/hcp_iam.tf | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/terraform/hcp_iam.tf b/terraform/hcp_iam.tf index 040d56b..79e2c10 100644 --- a/terraform/hcp_iam.tf +++ b/terraform/hcp_iam.tf @@ -37,6 +37,21 @@ import { id = "hcptf-afi-backup-monitor-plan:afi-backup-monitor-plan-refresh" } +import { + to = aws_iam_role_policy_attachments_exclusive.hcptf_apply + id = "hcptf-afi-backup-monitor" +} + +import { + to = aws_iam_role_policy_attachment.hcptf_plan_viewonly + id = "hcptf-afi-backup-monitor-plan/arn:aws:iam::aws:policy/job-function/ViewOnlyAccess" +} + +import { + to = aws_iam_role_policy_attachments_exclusive.hcptf_plan + id = "hcptf-afi-backup-monitor-plan" +} + data "aws_iam_policy_document" "hcptf_apply_trust" { statement { sid = "HcpApply" @@ -358,7 +373,6 @@ resource "aws_iam_role" "hcptf_apply" { name = "hcptf-afi-backup-monitor" assume_role_policy = data.aws_iam_policy_document.hcptf_apply_trust.json max_session_duration = 3600 - managed_policy_arns = [] tags = { Project = "afi-backup-monitor" @@ -367,11 +381,16 @@ resource "aws_iam_role" "hcptf_apply" { } } +# 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-afi-backup-monitor-plan" assume_role_policy = data.aws_iam_policy_document.hcptf_plan_trust.json max_session_duration = 3600 - managed_policy_arns = ["arn:aws:iam::aws:policy/job-function/ViewOnlyAccess"] tags = { Project = "afi-backup-monitor" @@ -380,6 +399,18 @@ resource "aws_iam_role" "hcptf_plan" { } } +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 From a8b91976edb6042c2c1725b397243aa6796426d7 Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Wed, 2 Sep 2026 12:52:05 -0400 Subject: [PATCH 6/6] ci(iam): retrigger HCP commit status (PLAT-146)